From 75b00bb2cc8b91973ca06bd760fd9582d4335a79 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 2 Nov 2022 23:17:16 -0400 Subject: [PATCH 001/466] import vazor222's XWI branch Initial import from https://github.com/vazor222/fs2open.github.com/tree/feature/vz_xwi_import. The only changes so far are source code deconflictions due to rebasing the branch on latest master. squashed commits: 48a2472bd021fd3867464ba7a2549de2bc2d306a import vazor222's XWI branch 99f6d1c0cd24751e3233f288a3fcff8ff7e08598 a bunch of tweaks b66235faf2e1ffa38709cb4644d00f1df67f70b9 more tweaking 9c15c56990cc78933eb1f26c47d23bb38dea247a even more tweaking, and now it compiles --- code/mission/missionparse.cpp | 93 +++-- code/mission/missionparse.h | 1 + code/mission/xwingbrflib.cpp | 33 ++ code/mission/xwingbrflib.h | 73 ++++ code/mission/xwinglib.cpp | 589 +++++++++++++++++++++++++++++ code/mission/xwinglib.h | 206 ++++++++++ code/mission/xwingmissionparse.cpp | 167 ++++++++ code/mission/xwingmissionparse.h | 11 + code/source_groups.cmake | 6 + fred2/fred.rc | 1 + fred2/freddoc.cpp | 129 ++++++- fred2/freddoc.h | 6 + fred2/resource.h | 3 +- 13 files changed, 1291 insertions(+), 27 deletions(-) create mode 100644 code/mission/xwingbrflib.cpp create mode 100644 code/mission/xwingbrflib.h create mode 100644 code/mission/xwinglib.cpp create mode 100644 code/mission/xwinglib.h create mode 100644 code/mission/xwingmissionparse.cpp create mode 100644 code/mission/xwingmissionparse.h diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 513789901e8..7723d9aaf69 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -43,6 +43,9 @@ #include "mission/missionlog.h" #include "mission/missionmessage.h" #include "mission/missionparse.h" +#include "mission/xwingbrflib.h" +#include "mission/xwinglib.h" +#include "mission/xwingmissionparse.h" #include "missionui/fictionviewer.h" #include "missionui/missioncmdbrief.h" #include "missionui/redalert.h" @@ -5985,7 +5988,7 @@ void parse_sexp_containers() } } -bool parse_mission(mission *pm, int flags) +bool parse_mission(mission *pm, XWingMission *xwim, int flags) { int saved_warning_count = Global_warning_count; int saved_error_count = Global_error_count; @@ -6000,33 +6003,43 @@ bool parse_mission(mission *pm, int flags) reset_parse(); mission_init(pm); - parse_mission_info(pm); + if (flags & MPF_IMPORT_XWI) + parse_xwi_mission_info(pm, xwim); + else + parse_mission_info(pm); Current_file_checksum = netmisc_calc_checksum(pm,MISSION_CHECKSUM_SIZE); if (flags & MPF_ONLY_MISSION_INFO) return true; - parse_plot_info(pm); - parse_variables(); - parse_sexp_containers(); - parse_briefing_info(pm); // TODO: obsolete code, keeping so we don't obsolete existing mission files - parse_cutscenes(pm); - parse_fiction(pm); - parse_cmd_briefs(pm); - parse_briefing(pm, flags); - parse_debriefing_new(pm); - parse_player_info(pm); - parse_objects(pm, flags); - parse_wings(pm); - parse_events(pm); - parse_goals(pm); - parse_waypoints_and_jumpnodes(pm); - parse_messages(pm, flags); - parse_reinforcements(pm); - parse_bitmaps(pm); - parse_asteroid_fields(pm); - parse_music(pm, flags); + if (flags & MPF_IMPORT_XWI) + { + parse_xwi_mission(pm, xwim); + } + else + { + parse_plot_info(pm); + parse_variables(); + parse_sexp_containers(); + parse_briefing_info(pm); // TODO: obsolete code, keeping so we don't obsolete existing mission files + parse_cutscenes(pm); + parse_fiction(pm); + parse_cmd_briefs(pm); + parse_briefing(pm, flags); + parse_debriefing_new(pm); + parse_player_info(pm); + parse_objects(pm, flags); + parse_wings(pm); + parse_events(pm); + parse_goals(pm); + parse_waypoints_and_jumpnodes(pm); + parse_messages(pm, flags); + parse_reinforcements(pm); + parse_bitmaps(pm); + parse_asteroid_fields(pm); + parse_music(pm, flags); + } // if we couldn't load some mod data if ((Num_unknown_ship_classes > 0) || ( Num_unknown_loadout_classes > 0 )) { @@ -6618,7 +6631,7 @@ bool parse_main(const char *mission_name, int flags) do { // don't do this for imports - if (!(flags & MPF_IMPORT_FSM)) { + if (!(flags & MPF_IMPORT_FSM) && !(flags & MPF_IMPORT_XWI)) { CFILE *ftemp = cfopen(mission_name, "rt", CFILE_NORMAL, CF_TYPE_MISSIONS); // fail situation. @@ -6639,16 +6652,46 @@ bool parse_main(const char *mission_name, int flags) try { + The_mission.Reset(); + // import FS1 mission if (flags & MPF_IMPORT_FSM) { read_file_text(mission_name, CF_TYPE_ANY); convertFSMtoFS2(); - rval = parse_mission(&The_mission, flags); + rval = parse_mission(&The_mission, nullptr, flags); + } + // import XWI mission from binary file + else if (flags & MPF_IMPORT_XWI) { + char temp_filename[MAX_PATH]; + strcpy_s(temp_filename, mission_name); + auto ch = strrchr(temp_filename, '.'); + if (!ch) + throw parse::ParseException("Couldn't find file extension"); + + if (stricmp(ch, ".BRF") == 0) + strcpy(ch, ".XWI"); + else if (stricmp(ch, ".XWI") != 0) + throw parse::ParseException("Filename does not have an .xwi or .brf extension"); + + // import the mission proper, followed by the briefing + read_file_bytes(temp_filename, CF_TYPE_ANY); + auto xwim = XWingMission::load(Parse_text_raw); + if (!xwim) + throw parse::ParseException("Could not parse XWI mission!"); + rval = parse_mission(&The_mission, xwim, flags); + + if (rval) + { + strcpy(ch, ".BRF"); + read_file_bytes(temp_filename, CF_TYPE_ANY); + auto xwib = XWingBriefing::load(Parse_text_raw); + parse_xwi_briefing(&The_mission, xwib); + } } // regular mission load else { read_file_text(mission_name, CF_TYPE_MISSIONS); - rval = parse_mission(&The_mission, flags); + rval = parse_mission(&The_mission, nullptr, flags); } display_parse_diagnostics(); diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index 1c671fc4211..1c84003bd00 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -56,6 +56,7 @@ extern const gameversion::version LEGACY_MISSION_VERSION; // mission parse flags used for parse_mission() to tell what kind of information to get from the mission file #define MPF_ONLY_MISSION_INFO (1 << 0) #define MPF_IMPORT_FSM (1 << 1) +#define MPF_IMPORT_XWI (1 << 2) // bitfield definitions for missions game types #define OLD_MAX_GAME_TYPES 4 // needed for compatibility diff --git a/code/mission/xwingbrflib.cpp b/code/mission/xwingbrflib.cpp new file mode 100644 index 00000000000..26a73b04eab --- /dev/null +++ b/code/mission/xwingbrflib.cpp @@ -0,0 +1,33 @@ +#include +#include +#include +#include +#include +#include "xwingbrflib.h" + + + +XWingBriefing::XWingBriefing() +{ +} + +XWingBriefing::~XWingBriefing() +{ +} + + +XWingBriefing *XWingBriefing::load(const char *data) +{ + // parse header + struct xwi_brf_header *h = (struct xwi_brf_header *)data; + if (h->version != 2) + return NULL; + + XWingBriefing *b = new XWingBriefing(); + + // h->icon_count == numShips + b->ships = *new std::vector(h->icon_count); + + return b; +} + diff --git a/code/mission/xwingbrflib.h b/code/mission/xwingbrflib.h new file mode 100644 index 00000000000..68fcf784da7 --- /dev/null +++ b/code/mission/xwingbrflib.h @@ -0,0 +1,73 @@ +#pragma once +#include +#include +#include "globalincs/pstypes.h" + + + +std::map > create_animation_map() +{ + std::map > m; + m[1] = { "wait_for_click" }; + m[10] = { "clear_text" }; + m[11] = { "show_title", "i", "text_id" }; + m[12] = { "show_main", "i", "text_id" }; + m[15] = { "center_map", "f", "x", "f", "y" }; + m[16] = { "zoom_map", "f", "x", "f", "y" }; + m[21] = { "clear_boxes" }; + m[22] = { "box_1", "i", "ship_id" }; + m[23] = { "box_2", "i", "ship_id" }; + m[24] = { "box_3", "i", "ship_id" }; + m[25] = { "box_4", "i", "ship_id" }; + m[26] = { "clear_tags" }; + m[27] = { "tag_1", "i", "tag_id", "f", "x", "f", "y" }; + m[28] = { "tag_2", "i", "tag_id", "f", "x", "f", "y" }; + m[29] = { "tag_3", "i", "tag_id", "f", "x", "f", "y" }; + m[30] = { "tag_4", "i", "tag_id", "f", "x", "f", "y" }; + return m; +} +const std::map > ANIMATION_COMMANDS = create_animation_map(); + + + + +#pragma pack(push, 1) + +struct xwi_brf_header { + short version; + short icon_count; + short coordinate_set_count; +}; + +#pragma pack(pop) + + +struct xwi_brf_ship { + vec3d coordinates; + short icon_type; + short iff; + short wave_size; + short num_waves; // wave respawn count + std::string designation; // self.ships[i]['designation'] = self._readFixedString(16) + std::string cargo; // self.ships[i]['cargo'] = self._readFixedString(16) + std::string alt_cargo; // special ship's cargo + short special_ship_in_wave; // 0 ship one, 1 ship two, ... + short rotation_x; // Degrees around X-axis = value/256*360 + short rotation_y; // Degrees around Y-axis = value/256*360 + short rotation_z; // Degrees around Z-axis = value/256*360 +}; + +class XWingBriefing +{ +public: + + XWingBriefing(); + ~XWingBriefing(); + + static XWingBriefing *load(const char *data); + + std::string message1; + xwi_brf_header header; + std::vector ships; +}; + diff --git a/code/mission/xwinglib.cpp b/code/mission/xwinglib.cpp new file mode 100644 index 00000000000..52bfaae4c61 --- /dev/null +++ b/code/mission/xwinglib.cpp @@ -0,0 +1,589 @@ +#include +#include +#include +#include +#include +#include "xwinglib.h" + +XWingMission::XWingMission() +{ +} + +#pragma pack(push, 1) +struct xwi_header { + short version; + short mission_time_limit; + short end_event; + short unknown1; + short mission_location; + char completion_msg_1[64]; + char completion_msg_2[64]; + char completion_msg_3[64]; + short number_of_flight_groups; + short number_of_objects; +}; + +struct xwi_flightgroup { + char designation[16]; + char cargo[16]; + char special_cargo[16]; + short special_ship_number; + short flight_group_type; + short craft_iff; + short craft_status; + short number_in_wave; + short number_of_waves; + short arrival_event; + short arrival_delay; + short arrival_flight_group; + short mothership; + short arrive_by_hyperspace; + short depart_by_hyperspace; + short start1_x; + short wp1_x; + short wp2_x; + short wp3_x; + short start2_x; + short start3_x; + short hyp_x; + short start1_y; + short wp1_y; + short wp2_y; + short wp3_y; + short start2_y; + short start3_y; + short hyp_y; + short start1_z; + short wp1_z; + short wp2_z; + short wp3_z; + short start2_z; + short start3_z; + short hyp_z; + short unknown1; + short unknown2; + short unknown3; + short unknown4; + short unknown5; + short unknown6; + short unknown7; + short formation; + short player_pos; + short craft_ai; + short order; + short dock_time_or_throttle; + short craft_color; + short unknown8; + short objective; + short primary_target; + short secondary_target; +}; +#pragma pack(pop) + +XWingMission *XWingMission::load(const char *data) +{ + struct xwi_header *h = (struct xwi_header *)data; + if (h->version != 2) + return NULL; + + XWingMission *m = new XWingMission(); + + m->missionTimeLimit = h->mission_time_limit; + + switch (h->end_event) { + case 0: + m->endEvent = XWingMission::ev_rescued; + break; + case 1: + m->endEvent = XWingMission::ev_captured; + break; + case 5: + m->endEvent = XWingMission::ev_hit_exhaust_port; + break; + default: + assert(false); + } + + m->unknown1 = h->unknown1; + + switch(h->mission_location) { + case 0: + m->missionLocation = XWingMission::ml_deep_space; + break; + case 1: + m->missionLocation = XWingMission::ml_death_star; + if (m->endEvent == XWingMission::ev_captured) + m->endEvent = XWingMission::ev_cleared_laser_turrets; + break; + default: + assert(false); + } + + m->completionMsg1 = h->completion_msg_1; + m->completionMsg2 = h->completion_msg_2; + m->completionMsg3 = h->completion_msg_3; + + for (int n = 0; n < h->number_of_flight_groups; n++) { + struct xwi_flightgroup *fg = (struct xwi_flightgroup *)(data + sizeof(struct xwi_header) + sizeof(struct xwi_flightgroup) * n); + XWMFlightGroup *nfg = new XWMFlightGroup(); + + nfg->designation = fg->designation; + nfg->cargo = fg->cargo; + nfg->specialCargo = fg->special_cargo; + nfg->specialShipNumber = fg->special_ship_number; + + switch(fg->flight_group_type) { + case 0: + nfg->flightGroupType = XWMFlightGroup::fg_None; + break; + case 1: + nfg->flightGroupType = XWMFlightGroup::fg_X_Wing; + break; + case 2: + nfg->flightGroupType = XWMFlightGroup::fg_Y_Wing; + break; + case 3: + nfg->flightGroupType = XWMFlightGroup::fg_A_Wing; + break; + case 4: + nfg->flightGroupType = XWMFlightGroup::fg_TIE_Fighter; + break; + case 5: + nfg->flightGroupType = XWMFlightGroup::fg_TIE_Interceptor; + break; + case 6: + nfg->flightGroupType = XWMFlightGroup::fg_TIE_Bomber; + break; + case 7: + nfg->flightGroupType = XWMFlightGroup::fg_Gunboat; + break; + case 8: + nfg->flightGroupType = XWMFlightGroup::fg_Transport; + break; + case 9: + nfg->flightGroupType = XWMFlightGroup::fg_Shuttle; + break; + case 10: + nfg->flightGroupType = XWMFlightGroup::fg_Tug; + break; + case 11: + nfg->flightGroupType = XWMFlightGroup::fg_Container; + break; + case 12: + nfg->flightGroupType = XWMFlightGroup::fg_Frieghter; + break; + case 13: + nfg->flightGroupType = XWMFlightGroup::fg_Calamari_Cruiser; + break; + case 14: + nfg->flightGroupType = XWMFlightGroup::fg_Nebulon_B_Frigate; + break; + case 15: + nfg->flightGroupType = XWMFlightGroup::fg_Corellian_Corvette; + break; + case 16: + nfg->flightGroupType = XWMFlightGroup::fg_Imperial_Star_Destroyer; + break; + case 17: + nfg->flightGroupType = XWMFlightGroup::fg_TIE_Advanced; + break; + case 18: + nfg->flightGroupType = XWMFlightGroup::fg_B_Wing; + break; + default: + assert(false); + } + + switch(fg->craft_iff) { + case 0: + nfg->craftIFF = XWMFlightGroup::iff_default; + break; + case 1: + nfg->craftIFF = XWMFlightGroup::iff_rebel; + break; + case 2: + nfg->craftIFF = XWMFlightGroup::iff_imperial; + break; + case 3: + nfg->craftIFF = XWMFlightGroup::iff_neutral; + break; + } + + switch(fg->craft_status) { + case 0: + nfg->craftStatus = XWMFlightGroup::cs_normal; + break; + case 1: + nfg->craftStatus = XWMFlightGroup::cs_no_missiles; + break; + case 2: + nfg->craftStatus = XWMFlightGroup::cs_half_missiles; + break; + case 3: + nfg->craftStatus = XWMFlightGroup::cs_no_shields; + break; + case 4: + // XXX Used by CENTURY 1 in CONVOY2.XWI + nfg->craftStatus = XWMFlightGroup::cs_normal; + break; + case 10: + // XXX Used by Unnammed B-Wing group in DESUPPLY.XWI + nfg->craftStatus = XWMFlightGroup::cs_normal; + break; + case 11: + // XXX Used by PROTOTYPE 2 in T5H1WB.XWI + nfg->craftStatus = XWMFlightGroup::cs_no_missiles; + break; + case 12: + // XXX Used by RED 1 in T5M19MB.XWI + nfg->craftStatus = XWMFlightGroup::cs_half_missiles; + break; + default: + assert(false); + } + + nfg->numberInWave = fg->number_in_wave; + nfg->numberOfWaves = fg->number_of_waves + 1; + + switch(fg->arrival_event) { + case 0: + nfg->arrivalEvent = XWMFlightGroup::ae_mission_start; + break; + case 1: + nfg->arrivalEvent = XWMFlightGroup::ae_afg_arrives; + break; + case 2: + nfg->arrivalEvent = XWMFlightGroup::ae_afg_destroyed; + break; + case 3: + nfg->arrivalEvent = XWMFlightGroup::ae_afg_attacked; + break; + case 4: + nfg->arrivalEvent = XWMFlightGroup::ae_afg_boarded; + break; + case 5: + nfg->arrivalEvent = XWMFlightGroup::ae_afg_identified; + break; + case 6: + nfg->arrivalEvent = XWMFlightGroup::ae_afg_disabled; + break; + default: + assert(false); + } + + // TODO: this strange encoding + nfg->arrivalDelay = fg->arrival_delay; + + nfg->arrivalFlightGroup = fg->arrival_flight_group; + nfg->mothership = fg->mothership; + assert(nfg->mothership == -1 || nfg->mothership < h->number_of_flight_groups); + nfg->arriveByHyperspace = fg->arrive_by_hyperspace == 0 ? false : true; + nfg->departByHyperspace = fg->depart_by_hyperspace == 0 ? false : true; + + nfg->start1_x = fg->start1_x / 160.0f; + nfg->start2_x = fg->start2_x / 160.0f; + nfg->start3_x = fg->start3_x / 160.0f; + nfg->start1_y = fg->start1_y / 160.0f; + nfg->start2_y = fg->start2_y / 160.0f; + nfg->start3_y = fg->start3_y / 160.0f; + nfg->start1_z = fg->start1_z / 160.0f; + nfg->start2_z = fg->start2_z / 160.0f; + nfg->start3_z = fg->start3_z / 160.0f; + + nfg->wp1_x = fg->wp1_x / 160.0f; + nfg->wp2_x = fg->wp2_x / 160.0f; + nfg->wp3_x = fg->wp3_x / 160.0f; + nfg->wp1_y = fg->wp1_y / 160.0f; + nfg->wp2_y = fg->wp2_y / 160.0f; + nfg->wp3_y = fg->wp3_y / 160.0f; + nfg->wp1_z = fg->wp1_z / 160.0f; + nfg->wp2_z = fg->wp2_z / 160.0f; + nfg->wp3_z = fg->wp3_z / 160.0f; + + nfg->hyperspace_x = fg->hyp_x / 160.0f; + nfg->hyperspace_y = fg->hyp_y / 160.0f; + nfg->hyperspace_z = fg->hyp_z / 160.0f; + + nfg->unknown1 = fg->unknown1; + nfg->unknown2 = fg->unknown2; + nfg->unknown3 = fg->unknown3; + nfg->unknown4 = fg->unknown4; + nfg->unknown5 = fg->unknown5; + nfg->unknown6 = fg->unknown6; + nfg->unknown7 = fg->unknown7; + + switch(fg->formation) { + case 0: + nfg->formation = XWMFlightGroup::f_Vic; + break; + case 1: + nfg->formation = XWMFlightGroup::f_Finger_Four; + break; + case 2: + nfg->formation = XWMFlightGroup::f_Line_Astern; + break; + case 3: + nfg->formation = XWMFlightGroup::f_Line_Abreast; + break; + case 4: + nfg->formation = XWMFlightGroup::f_Echelon_Right; + break; + case 5: + nfg->formation = XWMFlightGroup::f_Echelon_Left; + break; + case 6: + nfg->formation = XWMFlightGroup::f_Double_Astern; + break; + case 7: + nfg->formation = XWMFlightGroup::f_Diamond; + break; + case 8: + nfg->formation = XWMFlightGroup::f_Stacked; + break; + case 9: + nfg->formation = XWMFlightGroup::f_Spread; + break; + case 10: + nfg->formation = XWMFlightGroup::f_Hi_Lo; + break; + case 11: + nfg->formation = XWMFlightGroup::f_Spiral; + break; + default: + assert(false); + } + + nfg->playerPos = fg->player_pos; + + switch(fg->craft_ai) { + case 0: + nfg->craftAI = XWMFlightGroup::ai_Rookie; + break; + case 1: + nfg->craftAI = XWMFlightGroup::ai_Officer; + break; + case 2: + nfg->craftAI = XWMFlightGroup::ai_Veteran; + break; + case 3: + nfg->craftAI = XWMFlightGroup::ai_Ace; + break; + case 4: + nfg->craftAI = XWMFlightGroup::ai_Top_Ace; + break; + default: + assert(false); + } + + switch(fg->order) { + case 0: + nfg->order = XWMFlightGroup::o_Hold_Steady; + break; + case 1: + nfg->order = XWMFlightGroup::o_Fly_Home; + break; + case 2: + nfg->order = XWMFlightGroup::o_Circle_And_Ignore; + break; + case 3: + nfg->order = XWMFlightGroup::o_Fly_Once_And_Ignore; + break; + case 4: + nfg->order = XWMFlightGroup::o_Circle_And_Evade; + break; + case 5: + nfg->order = XWMFlightGroup::o_Fly_Once_And_Evade; + break; + case 6: + nfg->order = XWMFlightGroup::o_Close_Escort; + break; + case 7: + nfg->order = XWMFlightGroup::o_Loose_Escort; + break; + case 8: + nfg->order = XWMFlightGroup::o_Attack_Escorts; + break; + case 9: + nfg->order = XWMFlightGroup::o_Attack_Pri_And_Sec_Targets; + break; + case 10: + nfg->order = XWMFlightGroup::o_Attack_Enemies; + break; + case 11: + nfg->order = XWMFlightGroup::o_Rendezvous; + break; + case 12: + nfg->order = XWMFlightGroup::o_Disabled; + break; + case 13: + nfg->order = XWMFlightGroup::o_Board_To_Deliver; + break; + case 14: + nfg->order = XWMFlightGroup::o_Board_To_Take; + break; + case 15: + nfg->order = XWMFlightGroup::o_Board_To_Exchange; + break; + case 16: + nfg->order = XWMFlightGroup::o_Board_To_Capture; + break; + case 17: + nfg->order = XWMFlightGroup::o_Board_To_Destroy; + break; + case 18: + nfg->order = XWMFlightGroup::o_Disable_Pri_And_Sec_Targets; + break; + case 19: + nfg->order = XWMFlightGroup::o_Disable_All; + break; + case 20: + nfg->order = XWMFlightGroup::o_Attack_Transports; + break; + case 21: + nfg->order = XWMFlightGroup::o_Attack_Freighters; + break; + case 22: + nfg->order = XWMFlightGroup::o_Attack_Starships; + break; + case 23: + nfg->order = XWMFlightGroup::o_Attack_Satelites_And_Mines; + break; + case 24: + nfg->order = XWMFlightGroup::o_Disable_Frieghters; + break; + case 25: + nfg->order = XWMFlightGroup::o_Disable_Starships; + break; + case 26: + nfg->order = XWMFlightGroup::o_Starship_Sit_And_Fire; + break; + case 27: + nfg->order = XWMFlightGroup::o_Starship_Fly_Dance; + break; + case 28: + nfg->order = XWMFlightGroup::o_Starship_Circle; + break; + case 29: + nfg->order = XWMFlightGroup::o_Starship_Await_Return; + break; + case 30: + nfg->order = XWMFlightGroup::o_Starship_Await_Launch; + break; + case 31: + nfg->order = XWMFlightGroup::o_Starship_Await_Boarding; + break; + case 32: + // XXX Used by T-FORCE 1 in LEIA.XWI + nfg->order = XWMFlightGroup::o_Attack_Enemies; + break; + default: + assert(false); + } + + nfg->dockTime = fg->dock_time_or_throttle; + nfg->Throttle = fg->dock_time_or_throttle; + + switch(fg->craft_color) { + case 0: + nfg->craftColor = XWMFlightGroup::c_Red; + break; + case 1: + nfg->craftColor = XWMFlightGroup::c_Gold; + break; + case 2: + nfg->craftColor = XWMFlightGroup::c_Blue; + break; + default: + assert(false); + } + + nfg->unknown8 = fg->unknown8; + + switch(fg->objective) { + case 0: + nfg->objective = XWMFlightGroup::o_None; + break; + case 1: + nfg->objective = XWMFlightGroup::o_All_Destroyed; + break; + case 2: + nfg->objective = XWMFlightGroup::o_All_Survive; + break; + case 3: + nfg->objective = XWMFlightGroup::o_All_Captured; + break; + case 4: + nfg->objective = XWMFlightGroup::o_All_Docked; + break; + case 5: + nfg->objective = XWMFlightGroup::o_Special_Craft_Destroyed; + break; + case 6: + nfg->objective = XWMFlightGroup::o_Special_Craft_Survive; + break; + case 7: + nfg->objective = XWMFlightGroup::o_Special_Craft_Captured; + break; + case 8: + nfg->objective = XWMFlightGroup::o_Special_Craft_Docked; + break; + case 9: + nfg->objective = XWMFlightGroup::o_50_Percent_Destroyed; + break; + case 10: + nfg->objective = XWMFlightGroup::o_50_Percent_Survive; + break; + case 11: + nfg->objective = XWMFlightGroup::o_50_Percent_Captured; + break; + case 12: + nfg->objective = XWMFlightGroup::o_50_Percent_Docked; + break; + case 13: + nfg->objective = XWMFlightGroup::o_All_Identified; + break; + case 14: + nfg->objective = XWMFlightGroup::o_Special_Craft_Identifed; + break; + case 15: + nfg->objective = XWMFlightGroup::o_50_Percent_Identified; + break; + case 16: + nfg->objective = XWMFlightGroup::o_Arrive; + break; + default: + assert(false); + } + + // XXX LEVEL1.XWI seems to set primaryTarget to junk + if (nfg->objective == XWMFlightGroup::o_None) { + nfg->primaryTarget = -1; + nfg->secondaryTarget = -1; + } else { + nfg->primaryTarget = fg->primary_target; + nfg->secondaryTarget = fg->secondary_target; + } + + assert(nfg->primaryTarget == -1 || nfg->primaryTarget < h->number_of_flight_groups); + assert(nfg->secondaryTarget == -1 || nfg->secondaryTarget < h->number_of_flight_groups); + + m->flightgroups.push_back(nfg); + } + + return m; +} + +#ifdef TEST_XWINGLIB +int _tmain(int argc, _TCHAR* argv[]) +{ + if (argc < 2) { + printf("usage: xwinglib \n"); + return 1; + } + + XWingMission *m = XWingMission::load(argv[1]); + + return 0; +} +#endif + diff --git a/code/mission/xwinglib.h b/code/mission/xwinglib.h new file mode 100644 index 00000000000..06533e549c4 --- /dev/null +++ b/code/mission/xwinglib.h @@ -0,0 +1,206 @@ + +class XWMFlightGroup +{ +public: + + std::string designation; + std::string cargo; + std::string specialCargo; + + int specialShipNumber; + + enum { + fg_None, + fg_X_Wing, + fg_Y_Wing, + fg_A_Wing, + fg_TIE_Fighter, + fg_TIE_Interceptor, + fg_TIE_Bomber, + fg_Gunboat, + fg_Transport, + fg_Shuttle, + fg_Tug, + fg_Container, + fg_Frieghter, + fg_Calamari_Cruiser, + fg_Nebulon_B_Frigate, + fg_Corellian_Corvette, + fg_Imperial_Star_Destroyer, + fg_TIE_Advanced, + fg_B_Wing + } flightGroupType; + + enum { + iff_default, + iff_rebel, + iff_imperial, + iff_neutral + } craftIFF; + + enum { + cs_normal, + cs_no_missiles, + cs_half_missiles, + cs_no_shields + } craftStatus; + + int numberInWave; + int numberOfWaves; + + enum { + ae_mission_start, + ae_afg_arrives, + ae_afg_destroyed, + ae_afg_attacked, + ae_afg_boarded, + ae_afg_identified, + ae_afg_disabled + } arrivalEvent; + + int arrivalDelay; + + int arrivalFlightGroup; + + int mothership; + + bool arriveByHyperspace; + bool departByHyperspace; + + // TODO: use vector class + float start1_x, start1_y, start1_z; + float start2_x, start2_y, start2_z; + float start3_x, start3_y, start3_z; + float wp1_x, wp1_y, wp1_z; + float wp2_x, wp2_y, wp2_z; + float wp3_x, wp3_y, wp3_z; + float hyperspace_x, hyperspace_y, hyperspace_z; + + short unknown1; + short unknown2; + short unknown3; + short unknown4; + short unknown5; + short unknown6; + short unknown7; + + enum { + f_Vic, + f_Finger_Four, + f_Line_Astern, + f_Line_Abreast, + f_Echelon_Right, + f_Echelon_Left, + f_Double_Astern, + f_Diamond, + f_Stacked, + f_Spread, + f_Hi_Lo, + f_Spiral + } formation; + + int playerPos; + + enum { + ai_Rookie, + ai_Officer, + ai_Veteran, + ai_Ace, + ai_Top_Ace + } craftAI; + + enum { + o_Hold_Steady, + o_Fly_Home, + o_Circle_And_Ignore, + o_Fly_Once_And_Ignore, + o_Circle_And_Evade, + o_Fly_Once_And_Evade, + o_Close_Escort, + o_Loose_Escort, + o_Attack_Escorts, + o_Attack_Pri_And_Sec_Targets, + o_Attack_Enemies, + o_Rendezvous, + o_Disabled, + o_Board_To_Deliver, + o_Board_To_Take, + o_Board_To_Exchange, + o_Board_To_Capture, + o_Board_To_Destroy, + o_Disable_Pri_And_Sec_Targets, + o_Disable_All, + o_Attack_Transports, + o_Attack_Freighters, + o_Attack_Starships, + o_Attack_Satelites_And_Mines, + o_Disable_Frieghters, + o_Disable_Starships, + o_Starship_Sit_And_Fire, + o_Starship_Fly_Dance, + o_Starship_Circle, + o_Starship_Await_Return, + o_Starship_Await_Launch, + o_Starship_Await_Boarding + } order; + + int dockTime; + int Throttle; + + enum { + c_Red, + c_Gold, + c_Blue + } craftColor; + + short unknown8; + + enum { + o_None, + o_All_Destroyed, + o_All_Survive, + o_All_Captured, + o_All_Docked, + o_Special_Craft_Destroyed, + o_Special_Craft_Survive, + o_Special_Craft_Captured, + o_Special_Craft_Docked, + o_50_Percent_Destroyed, + o_50_Percent_Survive, + o_50_Percent_Captured, + o_50_Percent_Docked, + o_All_Identified, + o_Special_Craft_Identifed, + o_50_Percent_Identified, + o_Arrive + } objective; + + int primaryTarget; + int secondaryTarget; +}; + +class XWMObject +{ +public: + +}; + +class XWingMission +{ +protected: + XWingMission(); + +public: + + static XWingMission *load(const char *data); + + int missionTimeLimit; + enum { ev_rescued, ev_captured, ev_cleared_laser_turrets, ev_hit_exhaust_port } endEvent; + short unknown1; // XXX + enum { ml_deep_space, ml_death_star } missionLocation; + std::string completionMsg1; + std::string completionMsg2; + std::string completionMsg3; + std::vector flightgroups; + std::vector objects; +}; diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp new file mode 100644 index 00000000000..582920bd1b9 --- /dev/null +++ b/code/mission/xwingmissionparse.cpp @@ -0,0 +1,167 @@ +#include "mission/missionparse.h" +#include "missionui/redalert.h" +#include "nebula/neb.h" +#include "parse/parselo.h" +#include "species_defs/species_defs.h" +#include "starfield/starfield.h" + +#include "xwingbrflib.h" +#include "xwinglib.h" +#include "xwingmissionparse.h" + +// vazor222 +void parse_xwi_mission_info(mission *pm, XWingMission *xwim, bool basic) +{ + // XWI file version is checked on load, just use latest here + pm->version = MISSION_VERSION; + + // XWI missions will be assigned names in the briefing + strcpy_s(pm->name, "XWI Mission"); + + strcpy_s(pm->author, "XWIConverter"); + + // XWI missions don't track these timestamps, just use now + char time_string[DATE_TIME_LENGTH]; + time_t rawtime; + time(&rawtime); + strftime(time_string, DATE_TIME_LENGTH, "%D %T", localtime(&rawtime)); + strcpy_s(pm->created, time_string); + + strcpy_s(pm->modified, time_string); + + strcpy_s(pm->notes, "This is a FRED2_OPEN created mission, imported from XWI."); + + strcpy_s(pm->mission_desc, NOX("No description\n")); + + pm->game_type = MISSION_TYPE_SINGLE; // assume all XWI missions are single player + + pm->flags.reset(); + + // nebula mission stuff + Neb2_awacs = -1.0f; + Neb2_fog_near_mult = 1.0f; + Neb2_fog_far_mult = 1.0f; + + // Goober5000 - ship contrail speed threshold + pm->contrail_threshold = CONTRAIL_THRESHOLD_DEFAULT; + + pm->num_players = 1; + pm->num_respawns = 0; + The_mission.max_respawn_delay = -1; + + red_alert_invalidate_timestamp(); + + // if we are just requesting basic info then skip everything else. the reason + // for this is to make sure that we don't modify things outside of the mission struct + // that might not get reset afterwards (like what can happen in the techroom) - taylor + // + // NOTE: this can be dangerous so be sure that any get_mission_info() call (defaults to basic info) will + // only reference data parsed before this point!! (like current FRED2 and game code does) + if (basic) + return; + + // this is part of mission info but derived from the campaign file rather than parsed + extern int debrief_find_persona_index(); + pm->debriefing_persona = debrief_find_persona_index(); + + // set up support ships + pm->support_ships.arrival_location = ARRIVE_AT_LOCATION; + pm->support_ships.arrival_anchor = -1; + pm->support_ships.departure_location = DEPART_AT_LOCATION; + pm->support_ships.departure_anchor = -1; + pm->support_ships.max_hull_repair_val = 0.0f; + pm->support_ships.max_subsys_repair_val = 100.0f; //ASSUMPTION: full repair capabilities + pm->support_ships.max_support_ships = -1; // infinite + pm->support_ships.max_concurrent_ships = 1; + pm->support_ships.ship_class = -1; + pm->support_ships.tally = 0; + pm->support_ships.support_available_for_species = 0; + + // for each species, store whether support is available + for (int species = 0; species < (int)Species_info.size(); species++) { + if (Species_info[species].support_ship_index >= 0) { + pm->support_ships.support_available_for_species |= (1 << species); + } + } + + // disallow support in XWI missions unless specifically enabled + pm->support_ships.max_support_ships = 0; + + Mission_all_attack = 0; + + // Maybe delay the player's entry. + // TODO find player's Flight Group and use arrival delay value from that? + // TODO convert from 0 = 0?, 1 = one minute, 2D = 2 minutes, 30 seconds.. hmm, strange encoding + if (xwim->flightgroups[0]->arrivalDelay > 0) { + Entry_delay_time = xwim->flightgroups[0]->arrivalDelay; + } + else + { + Entry_delay_time = 0; + } + + // player is always flight group 0 + Parse_viewer_pos.xyz.x = xwim->flightgroups[0]->start1_x; + Parse_viewer_pos.xyz.y = xwim->flightgroups[0]->start1_y + 100; + Parse_viewer_pos.xyz.z = xwim->flightgroups[0]->start1_z - 100; + + // possible squadron reassignment + strcpy_s(pm->squad_name, ""); + strcpy_s(pm->squad_filename, ""); + + // XWI missions are always single player + Num_teams = 1; // assume 1 + + // TODO load deathstar surface skybox model when specified? + strcpy_s(pm->skybox_model, ""); + + vm_set_identity(&pm->skybox_orientation); + pm->skybox_flags = DEFAULT_NMODEL_FLAGS; + + // Goober5000 - AI on a per-mission basis + The_mission.ai_profile = &Ai_profiles[Default_ai_profile]; + + pm->sound_environment.id = -1; +} + +void parse_xwi_mission(mission *pm, XWingMission *xwim) +{ +} + +/** +* Set up xwi briefing based on assumed .brf file in the same folder. If .brf is not there, +* just use minimal xwi briefing. +* +* NOTE: This updates the global Briefing struct with all the data necessary to drive the briefing +*/ +void parse_xwi_briefing(mission *pm, XWingBriefing *xwBrief) +{ + brief_stage *bs; + briefing *bp; + + brief_reset(); + + bp = &Briefings[0]; + bp->num_stages = 1; // xwing briefings only have one stage + bs = &bp->stages[0]; + + if (xwBrief != NULL) + { + bs->text = xwBrief->message1; // this? + bs->text = xwBrief->ships[2].designation; // or this? + bs->num_icons = xwBrief->header.icon_count; // VZTODO is this the right place to store this? + } + else + { + bs->text = "Prepare for the next xwing mission!"; + strcpy_s(bs->voice, "none.wav"); + vm_vec_zero(&bs->camera_pos); + bs->camera_orient = SCALE_IDENTITY_VECTOR; + bs->camera_time = 500; + bs->num_lines = 0; + bs->num_icons = 0; + bs->flags = 0; + bs->formula = Locked_sexp_true; + } +} + diff --git a/code/mission/xwingmissionparse.h b/code/mission/xwingmissionparse.h new file mode 100644 index 00000000000..65070fa9bb6 --- /dev/null +++ b/code/mission/xwingmissionparse.h @@ -0,0 +1,11 @@ +#ifndef _XWI_PARSE_H +#define _XWI_PARSE_H + +struct mission; +class XWingMission; + +extern void parse_xwi_mission_info(mission *pm, XWingMission *xwim, bool basic = false); +extern void parse_xwi_mission(mission *pm, XWingMission *xwim); +extern void parse_xwi_briefing(mission* pm, XWingBriefing* xwBrief); + +#endif diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 282a1d3fb42..682a7caa6f8 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -823,6 +823,12 @@ add_file_folder("Mission" mission/missiontraining.cpp mission/missiontraining.h mission/mission_flags.h + mission/xwingbrflib.cpp + mission/xwingbrflib.h + mission/xwinglib.cpp + mission/xwinglib.h + mission/xwingmissionparse.cpp + mission/xwingmissionparse.h ) # MissionUI files diff --git a/fred2/fred.rc b/fred2/fred.rc index 6f01c16e4df..f4b6bc0690f 100644 --- a/fred2/fred.rc +++ b/fred2/fred.rc @@ -219,6 +219,7 @@ BEGIN POPUP "&Import" BEGIN MENUITEM "&FreeSpace 1 mission...", 33074 + MENUITEM "&X-Wing mission...", 33102 END MENUITEM SEPARATOR MENUITEM "&Run FreeSpace", 32985 diff --git a/fred2/freddoc.cpp b/fred2/freddoc.cpp index f51888d1852..b7950f13d9a 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -61,6 +61,7 @@ BEGIN_MESSAGE_MAP(CFREDDoc, CDocument) //{{AFX_MSG_MAP(CFREDDoc) ON_COMMAND(ID_EDIT_UNDO, OnEditUndo) ON_COMMAND(ID_FILE_IMPORT_FSM, OnFileImportFSM) + ON_COMMAND(ID_IMPORT_XWIMISSION, OnFileImportXWI) //}}AFX_MSG_MAP END_MESSAGE_MAP() @@ -230,7 +231,7 @@ bool CFREDDoc::load_mission(char *pathname, int flags) { // message 1: required version if (!parse_main(pathname, flags)) { - auto term = (flags & MPF_IMPORT_FSM) ? "import" : "load"; + auto term = ((flags & MPF_IMPORT_FSM) || (flags & MPF_IMPORT_XWI)) ? "import" : "load"; // the version will have been assigned before loading was aborted if (!gameversion::check_at_least(The_mission.required_fso_version)) { @@ -543,6 +544,132 @@ void CFREDDoc::OnFileImportFSM() { recreate_dialogs(); } +void CFREDDoc::OnFileImportXWI() +{ + char dest_directory[MAX_PATH + 1]; + + // if mission has been modified, offer to save before continuing. + if (!SaveModified()) + return; + + + // get location to import from + CFileDialog dlgFile(TRUE, "xwi", NULL, OFN_HIDEREADONLY | OFN_FILEMUSTEXIST | OFN_NOCHANGEDIR | OFN_ALLOWMULTISELECT, "XWI Missions (*.xwi)|*.xwi|All files (*.*)|*.*||"); + dlgFile.m_ofn.lpstrTitle = "Select one or more missions to import"; + dlgFile.m_ofn.lpstrInitialDir = NULL; + + // get XWI files + if (dlgFile.DoModal() != IDOK) + return; + + memset(dest_directory, 0, sizeof(dest_directory)); + + // get location to save to +#if ( _MFC_VER >= 0x0700 ) + BROWSEINFO bi; + bi.hwndOwner = theApp.GetMainWnd()->GetSafeHwnd(); + bi.pidlRoot = nullptr; + bi.pszDisplayName = dest_directory; + bi.lpszTitle = "Select a location to save in"; + bi.ulFlags = 0; + bi.lpfn = NULL; + bi.lParam = NULL; + bi.iImage = NULL; + + LPCITEMIDLIST ret_val = SHBrowseForFolder(&bi); + + if (ret_val == NULL) + return; + + SHGetPathFromIDList(ret_val, dest_directory); +#else + CFolderDialog dlgFolder(_T("Select a location to save in"), fs2_mission_path, NULL); + if (dlgFolder.DoModal() != IDOK) + return; + + strcpy_s(dest_directory, dlgFolder.GetFolderPath()); +#endif + + // clean things up first + if (Briefing_dialog) + Briefing_dialog->icon_select(-1); + + clear_mission(); + + // process all missions + POSITION pos(dlgFile.GetStartPosition()); + while (pos) { + char *ch; + char filename[1024]; + char xwi_path[MAX_PATH_LEN]; + char dest_path[MAX_PATH_LEN]; + + CString xwi_path_mfc(dlgFile.GetNextPathName(pos)); + CFred_mission_save save; + + DWORD attrib; + FILE *fp; + + + // path name too long? + if (strlen(xwi_path_mfc) > MAX_PATH_LEN - 1) + continue; + + // nothing here? + if (!strlen(xwi_path_mfc)) + continue; + + // get our mission + strcpy_s(xwi_path, xwi_path_mfc); + + // load mission into memory + if (load_mission(xwi_path, MPF_IMPORT_XWI)) + continue; + + // get filename + ch = strrchr(xwi_path, DIR_SEPARATOR_CHAR) + 1; + if (ch != NULL) + strcpy_s(filename, ch); + else + strcpy_s(filename, xwi_path); + + // truncate extension + ch = strrchr(filename, '.'); + if (ch != NULL) + *ch = '\0'; + + // add new extension + strcat_s(filename, ".fs2"); + + strcpy_s(Mission_filename, filename); + + // get new path + strcpy_s(dest_path, dest_directory); + strcat_s(dest_path, "\\"); + strcat_s(dest_path, filename); + + // check attributes + fp = fopen(dest_path, "r"); + if (fp) { + fclose(fp); + attrib = GetFileAttributes(dest_path); + if (attrib & FILE_ATTRIBUTE_READONLY) + continue; + } + + // try to save it + if (save.save_mission_file(dest_path)) + continue; + + // success + } + + create_new_mission(); + + MessageBox(NULL, "Import complete. Please check the destination folder to verify all missions were imported successfully.", "Status", MB_OK); + recreate_dialogs(); +} + BOOL CFREDDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; diff --git a/fred2/freddoc.h b/fred2/freddoc.h index c490884ea7d..e87bf996ce7 100644 --- a/fred2/freddoc.h +++ b/fred2/freddoc.h @@ -174,6 +174,12 @@ class CFREDDoc : public CDocument * @author Goober5000 */ afx_msg void OnFileImportFSM(); + + /** + * @brief Handler for File->Import XWI Mission + * @author vazor222 + */ + afx_msg void OnFileImportXWI(); //}}AFX_MSG DECLARE_MESSAGE_MAP() diff --git a/fred2/resource.h b/fred2/resource.h index 24147807cbd..00b4d4c1eb1 100644 --- a/fred2/resource.h +++ b/fred2/resource.h @@ -1511,6 +1511,7 @@ #define ID_MISC_POINTUSINGUVEC 33101 #define ID_MUSIC_PLAYER 33102 #define ID_EDITORS_VOLUMETRICS 33103 +#define ID_IMPORT_XWIMISSION 33104 #define ID_INDICATOR_MODE 59142 #define ID_INDICATOR_LEFT 59143 #define ID_INDICATOR_RIGHT 59144 @@ -1522,7 +1523,7 @@ #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_3D_CONTROLS 1 #define _APS_NEXT_RESOURCE_VALUE 335 -#define _APS_NEXT_COMMAND_VALUE 33104 +#define _APS_NEXT_COMMAND_VALUE 33105 #define _APS_NEXT_CONTROL_VALUE 1705 #define _APS_NEXT_SYMED_VALUE 105 #endif From 6d5830edb5d6edcee557747d8e0846f4ff693c70 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 5 Nov 2022 02:50:48 -0400 Subject: [PATCH 002/466] updates to the importer 919877ef5 if it's just one mission, don't clear after import part of 474594e9e some fixes --- code/mission/missionparse.cpp | 2 -- fred2/freddoc.cpp | 30 ++++++++++++++++++++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 7723d9aaf69..7e3f22c1079 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6652,8 +6652,6 @@ bool parse_main(const char *mission_name, int flags) try { - The_mission.Reset(); - // import FS1 mission if (flags & MPF_IMPORT_FSM) { read_file_text(mission_name, CF_TYPE_ANY); diff --git a/fred2/freddoc.cpp b/fred2/freddoc.cpp index b7950f13d9a..7b30f10c1ec 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -596,15 +596,18 @@ void CFREDDoc::OnFileImportXWI() clear_mission(); + int num_files = 0; + char dest_path[MAX_PATH_LEN]; + // process all missions POSITION pos(dlgFile.GetStartPosition()); while (pos) { char *ch; char filename[1024]; char xwi_path[MAX_PATH_LEN]; - char dest_path[MAX_PATH_LEN]; CString xwi_path_mfc(dlgFile.GetNextPathName(pos)); + num_files++; CFred_mission_save save; DWORD attrib; @@ -623,7 +626,7 @@ void CFREDDoc::OnFileImportXWI() strcpy_s(xwi_path, xwi_path_mfc); // load mission into memory - if (load_mission(xwi_path, MPF_IMPORT_XWI)) + if (!load_mission(xwi_path, MPF_IMPORT_XWI)) continue; // get filename @@ -638,6 +641,9 @@ void CFREDDoc::OnFileImportXWI() if (ch != NULL) *ch = '\0'; + // assign this as the mission name + strcpy_s(The_mission.name, filename); + // add new extension strcat_s(filename, ".fs2"); @@ -664,9 +670,25 @@ void CFREDDoc::OnFileImportXWI() // success } - create_new_mission(); + if (num_files > 1) + { + create_new_mission(); + MessageBox(NULL, "Import complete. Please check the destination folder to verify all missions were imported successfully.", "Status", MB_OK); + } + else if (num_files == 1) + { + SetModifiedFlag(FALSE); + + if (Briefing_dialog) { + Briefing_dialog->restore_editor_state(); + Briefing_dialog->update_data(1); + } + + // these aren't done automatically for imports + theApp.AddToRecentFileList((LPCTSTR)dest_path); + SetTitle((LPCTSTR)Mission_filename); + } - MessageBox(NULL, "Import complete. Please check the destination folder to verify all missions were imported successfully.", "Status", MB_OK); recreate_dialogs(); } From 61d65e257e15dc1c6617d86cbbf1208b6a160169 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Fri, 4 Nov 2022 23:25:54 -0400 Subject: [PATCH 003/466] import wings and ships --- code/mission/xwingmissionparse.cpp | 337 +++++++++++++++++++++-------- 1 file changed, 242 insertions(+), 95 deletions(-) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 582920bd1b9..fcb4aa4883b 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -1,7 +1,9 @@ +#include "iff_defs/iff_defs.h" #include "mission/missionparse.h" #include "missionui/redalert.h" #include "nebula/neb.h" #include "parse/parselo.h" +#include "ship/ship.h" #include "species_defs/species_defs.h" #include "starfield/starfield.h" @@ -9,123 +11,270 @@ #include "xwinglib.h" #include "xwingmissionparse.h" +static int Player_flight_group = 0; + // vazor222 void parse_xwi_mission_info(mission *pm, XWingMission *xwim, bool basic) { - // XWI file version is checked on load, just use latest here - pm->version = MISSION_VERSION; - // XWI missions will be assigned names in the briefing strcpy_s(pm->name, "XWI Mission"); strcpy_s(pm->author, "XWIConverter"); - // XWI missions don't track these timestamps, just use now - char time_string[DATE_TIME_LENGTH]; - time_t rawtime; - time(&rawtime); - strftime(time_string, DATE_TIME_LENGTH, "%D %T", localtime(&rawtime)); - strcpy_s(pm->created, time_string); - - strcpy_s(pm->modified, time_string); - - strcpy_s(pm->notes, "This is a FRED2_OPEN created mission, imported from XWI."); - - strcpy_s(pm->mission_desc, NOX("No description\n")); - - pm->game_type = MISSION_TYPE_SINGLE; // assume all XWI missions are single player - - pm->flags.reset(); - - // nebula mission stuff - Neb2_awacs = -1.0f; - Neb2_fog_near_mult = 1.0f; - Neb2_fog_far_mult = 1.0f; - - // Goober5000 - ship contrail speed threshold - pm->contrail_threshold = CONTRAIL_THRESHOLD_DEFAULT; - - pm->num_players = 1; - pm->num_respawns = 0; - The_mission.max_respawn_delay = -1; - - red_alert_invalidate_timestamp(); - - // if we are just requesting basic info then skip everything else. the reason - // for this is to make sure that we don't modify things outside of the mission struct - // that might not get reset afterwards (like what can happen in the techroom) - taylor - // - // NOTE: this can be dangerous so be sure that any get_mission_info() call (defaults to basic info) will - // only reference data parsed before this point!! (like current FRED2 and game code does) - if (basic) - return; - - // this is part of mission info but derived from the campaign file rather than parsed - extern int debrief_find_persona_index(); - pm->debriefing_persona = debrief_find_persona_index(); - - // set up support ships - pm->support_ships.arrival_location = ARRIVE_AT_LOCATION; - pm->support_ships.arrival_anchor = -1; - pm->support_ships.departure_location = DEPART_AT_LOCATION; - pm->support_ships.departure_anchor = -1; - pm->support_ships.max_hull_repair_val = 0.0f; - pm->support_ships.max_subsys_repair_val = 100.0f; //ASSUMPTION: full repair capabilities - pm->support_ships.max_support_ships = -1; // infinite - pm->support_ships.max_concurrent_ships = 1; - pm->support_ships.ship_class = -1; - pm->support_ships.tally = 0; - pm->support_ships.support_available_for_species = 0; - - // for each species, store whether support is available - for (int species = 0; species < (int)Species_info.size(); species++) { - if (Species_info[species].support_ship_index >= 0) { - pm->support_ships.support_available_for_species |= (1 << species); - } + Parse_viewer_pos.xyz.x = xwim->flightgroups[Player_flight_group]->start1_x; + Parse_viewer_pos.xyz.y = xwim->flightgroups[Player_flight_group]->start1_y + 100; + Parse_viewer_pos.xyz.z = xwim->flightgroups[Player_flight_group]->start1_z - 100; +} + +bool is_fighter_or_bomber(XWMFlightGroup *fg) +{ + switch (fg->flightGroupType) + { + case XWMFlightGroup::fg_X_Wing: + case XWMFlightGroup::fg_Y_Wing: + case XWMFlightGroup::fg_A_Wing: + case XWMFlightGroup::fg_B_Wing: + case XWMFlightGroup::fg_TIE_Fighter: + case XWMFlightGroup::fg_TIE_Interceptor: + case XWMFlightGroup::fg_TIE_Bomber: + case XWMFlightGroup::fg_Gunboat: + case XWMFlightGroup::fg_TIE_Advanced: + return true; + } + return false; +} + +int xwi_determine_anchor(XWingMission *xwim, XWMFlightGroup *fg) +{ + int mothership_number = fg->mothership; + + if (mothership_number != 0xffff) + { + if (mothership_number >= 0 && mothership_number < (int)xwim->flightgroups.size()) + return get_parse_name_index(xwim->flightgroups[mothership_number]->designation.c_str()); + else + Warning(LOCATION, "Mothership number %d is out of range for Flight Group %s", mothership_number, fg->designation.c_str()); + } + + return -1; +} + +const char *xwi_determine_ship_class(XWMFlightGroup *fg) +{ + int flightGroupType = fg->flightGroupType; + + switch (flightGroupType) + { + case XWMFlightGroup::fg_X_Wing: + return "T-65c X-wing"; + case XWMFlightGroup::fg_Y_Wing: + return "BTL-A4 Y-wing"; + case XWMFlightGroup::fg_A_Wing: + return "RZ-1 A-wing"; + case XWMFlightGroup::fg_TIE_Fighter: + return "TIE/ln Fighter"; + case XWMFlightGroup::fg_TIE_Interceptor: + return "TIE/In Interceptor"; + case XWMFlightGroup::fg_TIE_Bomber: + return "TIE/sa Bomber"; + case XWMFlightGroup::fg_Gunboat: + return nullptr; + case XWMFlightGroup::fg_Transport: + return "DX-9 Stormtrooper Transport"; + case XWMFlightGroup::fg_Shuttle: + return "Lambda-class T-4a Shuttle"; + case XWMFlightGroup::fg_Tug: + return nullptr; + case XWMFlightGroup::fg_Container: + return nullptr; + case XWMFlightGroup::fg_Frieghter: + return "BFF-1 Bulk Freighter"; + case XWMFlightGroup::fg_Calamari_Cruiser: + return "Liberty Type Star Cruiser"; + case XWMFlightGroup::fg_Nebulon_B_Frigate: + return "EF76 Nebulon-B Escort Frigate"; + case XWMFlightGroup::fg_Corellian_Corvette: + return "CR90 Corvette"; + case XWMFlightGroup::fg_Imperial_Star_Destroyer: + return "Imperial Star Destroyer"; + case XWMFlightGroup::fg_TIE_Advanced: + return nullptr; + case XWMFlightGroup::fg_B_Wing: + return "B-wing Starfighter"; } - // disallow support in XWI missions unless specifically enabled - pm->support_ships.max_support_ships = 0; + return nullptr; +} + +const char *xwi_determine_team(XWingMission *xwim, XWMFlightGroup *fg, ship_info *sip) +{ + auto player_iff = xwim->flightgroups[Player_flight_group]->craftIFF; - Mission_all_attack = 0; + if (fg->craftIFF == XWMFlightGroup::iff_imperial) + { + if (player_iff == XWMFlightGroup::iff_imperial) + return "Friendly"; + if (player_iff == XWMFlightGroup::iff_rebel) + return "Hostile"; + } - // Maybe delay the player's entry. - // TODO find player's Flight Group and use arrival delay value from that? - // TODO convert from 0 = 0?, 1 = one minute, 2D = 2 minutes, 30 seconds.. hmm, strange encoding - if (xwim->flightgroups[0]->arrivalDelay > 0) { - Entry_delay_time = xwim->flightgroups[0]->arrivalDelay; + if (fg->craftIFF == XWMFlightGroup::iff_rebel) + { + if (player_iff == XWMFlightGroup::iff_imperial) + return "Hostile"; + if (player_iff == XWMFlightGroup::iff_rebel) + return "Friendly"; } - else + + if (fg->craftIFF == XWMFlightGroup::iff_neutral) + return "True Neutral"; + + return nullptr; +} + +int xwi_lookup_cargo(const char *cargo_name) +{ + int index = string_lookup(cargo_name, Cargo_names, Num_cargo); + if (index < 0) { - Entry_delay_time = 0; + if (Num_cargo == MAX_CARGO) + { + Warning(LOCATION, "Can't add any more cargo!"); + return 0; + } + + index = Num_cargo++; + strcpy(Cargo_names[index], cargo_name); } + return index; +} - // player is always flight group 0 - Parse_viewer_pos.xyz.x = xwim->flightgroups[0]->start1_x; - Parse_viewer_pos.xyz.y = xwim->flightgroups[0]->start1_y + 100; - Parse_viewer_pos.xyz.z = xwim->flightgroups[0]->start1_z - 100; - - // possible squadron reassignment - strcpy_s(pm->squad_name, ""); - strcpy_s(pm->squad_filename, ""); +void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) +{ + wing *wingp = nullptr; + int wingnum = -1; + if (fg->numberInWave > 1 || fg->numberOfWaves > 1 || is_fighter_or_bomber(fg)) + { + wingnum = Num_wings++; + wingp = &Wings[wingnum]; - // XWI missions are always single player - Num_teams = 1; // assume 1 + strcpy_s(wingp->name, fg->designation.c_str()); + wingp->num_waves = fg->numberOfWaves; + wingp->formation = -1; // TODO - // TODO load deathstar surface skybox model when specified? - strcpy_s(pm->skybox_model, ""); + wingp->arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; + wingp->arrival_anchor = xwi_determine_anchor(xwim, fg); + wingp->departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; + wingp->departure_anchor = wingp->arrival_anchor; + + wingp->wave_count = fg->numberInWave; + } - vm_set_identity(&pm->skybox_orientation); - pm->skybox_flags = DEFAULT_NMODEL_FLAGS; + for (int i = 0; i < fg->numberInWave; i++) + { + p_object pobj; + + if (wingp) + { + wing_bash_ship_name(pobj.name, fg->designation.c_str(), i + 1, nullptr); + pobj.wingnum = wingnum; + pobj.pos_in_wing = i; + } + else + { + strcpy_s(pobj.name, fg->designation.c_str()); + + pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; + pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); + pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; + pobj.departure_anchor = pobj.arrival_anchor; + } - // Goober5000 - AI on a per-mission basis - The_mission.ai_profile = &Ai_profiles[Default_ai_profile]; + auto ship_class_name = xwi_determine_ship_class(fg); + int ship_class = 0; + if (ship_class_name) + { + int index = ship_info_lookup(ship_class_name); + if (index >= 0) + ship_class = index; + else + Warning(LOCATION, "Could not find ship class %s", ship_class_name); + } + else + Warning(LOCATION, "Unable to determine ship class for Flight Group %s", fg->designation.c_str()); + auto sip = &Ship_info[ship_class]; + pobj.ship_class = ship_class; + + pobj.warpin_params_index = sip->warpin_params_index; + pobj.warpout_params_index = sip->warpout_params_index; + + auto team_name = xwi_determine_team(xwim, fg, sip); + int team = Species_info[sip->species].default_iff; + if (team_name) + { + int index = iff_lookup(team_name); + if (index >= 0) + team = index; + else + Warning(LOCATION, "Could not find iff %s", team_name); + } + pobj.team = team; + + auto start1 = vm_vec_new(fg->start1_x, fg->start1_y, fg->start1_z); + auto start2 = vm_vec_new(fg->start2_x, fg->start2_y, fg->start2_z); + auto start3 = vm_vec_new(fg->start3_x, fg->start3_y, fg->start3_z); + auto waypoint1 = vm_vec_new(fg->wp1_x, fg->wp1_y, fg->wp1_z); + auto waypoint2 = vm_vec_new(fg->wp2_x, fg->wp2_y, fg->wp2_z); + auto waypoint3 = vm_vec_new(fg->wp3_x, fg->wp3_y, fg->wp3_z); + auto hyp = vm_vec_new(fg->hyperspace_x, fg->hyperspace_y, fg->hyperspace_z); + + pobj.pos = start1; + vec3d fvec; + vm_vec_normalized_dir(&fvec, &start1, &hyp); + vm_vector_2_matrix_norm(&pobj.orient, &fvec); + + if (wingp && i == fg->specialShipNumber) + pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); + else + pobj.cargo1 = (char)xwi_lookup_cargo(fg->cargo.c_str()); + + pobj.initial_velocity = 100; + pobj.initial_hull = 100; + pobj.initial_shields = 100; + + if (fg->playerPos == i + 1) + { + pobj.flags.set(Mission::Parse_Object_Flags::OF_Player_start); + pobj.flags.set(Mission::Parse_Object_Flags::SF_Cargo_known); + Player_starts++; + } - pm->sound_environment.id = -1; + pobj.score = sip->score; + } } void parse_xwi_mission(mission *pm, XWingMission *xwim) { + int index = -1; + + for (int i = 0; i < xwim->flightgroups.size(); i++) + { + if (xwim->flightgroups[i]->playerPos > 0) + { + index = i; + break; + } + } + if (index >= 0) + Player_flight_group = index; + else + { + Warning(LOCATION, "Player flight group not found?"); + Player_flight_group = 0; + } + + for (auto fg : xwim->flightgroups) + parse_xwi_flightgroup(pm, xwim, fg); } /** @@ -139,8 +288,6 @@ void parse_xwi_briefing(mission *pm, XWingBriefing *xwBrief) brief_stage *bs; briefing *bp; - brief_reset(); - bp = &Briefings[0]; bp->num_stages = 1; // xwing briefings only have one stage bs = &bp->stages[0]; @@ -153,7 +300,7 @@ void parse_xwi_briefing(mission *pm, XWingBriefing *xwBrief) } else { - bs->text = "Prepare for the next xwing mission!"; + bs->text = "Prepare for the next X-Wing mission!"; strcpy_s(bs->voice, "none.wav"); vm_vec_zero(&bs->camera_pos); bs->camera_orient = SCALE_IDENTITY_VECTOR; From ce43377c01eb6f3b3abe248762f29cdccf7ebace Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 5 Nov 2022 19:40:48 -0400 Subject: [PATCH 004/466] placement of flight groups should now be good --- code/mission/xwingbrflib.cpp | 23 ++++- code/mission/xwingbrflib.h | 26 +---- code/mission/xwingmissionparse.cpp | 149 +++++++++++++++++++++-------- 3 files changed, 130 insertions(+), 68 deletions(-) diff --git a/code/mission/xwingbrflib.cpp b/code/mission/xwingbrflib.cpp index 26a73b04eab..bb3dd743d7d 100644 --- a/code/mission/xwingbrflib.cpp +++ b/code/mission/xwingbrflib.cpp @@ -6,7 +6,6 @@ #include "xwingbrflib.h" - XWingBriefing::XWingBriefing() { } @@ -15,7 +14,6 @@ XWingBriefing::~XWingBriefing() { } - XWingBriefing *XWingBriefing::load(const char *data) { // parse header @@ -31,3 +29,24 @@ XWingBriefing *XWingBriefing::load(const char *data) return b; } +std::map > create_animation_map() +{ + std::map > m; + m[1] = { "wait_for_click" }; + m[10] = { "clear_text" }; + m[11] = { "show_title", "i", "text_id" }; + m[12] = { "show_main", "i", "text_id" }; + m[15] = { "center_map", "f", "x", "f", "y" }; + m[16] = { "zoom_map", "f", "x", "f", "y" }; + m[21] = { "clear_boxes" }; + m[22] = { "box_1", "i", "ship_id" }; + m[23] = { "box_2", "i", "ship_id" }; + m[24] = { "box_3", "i", "ship_id" }; + m[25] = { "box_4", "i", "ship_id" }; + m[26] = { "clear_tags" }; + m[27] = { "tag_1", "i", "tag_id", "f", "x", "f", "y" }; + m[28] = { "tag_2", "i", "tag_id", "f", "x", "f", "y" }; + m[29] = { "tag_3", "i", "tag_id", "f", "x", "f", "y" }; + m[30] = { "tag_4", "i", "tag_id", "f", "x", "f", "y" }; + return m; +} diff --git a/code/mission/xwingbrflib.h b/code/mission/xwingbrflib.h index 68fcf784da7..2d1579d2107 100644 --- a/code/mission/xwingbrflib.h +++ b/code/mission/xwingbrflib.h @@ -4,31 +4,7 @@ #include "globalincs/pstypes.h" - -std::map > create_animation_map() -{ - std::map > m; - m[1] = { "wait_for_click" }; - m[10] = { "clear_text" }; - m[11] = { "show_title", "i", "text_id" }; - m[12] = { "show_main", "i", "text_id" }; - m[15] = { "center_map", "f", "x", "f", "y" }; - m[16] = { "zoom_map", "f", "x", "f", "y" }; - m[21] = { "clear_boxes" }; - m[22] = { "box_1", "i", "ship_id" }; - m[23] = { "box_2", "i", "ship_id" }; - m[24] = { "box_3", "i", "ship_id" }; - m[25] = { "box_4", "i", "ship_id" }; - m[26] = { "clear_tags" }; - m[27] = { "tag_1", "i", "tag_id", "f", "x", "f", "y" }; - m[28] = { "tag_2", "i", "tag_id", "f", "x", "f", "y" }; - m[29] = { "tag_3", "i", "tag_id", "f", "x", "f", "y" }; - m[30] = { "tag_4", "i", "tag_id", "f", "x", "f", "y" }; - return m; -} -const std::map > ANIMATION_COMMANDS = create_animation_map(); - - +std::map > create_animation_map(); #pragma pack(push, 1) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index fcb4aa4883b..73bdf292a47 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -11,6 +11,8 @@ #include "xwinglib.h" #include "xwingmissionparse.h" +extern int allocate_subsys_status(); + static int Player_flight_group = 0; // vazor222 @@ -59,7 +61,7 @@ int xwi_determine_anchor(XWingMission *xwim, XWMFlightGroup *fg) return -1; } -const char *xwi_determine_ship_class(XWMFlightGroup *fg) +const char *xwi_determine_base_ship_class(XWMFlightGroup *fg) { int flightGroupType = fg->flightGroupType; @@ -106,6 +108,48 @@ const char *xwi_determine_ship_class(XWMFlightGroup *fg) return nullptr; } +int xwi_determine_ship_class(XWMFlightGroup *fg) +{ + char ship_class_buffer[NAME_LENGTH]; + + // base ship class must exist + auto class_name = xwi_determine_base_ship_class(fg); + if (class_name == nullptr) + return -1; + + strcpy_s(ship_class_buffer, class_name); + bool variant = false; + + // now see if we have any variants + if (fg->craftColor == XWMFlightGroup::c_Red) + { + strcat_s(ship_class_buffer, "#red"); + variant = true; + } + else if (fg->craftColor == XWMFlightGroup::c_Gold) + { + strcat_s(ship_class_buffer, "#gold"); + variant = true; + } + else if (fg->craftColor == XWMFlightGroup::c_Blue) + { + strcat_s(ship_class_buffer, "#blue"); + variant = true; + } + + if (variant) + { + int ship_class = ship_info_lookup(ship_class_buffer); + if (ship_class >= 0) + return ship_class; + + Warning(LOCATION, "Could not find variant ship class for flight group %s", fg->designation.c_str()); + } + + // no variant, or we're just going with the base class + return ship_info_lookup(class_name); +} + const char *xwi_determine_team(XWingMission *xwim, XWMFlightGroup *fg, ship_info *sip) { auto player_iff = xwim->flightgroups[Player_flight_group]->craftIFF; @@ -127,7 +171,7 @@ const char *xwi_determine_team(XWingMission *xwim, XWMFlightGroup *fg, ship_info } if (fg->craftIFF == XWMFlightGroup::iff_neutral) - return "True Neutral"; + return "Civilian"; return nullptr; } @@ -151,6 +195,7 @@ int xwi_lookup_cargo(const char *cargo_name) void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) { + // see if this flight group is what FreeSpace would treat as a wing wing *wingp = nullptr; int wingnum = -1; if (fg->numberInWave > 1 || fg->numberOfWaves > 1 || is_fighter_or_bomber(fg)) @@ -170,15 +215,46 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) wingp->wave_count = fg->numberInWave; } - for (int i = 0; i < fg->numberInWave; i++) + // all ships in the flight group share a class, so determine that here + int ship_class = xwi_determine_ship_class(fg); + if (ship_class < 0) + { + Warning(LOCATION, "Unable to determine ship class for Flight Group %s", fg->designation.c_str()); + ship_class = 0; + } + auto sip = &Ship_info[ship_class]; + + // similarly for the team + auto team_name = xwi_determine_team(xwim, fg, sip); + int team = Species_info[sip->species].default_iff; + if (team_name) + { + int index = iff_lookup(team_name); + if (index >= 0) + team = index; + else + Warning(LOCATION, "Could not find iff %s", team_name); + } + + // similarly for any waypoints + auto start1 = vm_vec_new(fg->start1_x, fg->start1_y, fg->start1_z); + auto start2 = vm_vec_new(fg->start2_x, fg->start2_y, fg->start2_z); + auto start3 = vm_vec_new(fg->start3_x, fg->start3_y, fg->start3_z); + auto waypoint1 = vm_vec_new(fg->wp1_x, fg->wp1_y, fg->wp1_z); + auto waypoint2 = vm_vec_new(fg->wp2_x, fg->wp2_y, fg->wp2_z); + auto waypoint3 = vm_vec_new(fg->wp3_x, fg->wp3_y, fg->wp3_z); + auto hyp = vm_vec_new(fg->hyperspace_x, fg->hyperspace_y, fg->hyperspace_z); + + // now configure each ship in the flight group + for (int wave_index = 0; wave_index < fg->numberInWave; wave_index++) { p_object pobj; if (wingp) { - wing_bash_ship_name(pobj.name, fg->designation.c_str(), i + 1, nullptr); + wing_bash_ship_name(pobj.name, fg->designation.c_str(), wave_index + 1, nullptr); pobj.wingnum = wingnum; - pobj.pos_in_wing = i; + pobj.pos_in_wing = wave_index; } else { @@ -190,66 +266,57 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) pobj.departure_anchor = pobj.arrival_anchor; } - auto ship_class_name = xwi_determine_ship_class(fg); - int ship_class = 0; - if (ship_class_name) - { - int index = ship_info_lookup(ship_class_name); - if (index >= 0) - ship_class = index; - else - Warning(LOCATION, "Could not find ship class %s", ship_class_name); - } - else - Warning(LOCATION, "Unable to determine ship class for Flight Group %s", fg->designation.c_str()); - auto sip = &Ship_info[ship_class]; pobj.ship_class = ship_class; + // initialize class-specific fields + pobj.ai_class = sip->ai_class; pobj.warpin_params_index = sip->warpin_params_index; pobj.warpout_params_index = sip->warpout_params_index; + pobj.ship_max_shield_strength = sip->max_shield_strength; + pobj.ship_max_hull_strength = sip->max_hull_strength; + Assert(pobj.ship_max_hull_strength > 0.0f); // Goober5000: div-0 check (not shield because we might not have one) + pobj.max_shield_recharge = sip->max_shield_recharge; + pobj.replacement_textures = sip->replacement_textures; // initialize our set with the ship class set, which may be empty + pobj.score = sip->score; - auto team_name = xwi_determine_team(xwim, fg, sip); - int team = Species_info[sip->species].default_iff; - if (team_name) - { - int index = iff_lookup(team_name); - if (index >= 0) - team = index; - else - Warning(LOCATION, "Could not find iff %s", team_name); - } pobj.team = team; - auto start1 = vm_vec_new(fg->start1_x, fg->start1_y, fg->start1_z); - auto start2 = vm_vec_new(fg->start2_x, fg->start2_y, fg->start2_z); - auto start3 = vm_vec_new(fg->start3_x, fg->start3_y, fg->start3_z); - auto waypoint1 = vm_vec_new(fg->wp1_x, fg->wp1_y, fg->wp1_z); - auto waypoint2 = vm_vec_new(fg->wp2_x, fg->wp2_y, fg->wp2_z); - auto waypoint3 = vm_vec_new(fg->wp3_x, fg->wp3_y, fg->wp3_z); - auto hyp = vm_vec_new(fg->hyperspace_x, fg->hyperspace_y, fg->hyperspace_z); - pobj.pos = start1; vec3d fvec; vm_vec_normalized_dir(&fvec, &start1, &hyp); vm_vector_2_matrix_norm(&pobj.orient, &fvec); - if (wingp && i == fg->specialShipNumber) + if (wingp && wave_index == fg->specialShipNumber) pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); else pobj.cargo1 = (char)xwi_lookup_cargo(fg->cargo.c_str()); pobj.initial_velocity = 100; - pobj.initial_hull = 100; - pobj.initial_shields = 100; - if (fg->playerPos == i + 1) + if (fg->playerPos == wave_index + 1) { pobj.flags.set(Mission::Parse_Object_Flags::OF_Player_start); pobj.flags.set(Mission::Parse_Object_Flags::SF_Cargo_known); Player_starts++; } - pobj.score = sip->score; + if (fg->craftStatus == XWMFlightGroup::cs_no_shields) + pobj.flags.set(Mission::Parse_Object_Flags::OF_No_shields); + + if (fg->craftStatus == XWMFlightGroup::cs_no_missiles || fg->craftStatus == XWMFlightGroup::cs_half_missiles) + { + // the only subsystem we actually need is Pilot, because everything else uses defaults + pobj.subsys_index = Subsys_index; + int subsys_index = allocate_subsys_status(); + pobj.subsys_count++; + strcpy_s(Subsys_status[subsys_index].name, NOX("Pilot")); + + for (int bank = 0; bank < MAX_SHIP_SECONDARY_BANKS; bank++) + { + Subsys_status[Subsys_index].secondary_banks[bank] = SUBSYS_STATUS_NO_CHANGE; + Subsys_status[Subsys_index].secondary_ammo[bank] = (fg->craftStatus == XWMFlightGroup::cs_no_missiles) ? 0 : 50; + } + } } } From db69cd083bb2b3d0de624691730635a953830c1f Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 5 Nov 2022 20:21:54 -0400 Subject: [PATCH 005/466] some fixes --- code/mission/xwingmissionparse.cpp | 111 ++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 26 deletions(-) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 73bdf292a47..bbe6071fc2c 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -18,14 +18,13 @@ static int Player_flight_group = 0; // vazor222 void parse_xwi_mission_info(mission *pm, XWingMission *xwim, bool basic) { - // XWI missions will be assigned names in the briefing - strcpy_s(pm->name, "XWI Mission"); + strcpy_s(pm->author, "X-Wing"); + strcpy_s(pm->created, "00/00/00 at 00:00:00"); - strcpy_s(pm->author, "XWIConverter"); - - Parse_viewer_pos.xyz.x = xwim->flightgroups[Player_flight_group]->start1_x; - Parse_viewer_pos.xyz.y = xwim->flightgroups[Player_flight_group]->start1_y + 100; - Parse_viewer_pos.xyz.z = xwim->flightgroups[Player_flight_group]->start1_z - 100; + Parse_viewer_pos.xyz.x = 1000 * xwim->flightgroups[Player_flight_group]->start1_x; + Parse_viewer_pos.xyz.y = 1000 * xwim->flightgroups[Player_flight_group]->start1_y + 100; + Parse_viewer_pos.xyz.z = 1000 * xwim->flightgroups[Player_flight_group]->start1_z - 100; + vm_angle_2_matrix(&Parse_viewer_orient, PI_4, 0); } bool is_fighter_or_bomber(XWMFlightGroup *fg) @@ -50,9 +49,9 @@ int xwi_determine_anchor(XWingMission *xwim, XWMFlightGroup *fg) { int mothership_number = fg->mothership; - if (mothership_number != 0xffff) + if (mothership_number >= 0) { - if (mothership_number >= 0 && mothership_number < (int)xwim->flightgroups.size()) + if (mothership_number < (int)xwim->flightgroups.size()) return get_parse_name_index(xwim->flightgroups[mothership_number]->designation.c_str()); else Warning(LOCATION, "Mothership number %d is out of range for Flight Group %s", mothership_number, fg->designation.c_str()); @@ -110,40 +109,38 @@ const char *xwi_determine_base_ship_class(XWMFlightGroup *fg) int xwi_determine_ship_class(XWMFlightGroup *fg) { - char ship_class_buffer[NAME_LENGTH]; - // base ship class must exist auto class_name = xwi_determine_base_ship_class(fg); if (class_name == nullptr) return -1; - strcpy_s(ship_class_buffer, class_name); + SCP_string variant_class = class_name; bool variant = false; // now see if we have any variants if (fg->craftColor == XWMFlightGroup::c_Red) { - strcat_s(ship_class_buffer, "#red"); + variant_class += "#red"; variant = true; } else if (fg->craftColor == XWMFlightGroup::c_Gold) { - strcat_s(ship_class_buffer, "#gold"); + variant_class += "#gold"; variant = true; } else if (fg->craftColor == XWMFlightGroup::c_Blue) { - strcat_s(ship_class_buffer, "#blue"); + variant_class += "#blue"; variant = true; } if (variant) { - int ship_class = ship_info_lookup(ship_class_buffer); + int ship_class = ship_info_lookup(variant_class.c_str()); if (ship_class >= 0) return ship_class; - Warning(LOCATION, "Could not find variant ship class for flight group %s", fg->designation.c_str()); + Warning(LOCATION, "Could not find variant ship class %s for flight group %s. Using base class instead.", variant_class.c_str(), fg->designation.c_str()); } // no variant, or we're just going with the base class @@ -178,6 +175,10 @@ const char *xwi_determine_team(XWingMission *xwim, XWMFlightGroup *fg, ship_info int xwi_lookup_cargo(const char *cargo_name) { + // empty cargo is the same as Nothing + if (!*cargo_name) + return 0; + int index = string_lookup(cargo_name, Cargo_names, Num_cargo); if (index < 0) { @@ -204,6 +205,7 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) wingp = &Wings[wingnum]; strcpy_s(wingp->name, fg->designation.c_str()); + SCP_totitle(wingp->name); wingp->num_waves = fg->numberOfWaves; wingp->formation = -1; // TODO @@ -212,6 +214,13 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) wingp->departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; wingp->departure_anchor = wingp->arrival_anchor; + // if a wing doesn't have an anchor, make sure it is at-location + // (flight groups present at mission start will have arriveByHyperspace set to false) + if (wingp->arrival_anchor < 0) + wingp->arrival_location = ARRIVE_AT_LOCATION; + if (wingp->departure_anchor < 0) + wingp->departure_location = DEPART_AT_LOCATION; + wingp->wave_count = fg->numberInWave; } @@ -245,6 +254,15 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) auto waypoint3 = vm_vec_new(fg->wp3_x, fg->wp3_y, fg->wp3_z); auto hyp = vm_vec_new(fg->hyperspace_x, fg->hyperspace_y, fg->hyperspace_z); + // waypoint units are in kilometers (after processing by xwinglib which handles the factor of 160), so scale them up + vm_vec_scale(&start1, 1000); + vm_vec_scale(&start2, 1000); + vm_vec_scale(&start3, 1000); + vm_vec_scale(&waypoint1, 1000); + vm_vec_scale(&waypoint2, 1000); + vm_vec_scale(&waypoint3, 1000); + vm_vec_scale(&hyp, 1000); + // now configure each ship in the flight group for (int wave_index = 0; wave_index < fg->numberInWave; wave_index++) { @@ -252,18 +270,26 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) if (wingp) { - wing_bash_ship_name(pobj.name, fg->designation.c_str(), wave_index + 1, nullptr); + wing_bash_ship_name(pobj.name, wingp->name, wave_index + 1, nullptr); pobj.wingnum = wingnum; pobj.pos_in_wing = wave_index; } else { strcpy_s(pobj.name, fg->designation.c_str()); + SCP_totitle(pobj.name); pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; pobj.departure_anchor = pobj.arrival_anchor; + + // if a ship doesn't have an anchor, make sure it is at-location + // (flight groups present at mission start will have arriveByHyperspace set to false) + if (pobj.arrival_anchor < 0) + pobj.arrival_location = ARRIVE_AT_LOCATION; + if (pobj.departure_anchor < 0) + pobj.departure_location = DEPART_AT_LOCATION; } pobj.ship_class = ship_class; @@ -282,19 +308,32 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) pobj.team = team; pobj.pos = start1; - vec3d fvec; - vm_vec_normalized_dir(&fvec, &start1, &hyp); - vm_vector_2_matrix_norm(&pobj.orient, &fvec); + + if (fg->arriveByHyperspace) + { + // YOGEME: When a craft arrives via hyperspace, it will be oriented such that it will pass through HYP and be pointing towards SP1 + vec3d fvec; + vm_vec_normalized_dir(&fvec, &start1, &hyp); + vm_vector_2_matrix_norm(&pobj.orient, &fvec); + } + else + { + vec3d fvec; + vm_vec_normalized_dir(&fvec, &hyp, &start1); + vm_vector_2_matrix_norm(&pobj.orient, &fvec); + } if (wingp && wave_index == fg->specialShipNumber) pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); else pobj.cargo1 = (char)xwi_lookup_cargo(fg->cargo.c_str()); - pobj.initial_velocity = 100; + if (fg->order != XWMFlightGroup::o_Hold_Steady && fg->order != XWMFlightGroup::o_Starship_Sit_And_Fire) + pobj.initial_velocity = 100; if (fg->playerPos == wave_index + 1) { + strcpy_s(Player_start_shipname, pobj.name); pobj.flags.set(Mission::Parse_Object_Flags::OF_Player_start); pobj.flags.set(Mission::Parse_Object_Flags::SF_Cargo_known); Player_starts++; @@ -307,16 +346,18 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) { // the only subsystem we actually need is Pilot, because everything else uses defaults pobj.subsys_index = Subsys_index; - int subsys_index = allocate_subsys_status(); + int this_subsys = allocate_subsys_status(); pobj.subsys_count++; - strcpy_s(Subsys_status[subsys_index].name, NOX("Pilot")); + strcpy_s(Subsys_status[this_subsys].name, NOX("Pilot")); for (int bank = 0; bank < MAX_SHIP_SECONDARY_BANKS; bank++) { - Subsys_status[Subsys_index].secondary_banks[bank] = SUBSYS_STATUS_NO_CHANGE; - Subsys_status[Subsys_index].secondary_ammo[bank] = (fg->craftStatus == XWMFlightGroup::cs_no_missiles) ? 0 : 50; + Subsys_status[this_subsys].secondary_banks[bank] = SUBSYS_STATUS_NO_CHANGE; + Subsys_status[this_subsys].secondary_ammo[bank] = (fg->craftStatus == XWMFlightGroup::cs_no_missiles) ? 0 : 50; } } + + Parse_objects.push_back(pobj); } } @@ -324,6 +365,7 @@ void parse_xwi_mission(mission *pm, XWingMission *xwim) { int index = -1; + // find player flight group for (int i = 0; i < xwim->flightgroups.size(); i++) { if (xwim->flightgroups[i]->playerPos > 0) @@ -340,6 +382,21 @@ void parse_xwi_mission(mission *pm, XWingMission *xwim) Player_flight_group = 0; } + // clear out wings by default + for (int i = 0; i < MAX_STARTING_WINGS; i++) + sprintf(Starting_wing_names[i], "Hidden %d", i); + for (int i = 0; i < MAX_SQUADRON_WINGS; i++) + sprintf(Squadron_wing_names[i], "Hidden %d", i); + for (int i = 0; i < MAX_TVT_WINGS; i++) + sprintf(TVT_wing_names[i], "Hidden %d", i); + + // put the player's flight group in the default spot + strcpy_s(Starting_wing_names[0], xwim->flightgroups[Player_flight_group]->designation.c_str()); + SCP_totitle(Starting_wing_names[0]); + strcpy_s(Squadron_wing_names[0], Starting_wing_names[0]); + strcpy_s(TVT_wing_names[0], Starting_wing_names[0]); + + // load flight groups for (auto fg : xwim->flightgroups) parse_xwi_flightgroup(pm, xwim, fg); } @@ -359,6 +416,7 @@ void parse_xwi_briefing(mission *pm, XWingBriefing *xwBrief) bp->num_stages = 1; // xwing briefings only have one stage bs = &bp->stages[0]; + /* if (xwBrief != NULL) { bs->text = xwBrief->message1; // this? @@ -366,6 +424,7 @@ void parse_xwi_briefing(mission *pm, XWingBriefing *xwBrief) bs->num_icons = xwBrief->header.icon_count; // VZTODO is this the right place to store this? } else + */ { bs->text = "Prepare for the next X-Wing mission!"; strcpy_s(bs->voice, "none.wav"); From 8bf008f692dfe35743a4ce4ee1bf48cccc36049a Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 6 Nov 2022 01:02:18 -0400 Subject: [PATCH 006/466] ai --- code/mission/xwingmissionparse.cpp | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index bbe6071fc2c..52030d470f1 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -194,6 +194,39 @@ int xwi_lookup_cargo(const char *cargo_name) return index; } +const char *xwi_determine_ai_class(XWMFlightGroup *fg) +{ + // Rookie = Cadet + // Officer = Officer + // Veteran = Captain + // Ace = Commander + // Top Ace = General + + switch (fg->craftAI) + { + case XWMFlightGroup::ai_Rookie: + return "Cadet"; + case XWMFlightGroup::ai_Officer: + return "Officer"; + case XWMFlightGroup::ai_Veteran: + return "Captain"; + case XWMFlightGroup::ai_Ace: + return "Commander"; + case XWMFlightGroup::ai_Top_Ace: + return "General"; + } + + return nullptr; +} + +int xwi_arrival_delay_to_seconds(int delay) +{ + // If the arrival delay is less than or equal to 20, it's in minutes. If it's over 20, it's in 6 second blocks. So 21 is 6 seconds, for example + if (delay <= 20) + return delay * 60; + return delay * 6; +} + void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) { // see if this flight group is what FreeSpace would treat as a wing @@ -245,6 +278,18 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) Warning(LOCATION, "Could not find iff %s", team_name); } + // similarly for the AI + auto ai_name = xwi_determine_ai_class(fg); + int ai_class = 0; + if (ai_name) + { + int index = string_lookup(ai_name, Ai_class_names, Num_ai_classes); + if (index >= 0) + ai_class = index; + else + Warning(LOCATION, "Could not find AI class %s", ai_name); + } + // similarly for any waypoints auto start1 = vm_vec_new(fg->start1_x, fg->start1_y, fg->start1_z); auto start2 = vm_vec_new(fg->start2_x, fg->start2_y, fg->start2_z); @@ -306,6 +351,7 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) pobj.score = sip->score; pobj.team = team; + pobj.ai_class = ai_class; pobj.pos = start1; From ae8d1595b805483c58a670967501a426f320f58a Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 6 Nov 2022 01:05:22 -0500 Subject: [PATCH 007/466] fg arrival --- code/mission/xwingmissionparse.cpp | 101 ++++++++++++++++++++++++++--- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 52030d470f1..6b1afafd6db 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -45,6 +45,89 @@ bool is_fighter_or_bomber(XWMFlightGroup *fg) return false; } +bool is_wing(XWMFlightGroup *fg) +{ + return (fg->numberInWave > 1 || fg->numberOfWaves > 1 || is_fighter_or_bomber(fg)); +} + +int xwi_arrival_delay_to_seconds(int delay) +{ + // If the arrival delay is less than or equal to 20, it's in minutes. If it's over 20, it's in 6 second blocks. So 21 is 6 seconds, for example + if (delay <= 20) + return delay * 60; + return delay * 6; +} + +int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) +{ + char name[NAME_LENGTH] = ""; + char sexp_buf[1024]; + + bool check_wing = false; + if (fg->arrivalFlightGroup >= 0) + { + strcpy_s(name, xwim->flightgroups[fg->arrivalFlightGroup]->designation.c_str()); + SCP_totitle(name); + check_wing = is_wing(xwim->flightgroups[fg->arrivalFlightGroup]); + } + + if (fg->arrivalEvent == XWMFlightGroup::ae_afg_arrives) + { + sprintf(sexp_buf, "( has-arrived-delay 0 \"%s\" )", name); + Mp = sexp_buf; + return get_sexp_main(); + } + + if (fg->arrivalEvent == XWMFlightGroup::ae_afg_attacked) + { + if (check_wing) + sprintf(sexp_buf, "( lua-is-wing-attacked \"%s\" )", name); + else + sprintf(sexp_buf, "( lua-is-ship-attacked \"%s\" )", name); + Mp = sexp_buf; + return get_sexp_main(); + } + + if (fg->arrivalEvent == XWMFlightGroup::ae_afg_boarded) + { + if (check_wing) + sprintf(sexp_buf, "( lua-is-wing-boarded \"%s\" )", name); + else + sprintf(sexp_buf, "( lua-is-ship-boarded \"%s\" )", name); + Mp = sexp_buf; + return get_sexp_main(); + } + + if (fg->arrivalEvent == XWMFlightGroup::ae_afg_destroyed) + { + sprintf(sexp_buf, "( is-destroyed-delay 0 \"%s\" )", name); + Mp = sexp_buf; + return get_sexp_main(); + } + + if (fg->arrivalEvent == XWMFlightGroup::ae_afg_disabled) + { + if (check_wing) + sprintf(sexp_buf, "( lua-is-wing-disabled \"%s\" )", name); + else + sprintf(sexp_buf, "( lua-is-ship-disabled \"%s\" )", name); + Mp = sexp_buf; + return get_sexp_main(); + } + + if (fg->arrivalEvent == XWMFlightGroup::ae_afg_identified) + { + if (check_wing) + sprintf(sexp_buf, "( lua-is-wing-identified \"%s\" )", name); + else + sprintf(sexp_buf, "( lua-is-ship-identified \"%s\" )", name); + Mp = sexp_buf; + return get_sexp_main(); + } + + return Locked_sexp_true; +} + int xwi_determine_anchor(XWingMission *xwim, XWMFlightGroup *fg) { int mothership_number = fg->mothership; @@ -219,20 +302,15 @@ const char *xwi_determine_ai_class(XWMFlightGroup *fg) return nullptr; } -int xwi_arrival_delay_to_seconds(int delay) -{ - // If the arrival delay is less than or equal to 20, it's in minutes. If it's over 20, it's in 6 second blocks. So 21 is 6 seconds, for example - if (delay <= 20) - return delay * 60; - return delay * 6; -} - void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) { + int arrival_cue = xwi_determine_arrival_cue(xwim, fg); + int arrival_delay = xwi_arrival_delay_to_seconds(fg->arrivalDelay); + // see if this flight group is what FreeSpace would treat as a wing wing *wingp = nullptr; int wingnum = -1; - if (fg->numberInWave > 1 || fg->numberOfWaves > 1 || is_fighter_or_bomber(fg)) + if (is_wing(fg)) { wingnum = Num_wings++; wingp = &Wings[wingnum]; @@ -242,6 +320,8 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) wingp->num_waves = fg->numberOfWaves; wingp->formation = -1; // TODO + wingp->arrival_cue = arrival_cue; + wingp->arrival_delay = -arrival_delay; wingp->arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; wingp->arrival_anchor = xwi_determine_anchor(xwim, fg); wingp->departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; @@ -318,12 +398,15 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) wing_bash_ship_name(pobj.name, wingp->name, wave_index + 1, nullptr); pobj.wingnum = wingnum; pobj.pos_in_wing = wave_index; + pobj.arrival_cue = Locked_sexp_false; } else { strcpy_s(pobj.name, fg->designation.c_str()); SCP_totitle(pobj.name); + pobj.arrival_cue = arrival_cue; + pobj.arrival_delay = -arrival_delay; pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; From b3d719d8e8ffc18aec432a111467010e20cc1e6f Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 6 Nov 2022 01:38:11 -0500 Subject: [PATCH 008/466] orientation --- code/mission/xwingmissionparse.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 6b1afafd6db..3b20dc44582 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -440,13 +440,16 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) if (fg->arriveByHyperspace) { - // YOGEME: When a craft arrives via hyperspace, it will be oriented such that it will pass through HYP and be pointing towards SP1 + // RandomStarfighter says: + // It arrives from start point and points toward waypoint 1, if waypoint 1 is enabled. + // This also matches FG Red orientation in STARSNDB vec3d fvec; - vm_vec_normalized_dir(&fvec, &start1, &hyp); + vm_vec_normalized_dir(&fvec, &waypoint1, &start1); vm_vector_2_matrix_norm(&pobj.orient, &fvec); } else { + // This matches the Intrepid's orientation in STARSNDB vec3d fvec; vm_vec_normalized_dir(&fvec, &hyp, &start1); vm_vector_2_matrix_norm(&pobj.orient, &fvec); From 3ebae529eaeb22bc9d4778e5a68b3db8ada877e8 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 6 Nov 2022 04:13:17 -0500 Subject: [PATCH 009/466] tweaks --- code/mission/xwingmissionparse.cpp | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 3b20dc44582..3d90bab0ce2 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -81,9 +81,9 @@ int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) if (fg->arrivalEvent == XWMFlightGroup::ae_afg_attacked) { if (check_wing) - sprintf(sexp_buf, "( lua-is-wing-attacked \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-wing-attacked \"%s\" )", name); else - sprintf(sexp_buf, "( lua-is-ship-attacked \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-ship-attacked \"%s\" )", name); Mp = sexp_buf; return get_sexp_main(); } @@ -91,9 +91,9 @@ int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) if (fg->arrivalEvent == XWMFlightGroup::ae_afg_boarded) { if (check_wing) - sprintf(sexp_buf, "( lua-is-wing-boarded \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-wing-boarded \"%s\" )", name); else - sprintf(sexp_buf, "( lua-is-ship-boarded \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-ship-boarded \"%s\" )", name); Mp = sexp_buf; return get_sexp_main(); } @@ -108,9 +108,9 @@ int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) if (fg->arrivalEvent == XWMFlightGroup::ae_afg_disabled) { if (check_wing) - sprintf(sexp_buf, "( lua-is-wing-disabled \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-wing-disabled \"%s\" )", name); else - sprintf(sexp_buf, "( lua-is-ship-disabled \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-ship-disabled \"%s\" )", name); Mp = sexp_buf; return get_sexp_main(); } @@ -118,9 +118,9 @@ int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) if (fg->arrivalEvent == XWMFlightGroup::ae_afg_identified) { if (check_wing) - sprintf(sexp_buf, "( lua-is-wing-identified \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-wing-identified \"%s\" )", name); else - sprintf(sexp_buf, "( lua-is-ship-identified \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-ship-identified \"%s\" )", name); Mp = sexp_buf; return get_sexp_main(); } @@ -223,7 +223,7 @@ int xwi_determine_ship_class(XWMFlightGroup *fg) if (ship_class >= 0) return ship_class; - Warning(LOCATION, "Could not find variant ship class %s for flight group %s. Using base class instead.", variant_class.c_str(), fg->designation.c_str()); + Warning(LOCATION, "Could not find variant ship class %s for Flight Group %s. Using base class instead.", variant_class.c_str(), fg->designation.c_str()); } // no variant, or we're just going with the base class @@ -273,6 +273,7 @@ int xwi_lookup_cargo(const char *cargo_name) index = Num_cargo++; strcpy(Cargo_names[index], cargo_name); + SCP_totitle(Cargo_names[index]); } return index; } @@ -321,7 +322,7 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) wingp->formation = -1; // TODO wingp->arrival_cue = arrival_cue; - wingp->arrival_delay = -arrival_delay; + wingp->arrival_delay = arrival_delay; wingp->arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; wingp->arrival_anchor = xwi_determine_anchor(xwim, fg); wingp->departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; @@ -334,6 +335,12 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) if (wingp->departure_anchor < 0) wingp->departure_location = DEPART_AT_LOCATION; + if (fg->numberInWave > MAX_SHIPS_PER_WING) + { + Warning(LOCATION, "Too many ships in Flight Group %s. FreeSpace supports up to a maximum of %d.", fg->designation.c_str(), MAX_SHIPS_PER_WING); + fg->numberInWave = MAX_SHIPS_PER_WING; + } + wingp->wave_count = fg->numberInWave; } @@ -406,7 +413,7 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) SCP_totitle(pobj.name); pobj.arrival_cue = arrival_cue; - pobj.arrival_delay = -arrival_delay; + pobj.arrival_delay = arrival_delay; pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; From c0221e0fd81caf05a5d0f1687cc2e40726375137 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 6 Nov 2022 13:08:52 -0500 Subject: [PATCH 010/466] quite a bit of refactoring --- code/mission/missionparse.cpp | 11 +- code/mission/xwingbrflib.cpp | 22 +- code/mission/xwingbrflib.h | 9 +- code/mission/xwinglib.cpp | 366 ++++++++++++++--------------- code/mission/xwinglib.h | 338 ++++++++++++++------------ code/mission/xwingmissionparse.cpp | 244 +++++++++---------- code/mission/xwingmissionparse.h | 6 +- 7 files changed, 503 insertions(+), 493 deletions(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 7e3f22c1079..4bf45c63920 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6673,17 +6673,18 @@ bool parse_main(const char *mission_name, int flags) // import the mission proper, followed by the briefing read_file_bytes(temp_filename, CF_TYPE_ANY); - auto xwim = XWingMission::load(Parse_text_raw); - if (!xwim) + XWingMission xwim; + if (!XWingMission::load(&xwim, Parse_text_raw)) throw parse::ParseException("Could not parse XWI mission!"); - rval = parse_mission(&The_mission, xwim, flags); + rval = parse_mission(&The_mission, &xwim, flags); if (rval) { strcpy(ch, ".BRF"); read_file_bytes(temp_filename, CF_TYPE_ANY); - auto xwib = XWingBriefing::load(Parse_text_raw); - parse_xwi_briefing(&The_mission, xwib); + XWingBriefing xwib; + XWingBriefing::load(&xwib, Parse_text_raw); + parse_xwi_briefing(&The_mission, &xwib); } } // regular mission load diff --git a/code/mission/xwingbrflib.cpp b/code/mission/xwingbrflib.cpp index bb3dd743d7d..86aec8a5643 100644 --- a/code/mission/xwingbrflib.cpp +++ b/code/mission/xwingbrflib.cpp @@ -1,30 +1,18 @@ -#include -#include + #include #include -#include #include "xwingbrflib.h" -XWingBriefing::XWingBriefing() -{ -} - -XWingBriefing::~XWingBriefing() -{ -} - -XWingBriefing *XWingBriefing::load(const char *data) +bool XWingBriefing::load(XWingBriefing *b, const char *data) { // parse header - struct xwi_brf_header *h = (struct xwi_brf_header *)data; + xwi_brf_header *h = (xwi_brf_header *)data; if (h->version != 2) - return NULL; - - XWingBriefing *b = new XWingBriefing(); + return false; // h->icon_count == numShips - b->ships = *new std::vector(h->icon_count); + b->ships.resize(h->icon_count); return b; } diff --git a/code/mission/xwingbrflib.h b/code/mission/xwingbrflib.h index 2d1579d2107..a60c3e96c5b 100644 --- a/code/mission/xwingbrflib.h +++ b/code/mission/xwingbrflib.h @@ -1,8 +1,6 @@ #pragma once #include #include -#include "globalincs/pstypes.h" - std::map > create_animation_map(); @@ -19,7 +17,7 @@ struct xwi_brf_header { struct xwi_brf_ship { - vec3d coordinates; + float coordinates_x, coordinates_y, coordinates_z; short icon_type; short iff; short wave_size; @@ -37,10 +35,7 @@ class XWingBriefing { public: - XWingBriefing(); - ~XWingBriefing(); - - static XWingBriefing *load(const char *data); + static bool load(XWingBriefing *b, const char *data); std::string message1; xwi_brf_header header; diff --git a/code/mission/xwinglib.cpp b/code/mission/xwinglib.cpp index 52bfaae4c61..6059e6463a6 100644 --- a/code/mission/xwinglib.cpp +++ b/code/mission/xwinglib.cpp @@ -1,20 +1,14 @@ -#include -#include + #include #include -#include #include "xwinglib.h" -XWingMission::XWingMission() -{ -} - #pragma pack(push, 1) struct xwi_header { short version; short mission_time_limit; short end_event; - short unknown1; + short rnd_seed; short mission_location; char completion_msg_1[64]; char completion_msg_2[64]; @@ -60,63 +54,69 @@ struct xwi_flightgroup { short start2_z; short start3_z; short hyp_z; - short unknown1; - short unknown2; - short unknown3; - short unknown4; - short unknown5; - short unknown6; - short unknown7; + short start1_enabled; + short wp1_enabled; + short wp2_enabled; + short wp3_enabled; + short start2_enabled; + short start3_enabled; + short hyp_enabled; short formation; short player_pos; short craft_ai; short order; short dock_time_or_throttle; - short craft_color; - short unknown8; + short craft_markings1; + short craft_markings2; short objective; short primary_target; short secondary_target; }; #pragma pack(pop) -XWingMission *XWingMission::load(const char *data) +int XWingMission::arrival_delay_to_seconds(int delay) { - struct xwi_header *h = (struct xwi_header *)data; - if (h->version != 2) - return NULL; + // If the arrival delay is less than or equal to 20, it's in minutes. If it's over 20, it's in 6 second blocks. So 21 is 6 seconds, for example + if (delay <= 20) + return delay * 60; + return delay * 6; +} - XWingMission *m = new XWingMission(); +bool XWingMission::load(XWingMission *m, const char *data) +{ + xwi_header *h = (xwi_header *)data; + if (h->version != 2) + return false; m->missionTimeLimit = h->mission_time_limit; switch (h->end_event) { case 0: - m->endEvent = XWingMission::ev_rescued; + m->endEvent = XWMEndEvent::ev_rescued; break; case 1: - m->endEvent = XWingMission::ev_captured; + m->endEvent = XWMEndEvent::ev_captured; break; case 5: - m->endEvent = XWingMission::ev_hit_exhaust_port; + m->endEvent = XWMEndEvent::ev_hit_exhaust_port; break; default: - assert(false); + return false; } - m->unknown1 = h->unknown1; + m->rnd_seed = h->rnd_seed; switch(h->mission_location) { case 0: - m->missionLocation = XWingMission::ml_deep_space; + m->missionLocation = XWMMissionLocation::ml_deep_space; break; case 1: - m->missionLocation = XWingMission::ml_death_star; - if (m->endEvent == XWingMission::ev_captured) - m->endEvent = XWingMission::ev_cleared_laser_turrets; + m->missionLocation = XWMMissionLocation::ml_death_star; + if (m->endEvent == XWMEndEvent::ev_captured) + m->endEvent = XWMEndEvent::ev_cleared_laser_turrets; break; default: - assert(false); + return false; } m->completionMsg1 = h->completion_msg_1; @@ -124,8 +124,9 @@ XWingMission *XWingMission::load(const char *data) m->completionMsg3 = h->completion_msg_3; for (int n = 0; n < h->number_of_flight_groups; n++) { - struct xwi_flightgroup *fg = (struct xwi_flightgroup *)(data + sizeof(struct xwi_header) + sizeof(struct xwi_flightgroup) * n); - XWMFlightGroup *nfg = new XWMFlightGroup(); + xwi_flightgroup *fg = (xwi_flightgroup *)(data + sizeof(xwi_header) + sizeof(xwi_flightgroup) * n); + XWMFlightGroup nfg_buf; + XWMFlightGroup *nfg = &nfg_buf; nfg->designation = fg->designation; nfg->cargo = fg->cargo; @@ -134,112 +135,112 @@ XWingMission *XWingMission::load(const char *data) switch(fg->flight_group_type) { case 0: - nfg->flightGroupType = XWMFlightGroup::fg_None; + nfg->flightGroupType = XWMFlightGroupType::fg_None; break; case 1: - nfg->flightGroupType = XWMFlightGroup::fg_X_Wing; + nfg->flightGroupType = XWMFlightGroupType::fg_X_Wing; break; case 2: - nfg->flightGroupType = XWMFlightGroup::fg_Y_Wing; + nfg->flightGroupType = XWMFlightGroupType::fg_Y_Wing; break; case 3: - nfg->flightGroupType = XWMFlightGroup::fg_A_Wing; + nfg->flightGroupType = XWMFlightGroupType::fg_A_Wing; break; case 4: - nfg->flightGroupType = XWMFlightGroup::fg_TIE_Fighter; + nfg->flightGroupType = XWMFlightGroupType::fg_TIE_Fighter; break; case 5: - nfg->flightGroupType = XWMFlightGroup::fg_TIE_Interceptor; + nfg->flightGroupType = XWMFlightGroupType::fg_TIE_Interceptor; break; case 6: - nfg->flightGroupType = XWMFlightGroup::fg_TIE_Bomber; + nfg->flightGroupType = XWMFlightGroupType::fg_TIE_Bomber; break; case 7: - nfg->flightGroupType = XWMFlightGroup::fg_Gunboat; + nfg->flightGroupType = XWMFlightGroupType::fg_Gunboat; break; case 8: - nfg->flightGroupType = XWMFlightGroup::fg_Transport; + nfg->flightGroupType = XWMFlightGroupType::fg_Transport; break; case 9: - nfg->flightGroupType = XWMFlightGroup::fg_Shuttle; + nfg->flightGroupType = XWMFlightGroupType::fg_Shuttle; break; case 10: - nfg->flightGroupType = XWMFlightGroup::fg_Tug; + nfg->flightGroupType = XWMFlightGroupType::fg_Tug; break; case 11: - nfg->flightGroupType = XWMFlightGroup::fg_Container; + nfg->flightGroupType = XWMFlightGroupType::fg_Container; break; case 12: - nfg->flightGroupType = XWMFlightGroup::fg_Frieghter; + nfg->flightGroupType = XWMFlightGroupType::fg_Freighter; break; case 13: - nfg->flightGroupType = XWMFlightGroup::fg_Calamari_Cruiser; + nfg->flightGroupType = XWMFlightGroupType::fg_Calamari_Cruiser; break; case 14: - nfg->flightGroupType = XWMFlightGroup::fg_Nebulon_B_Frigate; + nfg->flightGroupType = XWMFlightGroupType::fg_Nebulon_B_Frigate; break; case 15: - nfg->flightGroupType = XWMFlightGroup::fg_Corellian_Corvette; + nfg->flightGroupType = XWMFlightGroupType::fg_Corellian_Corvette; break; case 16: - nfg->flightGroupType = XWMFlightGroup::fg_Imperial_Star_Destroyer; + nfg->flightGroupType = XWMFlightGroupType::fg_Imperial_Star_Destroyer; break; case 17: - nfg->flightGroupType = XWMFlightGroup::fg_TIE_Advanced; + nfg->flightGroupType = XWMFlightGroupType::fg_TIE_Advanced; break; case 18: - nfg->flightGroupType = XWMFlightGroup::fg_B_Wing; + nfg->flightGroupType = XWMFlightGroupType::fg_B_Wing; break; default: - assert(false); + return false; } switch(fg->craft_iff) { case 0: - nfg->craftIFF = XWMFlightGroup::iff_default; + nfg->craftIFF = XWMCraftIFF::iff_default; break; case 1: - nfg->craftIFF = XWMFlightGroup::iff_rebel; + nfg->craftIFF = XWMCraftIFF::iff_rebel; break; case 2: - nfg->craftIFF = XWMFlightGroup::iff_imperial; + nfg->craftIFF = XWMCraftIFF::iff_imperial; break; case 3: - nfg->craftIFF = XWMFlightGroup::iff_neutral; + nfg->craftIFF = XWMCraftIFF::iff_neutral; break; } switch(fg->craft_status) { case 0: - nfg->craftStatus = XWMFlightGroup::cs_normal; + nfg->craftStatus = XWMCraftStatus::cs_normal; break; case 1: - nfg->craftStatus = XWMFlightGroup::cs_no_missiles; + nfg->craftStatus = XWMCraftStatus::cs_no_missiles; break; case 2: - nfg->craftStatus = XWMFlightGroup::cs_half_missiles; + nfg->craftStatus = XWMCraftStatus::cs_half_missiles; break; case 3: - nfg->craftStatus = XWMFlightGroup::cs_no_shields; + nfg->craftStatus = XWMCraftStatus::cs_no_shields; break; case 4: // XXX Used by CENTURY 1 in CONVOY2.XWI - nfg->craftStatus = XWMFlightGroup::cs_normal; + nfg->craftStatus = XWMCraftStatus::cs_normal; break; case 10: // XXX Used by Unnammed B-Wing group in DESUPPLY.XWI - nfg->craftStatus = XWMFlightGroup::cs_normal; + nfg->craftStatus = XWMCraftStatus::cs_normal; break; case 11: // XXX Used by PROTOTYPE 2 in T5H1WB.XWI - nfg->craftStatus = XWMFlightGroup::cs_no_missiles; + nfg->craftStatus = XWMCraftStatus::cs_no_missiles; break; case 12: // XXX Used by RED 1 in T5M19MB.XWI - nfg->craftStatus = XWMFlightGroup::cs_half_missiles; + nfg->craftStatus = XWMCraftStatus::cs_half_missiles; break; default: - assert(false); + return false; } nfg->numberInWave = fg->number_in_wave; @@ -247,38 +248,36 @@ XWingMission *XWingMission::load(const char *data) switch(fg->arrival_event) { case 0: - nfg->arrivalEvent = XWMFlightGroup::ae_mission_start; + nfg->arrivalEvent = XWMArrivalEvent::ae_mission_start; break; case 1: - nfg->arrivalEvent = XWMFlightGroup::ae_afg_arrives; + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_arrives; break; case 2: - nfg->arrivalEvent = XWMFlightGroup::ae_afg_destroyed; + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_destroyed; break; case 3: - nfg->arrivalEvent = XWMFlightGroup::ae_afg_attacked; + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_attacked; break; case 4: - nfg->arrivalEvent = XWMFlightGroup::ae_afg_boarded; + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_boarded; break; case 5: - nfg->arrivalEvent = XWMFlightGroup::ae_afg_identified; + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_identified; break; case 6: - nfg->arrivalEvent = XWMFlightGroup::ae_afg_disabled; + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_disabled; break; default: - assert(false); + return false; } - // TODO: this strange encoding - nfg->arrivalDelay = fg->arrival_delay; + nfg->arrivalDelay = arrival_delay_to_seconds(fg->arrival_delay); nfg->arrivalFlightGroup = fg->arrival_flight_group; nfg->mothership = fg->mothership; - assert(nfg->mothership == -1 || nfg->mothership < h->number_of_flight_groups); - nfg->arriveByHyperspace = fg->arrive_by_hyperspace == 0 ? false : true; - nfg->departByHyperspace = fg->depart_by_hyperspace == 0 ? false : true; + nfg->arriveByHyperspace = (fg->arrive_by_hyperspace != 0); + nfg->departByHyperspace = (fg->depart_by_hyperspace != 0); nfg->start1_x = fg->start1_x / 160.0f; nfg->start2_x = fg->start2_x / 160.0f; @@ -290,273 +289,273 @@ XWingMission *XWingMission::load(const char *data) nfg->start2_z = fg->start2_z / 160.0f; nfg->start3_z = fg->start3_z / 160.0f; - nfg->wp1_x = fg->wp1_x / 160.0f; - nfg->wp2_x = fg->wp2_x / 160.0f; - nfg->wp3_x = fg->wp3_x / 160.0f; - nfg->wp1_y = fg->wp1_y / 160.0f; - nfg->wp2_y = fg->wp2_y / 160.0f; - nfg->wp3_y = fg->wp3_y / 160.0f; - nfg->wp1_z = fg->wp1_z / 160.0f; - nfg->wp2_z = fg->wp2_z / 160.0f; - nfg->wp3_z = fg->wp3_z / 160.0f; + nfg->waypoint1_x = fg->wp1_x / 160.0f; + nfg->waypoint2_x = fg->wp2_x / 160.0f; + nfg->waypoint3_x = fg->wp3_x / 160.0f; + nfg->waypoint1_y = fg->wp1_y / 160.0f; + nfg->waypoint2_y = fg->wp2_y / 160.0f; + nfg->waypoint3_y = fg->wp3_y / 160.0f; + nfg->waypoint1_z = fg->wp1_z / 160.0f; + nfg->waypoint2_z = fg->wp2_z / 160.0f; + nfg->waypoint3_z = fg->wp3_z / 160.0f; nfg->hyperspace_x = fg->hyp_x / 160.0f; nfg->hyperspace_y = fg->hyp_y / 160.0f; nfg->hyperspace_z = fg->hyp_z / 160.0f; - nfg->unknown1 = fg->unknown1; - nfg->unknown2 = fg->unknown2; - nfg->unknown3 = fg->unknown3; - nfg->unknown4 = fg->unknown4; - nfg->unknown5 = fg->unknown5; - nfg->unknown6 = fg->unknown6; - nfg->unknown7 = fg->unknown7; + nfg->start1_enabled = (fg->start1_enabled != 0); + nfg->start2_enabled = (fg->start2_enabled != 0); + nfg->start3_enabled = (fg->start3_enabled != 0); + nfg->waypoint1_enabled = (fg->wp1_enabled != 0); + nfg->waypoint2_enabled = (fg->wp2_enabled != 0); + nfg->waypoint3_enabled = (fg->wp3_enabled != 0); + nfg->hyperspace_enabled = (fg->hyp_enabled != 0); switch(fg->formation) { case 0: - nfg->formation = XWMFlightGroup::f_Vic; + nfg->formation = XWMFormation::f_Vic; break; case 1: - nfg->formation = XWMFlightGroup::f_Finger_Four; + nfg->formation = XWMFormation::f_Finger_Four; break; case 2: - nfg->formation = XWMFlightGroup::f_Line_Astern; + nfg->formation = XWMFormation::f_Line_Astern; break; case 3: - nfg->formation = XWMFlightGroup::f_Line_Abreast; + nfg->formation = XWMFormation::f_Line_Abreast; break; case 4: - nfg->formation = XWMFlightGroup::f_Echelon_Right; + nfg->formation = XWMFormation::f_Echelon_Right; break; case 5: - nfg->formation = XWMFlightGroup::f_Echelon_Left; + nfg->formation = XWMFormation::f_Echelon_Left; break; case 6: - nfg->formation = XWMFlightGroup::f_Double_Astern; + nfg->formation = XWMFormation::f_Double_Astern; break; case 7: - nfg->formation = XWMFlightGroup::f_Diamond; + nfg->formation = XWMFormation::f_Diamond; break; case 8: - nfg->formation = XWMFlightGroup::f_Stacked; + nfg->formation = XWMFormation::f_Stacked; break; case 9: - nfg->formation = XWMFlightGroup::f_Spread; + nfg->formation = XWMFormation::f_Spread; break; case 10: - nfg->formation = XWMFlightGroup::f_Hi_Lo; + nfg->formation = XWMFormation::f_Hi_Lo; break; case 11: - nfg->formation = XWMFlightGroup::f_Spiral; + nfg->formation = XWMFormation::f_Spiral; break; default: - assert(false); + return false; } nfg->playerPos = fg->player_pos; switch(fg->craft_ai) { case 0: - nfg->craftAI = XWMFlightGroup::ai_Rookie; + nfg->craftAI = XWMCraftAI::ai_Rookie; break; case 1: - nfg->craftAI = XWMFlightGroup::ai_Officer; + nfg->craftAI = XWMCraftAI::ai_Officer; break; case 2: - nfg->craftAI = XWMFlightGroup::ai_Veteran; + nfg->craftAI = XWMCraftAI::ai_Veteran; break; case 3: - nfg->craftAI = XWMFlightGroup::ai_Ace; + nfg->craftAI = XWMCraftAI::ai_Ace; break; case 4: - nfg->craftAI = XWMFlightGroup::ai_Top_Ace; + nfg->craftAI = XWMCraftAI::ai_Top_Ace; break; default: - assert(false); + return false; } switch(fg->order) { case 0: - nfg->order = XWMFlightGroup::o_Hold_Steady; + nfg->craftOrder = XWMCraftOrder::o_Hold_Steady; break; case 1: - nfg->order = XWMFlightGroup::o_Fly_Home; + nfg->craftOrder = XWMCraftOrder::o_Fly_Home; break; case 2: - nfg->order = XWMFlightGroup::o_Circle_And_Ignore; + nfg->craftOrder = XWMCraftOrder::o_Circle_And_Ignore; break; case 3: - nfg->order = XWMFlightGroup::o_Fly_Once_And_Ignore; + nfg->craftOrder = XWMCraftOrder::o_Fly_Once_And_Ignore; break; case 4: - nfg->order = XWMFlightGroup::o_Circle_And_Evade; + nfg->craftOrder = XWMCraftOrder::o_Circle_And_Evade; break; case 5: - nfg->order = XWMFlightGroup::o_Fly_Once_And_Evade; + nfg->craftOrder = XWMCraftOrder::o_Fly_Once_And_Evade; break; case 6: - nfg->order = XWMFlightGroup::o_Close_Escort; + nfg->craftOrder = XWMCraftOrder::o_Close_Escort; break; case 7: - nfg->order = XWMFlightGroup::o_Loose_Escort; + nfg->craftOrder = XWMCraftOrder::o_Loose_Escort; break; case 8: - nfg->order = XWMFlightGroup::o_Attack_Escorts; + nfg->craftOrder = XWMCraftOrder::o_Attack_Escorts; break; case 9: - nfg->order = XWMFlightGroup::o_Attack_Pri_And_Sec_Targets; + nfg->craftOrder = XWMCraftOrder::o_Attack_Pri_And_Sec_Targets; break; case 10: - nfg->order = XWMFlightGroup::o_Attack_Enemies; + nfg->craftOrder = XWMCraftOrder::o_Attack_Enemies; break; case 11: - nfg->order = XWMFlightGroup::o_Rendezvous; + nfg->craftOrder = XWMCraftOrder::o_Rendezvous; break; case 12: - nfg->order = XWMFlightGroup::o_Disabled; + nfg->craftOrder = XWMCraftOrder::o_Disabled; break; case 13: - nfg->order = XWMFlightGroup::o_Board_To_Deliver; + nfg->craftOrder = XWMCraftOrder::o_Board_To_Deliver; break; case 14: - nfg->order = XWMFlightGroup::o_Board_To_Take; + nfg->craftOrder = XWMCraftOrder::o_Board_To_Take; break; case 15: - nfg->order = XWMFlightGroup::o_Board_To_Exchange; + nfg->craftOrder = XWMCraftOrder::o_Board_To_Exchange; break; case 16: - nfg->order = XWMFlightGroup::o_Board_To_Capture; + nfg->craftOrder = XWMCraftOrder::o_Board_To_Capture; break; case 17: - nfg->order = XWMFlightGroup::o_Board_To_Destroy; + nfg->craftOrder = XWMCraftOrder::o_Board_To_Destroy; break; case 18: - nfg->order = XWMFlightGroup::o_Disable_Pri_And_Sec_Targets; + nfg->craftOrder = XWMCraftOrder::o_Disable_Pri_And_Sec_Targets; break; case 19: - nfg->order = XWMFlightGroup::o_Disable_All; + nfg->craftOrder = XWMCraftOrder::o_Disable_All; break; case 20: - nfg->order = XWMFlightGroup::o_Attack_Transports; + nfg->craftOrder = XWMCraftOrder::o_Attack_Transports; break; case 21: - nfg->order = XWMFlightGroup::o_Attack_Freighters; + nfg->craftOrder = XWMCraftOrder::o_Attack_Freighters; break; case 22: - nfg->order = XWMFlightGroup::o_Attack_Starships; + nfg->craftOrder = XWMCraftOrder::o_Attack_Starships; break; case 23: - nfg->order = XWMFlightGroup::o_Attack_Satelites_And_Mines; + nfg->craftOrder = XWMCraftOrder::o_Attack_Satellites_And_Mines; break; case 24: - nfg->order = XWMFlightGroup::o_Disable_Frieghters; + nfg->craftOrder = XWMCraftOrder::o_Disable_Freighters; break; case 25: - nfg->order = XWMFlightGroup::o_Disable_Starships; + nfg->craftOrder = XWMCraftOrder::o_Disable_Starships; break; case 26: - nfg->order = XWMFlightGroup::o_Starship_Sit_And_Fire; + nfg->craftOrder = XWMCraftOrder::o_Starship_Sit_And_Fire; break; case 27: - nfg->order = XWMFlightGroup::o_Starship_Fly_Dance; + nfg->craftOrder = XWMCraftOrder::o_Starship_Fly_Dance; break; case 28: - nfg->order = XWMFlightGroup::o_Starship_Circle; + nfg->craftOrder = XWMCraftOrder::o_Starship_Circle; break; case 29: - nfg->order = XWMFlightGroup::o_Starship_Await_Return; + nfg->craftOrder = XWMCraftOrder::o_Starship_Await_Return; break; case 30: - nfg->order = XWMFlightGroup::o_Starship_Await_Launch; + nfg->craftOrder = XWMCraftOrder::o_Starship_Await_Launch; break; case 31: - nfg->order = XWMFlightGroup::o_Starship_Await_Boarding; + nfg->craftOrder = XWMCraftOrder::o_Starship_Await_Boarding; break; case 32: // XXX Used by T-FORCE 1 in LEIA.XWI - nfg->order = XWMFlightGroup::o_Attack_Enemies; + nfg->craftOrder = XWMCraftOrder::o_Attack_Enemies; break; default: - assert(false); + return false; } nfg->dockTime = fg->dock_time_or_throttle; nfg->Throttle = fg->dock_time_or_throttle; - switch(fg->craft_color) { + switch(fg->craft_markings1) { case 0: - nfg->craftColor = XWMFlightGroup::c_Red; + nfg->craftColor = XWMCraftColor::c_Red; break; case 1: - nfg->craftColor = XWMFlightGroup::c_Gold; + nfg->craftColor = XWMCraftColor::c_Gold; break; case 2: - nfg->craftColor = XWMFlightGroup::c_Blue; + nfg->craftColor = XWMCraftColor::c_Blue; break; default: - assert(false); + return false; } - nfg->unknown8 = fg->unknown8; + nfg->craftMarkings = fg->craft_markings2; switch(fg->objective) { case 0: - nfg->objective = XWMFlightGroup::o_None; + nfg->objective = XWMObjective::o_None; break; case 1: - nfg->objective = XWMFlightGroup::o_All_Destroyed; + nfg->objective = XWMObjective::o_All_Destroyed; break; case 2: - nfg->objective = XWMFlightGroup::o_All_Survive; + nfg->objective = XWMObjective::o_All_Survive; break; case 3: - nfg->objective = XWMFlightGroup::o_All_Captured; + nfg->objective = XWMObjective::o_All_Captured; break; case 4: - nfg->objective = XWMFlightGroup::o_All_Docked; + nfg->objective = XWMObjective::o_All_Docked; break; case 5: - nfg->objective = XWMFlightGroup::o_Special_Craft_Destroyed; + nfg->objective = XWMObjective::o_Special_Craft_Destroyed; break; case 6: - nfg->objective = XWMFlightGroup::o_Special_Craft_Survive; + nfg->objective = XWMObjective::o_Special_Craft_Survive; break; case 7: - nfg->objective = XWMFlightGroup::o_Special_Craft_Captured; + nfg->objective = XWMObjective::o_Special_Craft_Captured; break; case 8: - nfg->objective = XWMFlightGroup::o_Special_Craft_Docked; + nfg->objective = XWMObjective::o_Special_Craft_Docked; break; case 9: - nfg->objective = XWMFlightGroup::o_50_Percent_Destroyed; + nfg->objective = XWMObjective::o_50_Percent_Destroyed; break; case 10: - nfg->objective = XWMFlightGroup::o_50_Percent_Survive; + nfg->objective = XWMObjective::o_50_Percent_Survive; break; case 11: - nfg->objective = XWMFlightGroup::o_50_Percent_Captured; + nfg->objective = XWMObjective::o_50_Percent_Captured; break; case 12: - nfg->objective = XWMFlightGroup::o_50_Percent_Docked; + nfg->objective = XWMObjective::o_50_Percent_Docked; break; case 13: - nfg->objective = XWMFlightGroup::o_All_Identified; + nfg->objective = XWMObjective::o_All_Identified; break; case 14: - nfg->objective = XWMFlightGroup::o_Special_Craft_Identifed; + nfg->objective = XWMObjective::o_Special_Craft_Identifed; break; case 15: - nfg->objective = XWMFlightGroup::o_50_Percent_Identified; + nfg->objective = XWMObjective::o_50_Percent_Identified; break; case 16: - nfg->objective = XWMFlightGroup::o_Arrive; + nfg->objective = XWMObjective::o_Arrive; break; default: - assert(false); + return false; } // XXX LEVEL1.XWI seems to set primaryTarget to junk - if (nfg->objective == XWMFlightGroup::o_None) { + if (nfg->objective == XWMObjective::o_None) { nfg->primaryTarget = -1; nfg->secondaryTarget = -1; } else { @@ -567,23 +566,8 @@ XWingMission *XWingMission::load(const char *data) assert(nfg->primaryTarget == -1 || nfg->primaryTarget < h->number_of_flight_groups); assert(nfg->secondaryTarget == -1 || nfg->secondaryTarget < h->number_of_flight_groups); - m->flightgroups.push_back(nfg); + m->flightgroups.push_back(*nfg); } - return m; + return true; } - -#ifdef TEST_XWINGLIB -int _tmain(int argc, _TCHAR* argv[]) -{ - if (argc < 2) { - printf("usage: xwinglib \n"); - return 1; - } - - XWingMission *m = XWingMission::load(argv[1]); - - return 0; -} -#endif - diff --git a/code/mission/xwinglib.h b/code/mission/xwinglib.h index 06533e549c4..1e150af1a06 100644 --- a/code/mission/xwinglib.h +++ b/code/mission/xwinglib.h @@ -1,4 +1,144 @@ +enum class XWMFlightGroupType : short +{ + fg_None = 0, + fg_X_Wing, + fg_Y_Wing, + fg_A_Wing, + fg_TIE_Fighter, + fg_TIE_Interceptor, + fg_TIE_Bomber, + fg_Gunboat, + fg_Transport, + fg_Shuttle, + fg_Tug, + fg_Container, + fg_Freighter, + fg_Calamari_Cruiser, + fg_Nebulon_B_Frigate, + fg_Corellian_Corvette, + fg_Imperial_Star_Destroyer, + fg_TIE_Advanced, + fg_B_Wing +}; + +enum class XWMCraftStatus : short +{ + cs_normal = 0, + cs_no_missiles, + cs_half_missiles, + cs_no_shields +}; + +enum class XWMCraftIFF : short +{ + iff_default = 0, + iff_rebel, + iff_imperial, + iff_neutral +}; + +enum class XWMArrivalEvent : short +{ + ae_mission_start = 0, + ae_afg_arrives, + ae_afg_destroyed, + ae_afg_attacked, + ae_afg_boarded, + ae_afg_identified, + ae_afg_disabled +}; + +enum class XWMFormation : short +{ + f_Vic = 0, + f_Finger_Four, + f_Line_Astern, + f_Line_Abreast, + f_Echelon_Right, + f_Echelon_Left, + f_Double_Astern, + f_Diamond, + f_Stacked, + f_Spread, + f_Hi_Lo, + f_Spiral +}; + +enum class XWMCraftAI : short +{ + ai_Rookie = 0, + ai_Officer, + ai_Veteran, + ai_Ace, + ai_Top_Ace +}; + +enum class XWMCraftOrder : short +{ + o_Hold_Steady = 0, + o_Fly_Home, + o_Circle_And_Ignore, + o_Fly_Once_And_Ignore, + o_Circle_And_Evade, + o_Fly_Once_And_Evade, + o_Close_Escort, + o_Loose_Escort, + o_Attack_Escorts, + o_Attack_Pri_And_Sec_Targets, + o_Attack_Enemies, + o_Rendezvous, + o_Disabled, + o_Board_To_Deliver, + o_Board_To_Take, + o_Board_To_Exchange, + o_Board_To_Capture, + o_Board_To_Destroy, + o_Disable_Pri_And_Sec_Targets, + o_Disable_All, + o_Attack_Transports, + o_Attack_Freighters, + o_Attack_Starships, + o_Attack_Satellites_And_Mines, + o_Disable_Freighters, + o_Disable_Starships, + o_Starship_Sit_And_Fire, + o_Starship_Fly_Dance, + o_Starship_Circle, + o_Starship_Await_Return, + o_Starship_Await_Launch, + o_Starship_Await_Boarding +}; + +enum class XWMCraftColor : short +{ + c_Red = 0, + c_Gold, + c_Blue +}; + +enum class XWMObjective : short +{ + o_None = 0, + o_All_Destroyed, + o_All_Survive, + o_All_Captured, + o_All_Docked, + o_Special_Craft_Destroyed, + o_Special_Craft_Survive, + o_Special_Craft_Captured, + o_Special_Craft_Docked, + o_50_Percent_Destroyed, + o_50_Percent_Survive, + o_50_Percent_Captured, + o_50_Percent_Docked, + o_All_Identified, + o_Special_Craft_Identifed, + o_50_Percent_Identified, + o_Arrive +}; + + class XWMFlightGroup { public: @@ -9,54 +149,16 @@ class XWMFlightGroup int specialShipNumber; - enum { - fg_None, - fg_X_Wing, - fg_Y_Wing, - fg_A_Wing, - fg_TIE_Fighter, - fg_TIE_Interceptor, - fg_TIE_Bomber, - fg_Gunboat, - fg_Transport, - fg_Shuttle, - fg_Tug, - fg_Container, - fg_Frieghter, - fg_Calamari_Cruiser, - fg_Nebulon_B_Frigate, - fg_Corellian_Corvette, - fg_Imperial_Star_Destroyer, - fg_TIE_Advanced, - fg_B_Wing - } flightGroupType; - - enum { - iff_default, - iff_rebel, - iff_imperial, - iff_neutral - } craftIFF; - - enum { - cs_normal, - cs_no_missiles, - cs_half_missiles, - cs_no_shields - } craftStatus; + XWMFlightGroupType flightGroupType; + + XWMCraftIFF craftIFF; + + XWMCraftStatus craftStatus; int numberInWave; int numberOfWaves; - enum { - ae_mission_start, - ae_afg_arrives, - ae_afg_destroyed, - ae_afg_attacked, - ae_afg_boarded, - ae_afg_identified, - ae_afg_disabled - } arrivalEvent; + XWMArrivalEvent arrivalEvent; int arrivalDelay; @@ -67,113 +169,38 @@ class XWMFlightGroup bool arriveByHyperspace; bool departByHyperspace; - // TODO: use vector class float start1_x, start1_y, start1_z; float start2_x, start2_y, start2_z; float start3_x, start3_y, start3_z; - float wp1_x, wp1_y, wp1_z; - float wp2_x, wp2_y, wp2_z; - float wp3_x, wp3_y, wp3_z; + float waypoint1_x, waypoint1_y, waypoint1_z; + float waypoint2_x, waypoint2_y, waypoint2_z; + float waypoint3_x, waypoint3_y, waypoint3_z; float hyperspace_x, hyperspace_y, hyperspace_z; - short unknown1; - short unknown2; - short unknown3; - short unknown4; - short unknown5; - short unknown6; - short unknown7; - - enum { - f_Vic, - f_Finger_Four, - f_Line_Astern, - f_Line_Abreast, - f_Echelon_Right, - f_Echelon_Left, - f_Double_Astern, - f_Diamond, - f_Stacked, - f_Spread, - f_Hi_Lo, - f_Spiral - } formation; + bool start1_enabled; + bool start2_enabled; + bool start3_enabled; + bool waypoint1_enabled; + bool waypoint2_enabled; + bool waypoint3_enabled; + bool hyperspace_enabled; + + XWMFormation formation; int playerPos; - enum { - ai_Rookie, - ai_Officer, - ai_Veteran, - ai_Ace, - ai_Top_Ace - } craftAI; - - enum { - o_Hold_Steady, - o_Fly_Home, - o_Circle_And_Ignore, - o_Fly_Once_And_Ignore, - o_Circle_And_Evade, - o_Fly_Once_And_Evade, - o_Close_Escort, - o_Loose_Escort, - o_Attack_Escorts, - o_Attack_Pri_And_Sec_Targets, - o_Attack_Enemies, - o_Rendezvous, - o_Disabled, - o_Board_To_Deliver, - o_Board_To_Take, - o_Board_To_Exchange, - o_Board_To_Capture, - o_Board_To_Destroy, - o_Disable_Pri_And_Sec_Targets, - o_Disable_All, - o_Attack_Transports, - o_Attack_Freighters, - o_Attack_Starships, - o_Attack_Satelites_And_Mines, - o_Disable_Frieghters, - o_Disable_Starships, - o_Starship_Sit_And_Fire, - o_Starship_Fly_Dance, - o_Starship_Circle, - o_Starship_Await_Return, - o_Starship_Await_Launch, - o_Starship_Await_Boarding - } order; + XWMCraftAI craftAI; + + XWMCraftOrder craftOrder; int dockTime; int Throttle; - enum { - c_Red, - c_Gold, - c_Blue - } craftColor; - - short unknown8; - - enum { - o_None, - o_All_Destroyed, - o_All_Survive, - o_All_Captured, - o_All_Docked, - o_Special_Craft_Destroyed, - o_Special_Craft_Survive, - o_Special_Craft_Captured, - o_Special_Craft_Docked, - o_50_Percent_Destroyed, - o_50_Percent_Survive, - o_50_Percent_Captured, - o_50_Percent_Docked, - o_All_Identified, - o_Special_Craft_Identifed, - o_50_Percent_Identified, - o_Arrive - } objective; + XWMCraftColor craftColor; + + short craftMarkings; + + XWMObjective objective; int primaryTarget; int secondaryTarget; @@ -185,22 +212,37 @@ class XWMObject }; -class XWingMission + +enum class XWMEndEvent : short { -protected: - XWingMission(); + ev_rescued = 0, + ev_captured, + ev_cleared_laser_turrets, + ev_hit_exhaust_port +}; +enum class XWMMissionLocation : short +{ + ml_deep_space = 0, + ml_death_star +}; + +class XWingMission +{ public: - static XWingMission *load(const char *data); + static int arrival_delay_to_seconds(int delay); + static bool load(XWingMission *xwim, const char *data); int missionTimeLimit; - enum { ev_rescued, ev_captured, ev_cleared_laser_turrets, ev_hit_exhaust_port } endEvent; - short unknown1; // XXX - enum { ml_deep_space, ml_death_star } missionLocation; + XWMEndEvent endEvent; + short rnd_seed; // Not used + XWMMissionLocation missionLocation; + std::string completionMsg1; std::string completionMsg2; std::string completionMsg3; - std::vector flightgroups; - std::vector objects; + + std::vector flightgroups; + std::vector objects; }; diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 3d90bab0ce2..4b5942efe9e 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -16,49 +16,41 @@ extern int allocate_subsys_status(); static int Player_flight_group = 0; // vazor222 -void parse_xwi_mission_info(mission *pm, XWingMission *xwim, bool basic) +void parse_xwi_mission_info(mission *pm, const XWingMission *xwim) { strcpy_s(pm->author, "X-Wing"); strcpy_s(pm->created, "00/00/00 at 00:00:00"); - Parse_viewer_pos.xyz.x = 1000 * xwim->flightgroups[Player_flight_group]->start1_x; - Parse_viewer_pos.xyz.y = 1000 * xwim->flightgroups[Player_flight_group]->start1_y + 100; - Parse_viewer_pos.xyz.z = 1000 * xwim->flightgroups[Player_flight_group]->start1_z - 100; + Parse_viewer_pos.xyz.x = 1000 * xwim->flightgroups[Player_flight_group].start1_x; + Parse_viewer_pos.xyz.y = 1000 * xwim->flightgroups[Player_flight_group].start1_y + 100; + Parse_viewer_pos.xyz.z = 1000 * xwim->flightgroups[Player_flight_group].start1_z - 100; vm_angle_2_matrix(&Parse_viewer_orient, PI_4, 0); } -bool is_fighter_or_bomber(XWMFlightGroup *fg) +bool is_fighter_or_bomber(const XWMFlightGroup *fg) { switch (fg->flightGroupType) { - case XWMFlightGroup::fg_X_Wing: - case XWMFlightGroup::fg_Y_Wing: - case XWMFlightGroup::fg_A_Wing: - case XWMFlightGroup::fg_B_Wing: - case XWMFlightGroup::fg_TIE_Fighter: - case XWMFlightGroup::fg_TIE_Interceptor: - case XWMFlightGroup::fg_TIE_Bomber: - case XWMFlightGroup::fg_Gunboat: - case XWMFlightGroup::fg_TIE_Advanced: + case XWMFlightGroupType::fg_X_Wing: + case XWMFlightGroupType::fg_Y_Wing: + case XWMFlightGroupType::fg_A_Wing: + case XWMFlightGroupType::fg_B_Wing: + case XWMFlightGroupType::fg_TIE_Fighter: + case XWMFlightGroupType::fg_TIE_Interceptor: + case XWMFlightGroupType::fg_TIE_Bomber: + case XWMFlightGroupType::fg_Gunboat: + case XWMFlightGroupType::fg_TIE_Advanced: return true; } return false; } -bool is_wing(XWMFlightGroup *fg) +bool is_wing(const XWMFlightGroup *fg) { return (fg->numberInWave > 1 || fg->numberOfWaves > 1 || is_fighter_or_bomber(fg)); } -int xwi_arrival_delay_to_seconds(int delay) -{ - // If the arrival delay is less than or equal to 20, it's in minutes. If it's over 20, it's in 6 second blocks. So 21 is 6 seconds, for example - if (delay <= 20) - return delay * 60; - return delay * 6; -} - -int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) +int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg) { char name[NAME_LENGTH] = ""; char sexp_buf[1024]; @@ -66,19 +58,19 @@ int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) bool check_wing = false; if (fg->arrivalFlightGroup >= 0) { - strcpy_s(name, xwim->flightgroups[fg->arrivalFlightGroup]->designation.c_str()); + strcpy_s(name, xwim->flightgroups[fg->arrivalFlightGroup].designation.c_str()); SCP_totitle(name); - check_wing = is_wing(xwim->flightgroups[fg->arrivalFlightGroup]); + check_wing = is_wing(&xwim->flightgroups[fg->arrivalFlightGroup]); } - if (fg->arrivalEvent == XWMFlightGroup::ae_afg_arrives) + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_arrives) { sprintf(sexp_buf, "( has-arrived-delay 0 \"%s\" )", name); Mp = sexp_buf; return get_sexp_main(); } - if (fg->arrivalEvent == XWMFlightGroup::ae_afg_attacked) + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_attacked) { if (check_wing) sprintf(sexp_buf, "( fotg-is-wing-attacked \"%s\" )", name); @@ -88,7 +80,7 @@ int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) return get_sexp_main(); } - if (fg->arrivalEvent == XWMFlightGroup::ae_afg_boarded) + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_boarded) { if (check_wing) sprintf(sexp_buf, "( fotg-is-wing-boarded \"%s\" )", name); @@ -98,14 +90,14 @@ int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) return get_sexp_main(); } - if (fg->arrivalEvent == XWMFlightGroup::ae_afg_destroyed) + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_destroyed) { sprintf(sexp_buf, "( is-destroyed-delay 0 \"%s\" )", name); Mp = sexp_buf; return get_sexp_main(); } - if (fg->arrivalEvent == XWMFlightGroup::ae_afg_disabled) + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_disabled) { if (check_wing) sprintf(sexp_buf, "( fotg-is-wing-disabled \"%s\" )", name); @@ -115,7 +107,7 @@ int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) return get_sexp_main(); } - if (fg->arrivalEvent == XWMFlightGroup::ae_afg_identified) + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_identified) { if (check_wing) sprintf(sexp_buf, "( fotg-is-wing-identified \"%s\" )", name); @@ -128,14 +120,14 @@ int xwi_determine_arrival_cue(XWingMission *xwim, XWMFlightGroup *fg) return Locked_sexp_true; } -int xwi_determine_anchor(XWingMission *xwim, XWMFlightGroup *fg) +int xwi_determine_anchor(const XWingMission *xwim, const XWMFlightGroup *fg) { int mothership_number = fg->mothership; if (mothership_number >= 0) { if (mothership_number < (int)xwim->flightgroups.size()) - return get_parse_name_index(xwim->flightgroups[mothership_number]->designation.c_str()); + return get_parse_name_index(xwim->flightgroups[mothership_number].designation.c_str()); else Warning(LOCATION, "Mothership number %d is out of range for Flight Group %s", mothership_number, fg->designation.c_str()); } @@ -143,54 +135,52 @@ int xwi_determine_anchor(XWingMission *xwim, XWMFlightGroup *fg) return -1; } -const char *xwi_determine_base_ship_class(XWMFlightGroup *fg) +const char *xwi_determine_base_ship_class(const XWMFlightGroup *fg) { - int flightGroupType = fg->flightGroupType; - - switch (flightGroupType) + switch (fg->flightGroupType) { - case XWMFlightGroup::fg_X_Wing: + case XWMFlightGroupType::fg_X_Wing: return "T-65c X-wing"; - case XWMFlightGroup::fg_Y_Wing: + case XWMFlightGroupType::fg_Y_Wing: return "BTL-A4 Y-wing"; - case XWMFlightGroup::fg_A_Wing: + case XWMFlightGroupType::fg_A_Wing: return "RZ-1 A-wing"; - case XWMFlightGroup::fg_TIE_Fighter: + case XWMFlightGroupType::fg_TIE_Fighter: return "TIE/ln Fighter"; - case XWMFlightGroup::fg_TIE_Interceptor: + case XWMFlightGroupType::fg_TIE_Interceptor: return "TIE/In Interceptor"; - case XWMFlightGroup::fg_TIE_Bomber: + case XWMFlightGroupType::fg_TIE_Bomber: return "TIE/sa Bomber"; - case XWMFlightGroup::fg_Gunboat: + case XWMFlightGroupType::fg_Gunboat: return nullptr; - case XWMFlightGroup::fg_Transport: + case XWMFlightGroupType::fg_Transport: return "DX-9 Stormtrooper Transport"; - case XWMFlightGroup::fg_Shuttle: + case XWMFlightGroupType::fg_Shuttle: return "Lambda-class T-4a Shuttle"; - case XWMFlightGroup::fg_Tug: + case XWMFlightGroupType::fg_Tug: return nullptr; - case XWMFlightGroup::fg_Container: + case XWMFlightGroupType::fg_Container: return nullptr; - case XWMFlightGroup::fg_Frieghter: + case XWMFlightGroupType::fg_Freighter: return "BFF-1 Bulk Freighter"; - case XWMFlightGroup::fg_Calamari_Cruiser: + case XWMFlightGroupType::fg_Calamari_Cruiser: return "Liberty Type Star Cruiser"; - case XWMFlightGroup::fg_Nebulon_B_Frigate: + case XWMFlightGroupType::fg_Nebulon_B_Frigate: return "EF76 Nebulon-B Escort Frigate"; - case XWMFlightGroup::fg_Corellian_Corvette: + case XWMFlightGroupType::fg_Corellian_Corvette: return "CR90 Corvette"; - case XWMFlightGroup::fg_Imperial_Star_Destroyer: + case XWMFlightGroupType::fg_Imperial_Star_Destroyer: return "Imperial Star Destroyer"; - case XWMFlightGroup::fg_TIE_Advanced: + case XWMFlightGroupType::fg_TIE_Advanced: return nullptr; - case XWMFlightGroup::fg_B_Wing: + case XWMFlightGroupType::fg_B_Wing: return "B-wing Starfighter"; } return nullptr; } -int xwi_determine_ship_class(XWMFlightGroup *fg) +int xwi_determine_ship_class(const XWMFlightGroup *fg) { // base ship class must exist auto class_name = xwi_determine_base_ship_class(fg); @@ -201,17 +191,17 @@ int xwi_determine_ship_class(XWMFlightGroup *fg) bool variant = false; // now see if we have any variants - if (fg->craftColor == XWMFlightGroup::c_Red) + if (fg->craftColor == XWMCraftColor::c_Red) { variant_class += "#red"; variant = true; } - else if (fg->craftColor == XWMFlightGroup::c_Gold) + else if (fg->craftColor == XWMCraftColor::c_Gold) { variant_class += "#gold"; variant = true; } - else if (fg->craftColor == XWMFlightGroup::c_Blue) + else if (fg->craftColor == XWMCraftColor::c_Blue) { variant_class += "#blue"; variant = true; @@ -230,27 +220,27 @@ int xwi_determine_ship_class(XWMFlightGroup *fg) return ship_info_lookup(class_name); } -const char *xwi_determine_team(XWingMission *xwim, XWMFlightGroup *fg, ship_info *sip) +const char *xwi_determine_team(const XWingMission *xwim, const XWMFlightGroup *fg, const ship_info *sip) { - auto player_iff = xwim->flightgroups[Player_flight_group]->craftIFF; + auto player_iff = xwim->flightgroups[Player_flight_group].craftIFF; - if (fg->craftIFF == XWMFlightGroup::iff_imperial) + if (fg->craftIFF == XWMCraftIFF::iff_imperial) { - if (player_iff == XWMFlightGroup::iff_imperial) + if (player_iff == XWMCraftIFF::iff_imperial) return "Friendly"; - if (player_iff == XWMFlightGroup::iff_rebel) + if (player_iff == XWMCraftIFF::iff_rebel) return "Hostile"; } - if (fg->craftIFF == XWMFlightGroup::iff_rebel) + if (fg->craftIFF == XWMCraftIFF::iff_rebel) { - if (player_iff == XWMFlightGroup::iff_imperial) + if (player_iff == XWMCraftIFF::iff_imperial) return "Hostile"; - if (player_iff == XWMFlightGroup::iff_rebel) + if (player_iff == XWMCraftIFF::iff_rebel) return "Friendly"; } - if (fg->craftIFF == XWMFlightGroup::iff_neutral) + if (fg->craftIFF == XWMCraftIFF::iff_neutral) return "Civilian"; return nullptr; @@ -278,7 +268,7 @@ int xwi_lookup_cargo(const char *cargo_name) return index; } -const char *xwi_determine_ai_class(XWMFlightGroup *fg) +const char *xwi_determine_ai_class(const XWMFlightGroup *fg) { // Rookie = Cadet // Officer = Officer @@ -288,25 +278,50 @@ const char *xwi_determine_ai_class(XWMFlightGroup *fg) switch (fg->craftAI) { - case XWMFlightGroup::ai_Rookie: + case XWMCraftAI::ai_Rookie: return "Cadet"; - case XWMFlightGroup::ai_Officer: + case XWMCraftAI::ai_Officer: return "Officer"; - case XWMFlightGroup::ai_Veteran: + case XWMCraftAI::ai_Veteran: return "Captain"; - case XWMFlightGroup::ai_Ace: + case XWMCraftAI::ai_Ace: return "Commander"; - case XWMFlightGroup::ai_Top_Ace: + case XWMCraftAI::ai_Top_Ace: return "General"; } return nullptr; } -void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) +void xwi_determine_orientation(matrix *orient, const XWMFlightGroup *fg, const vec3d *start1, const vec3d *start2, const vec3d *start3, + const vec3d *waypoint1, const vec3d *waypoint2, const vec3d *waypoint3, const vec3d *hyperspace) +{ + vec3d fvec; + + // RandomStarfighter says: + // It arrives from start point and points toward waypoint 1, if waypoint 1 is enabled. + // This also matches FG Red orientation in STARSNDB + vm_vec_normalized_dir(&fvec, waypoint1, start1); + vm_vector_2_matrix_norm(orient, &fvec); + + if (!fg->arriveByHyperspace && fg->craftOrder == XWMCraftOrder::o_Starship_Sit_And_Fire) + { + // This matches the Intrepid's orientation in STARSNDB + vm_vec_normalized_dir(&fvec, hyperspace, start1); + vm_vector_2_matrix_norm(orient, &fvec); + } +} + +void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFlightGroup *fg) { int arrival_cue = xwi_determine_arrival_cue(xwim, fg); - int arrival_delay = xwi_arrival_delay_to_seconds(fg->arrivalDelay); + + int number_in_wave = fg->numberInWave; + if (number_in_wave > MAX_SHIPS_PER_WING) + { + Warning(LOCATION, "Too many ships in Flight Group %s. FreeSpace supports up to a maximum of %d.", fg->designation.c_str(), MAX_SHIPS_PER_WING); + number_in_wave = MAX_SHIPS_PER_WING; + } // see if this flight group is what FreeSpace would treat as a wing wing *wingp = nullptr; @@ -322,7 +337,7 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) wingp->formation = -1; // TODO wingp->arrival_cue = arrival_cue; - wingp->arrival_delay = arrival_delay; + wingp->arrival_delay = fg->arrivalDelay; wingp->arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; wingp->arrival_anchor = xwi_determine_anchor(xwim, fg); wingp->departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; @@ -335,13 +350,7 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) if (wingp->departure_anchor < 0) wingp->departure_location = DEPART_AT_LOCATION; - if (fg->numberInWave > MAX_SHIPS_PER_WING) - { - Warning(LOCATION, "Too many ships in Flight Group %s. FreeSpace supports up to a maximum of %d.", fg->designation.c_str(), MAX_SHIPS_PER_WING); - fg->numberInWave = MAX_SHIPS_PER_WING; - } - - wingp->wave_count = fg->numberInWave; + wingp->wave_count = number_in_wave; } // all ships in the flight group share a class, so determine that here @@ -381,10 +390,10 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) auto start1 = vm_vec_new(fg->start1_x, fg->start1_y, fg->start1_z); auto start2 = vm_vec_new(fg->start2_x, fg->start2_y, fg->start2_z); auto start3 = vm_vec_new(fg->start3_x, fg->start3_y, fg->start3_z); - auto waypoint1 = vm_vec_new(fg->wp1_x, fg->wp1_y, fg->wp1_z); - auto waypoint2 = vm_vec_new(fg->wp2_x, fg->wp2_y, fg->wp2_z); - auto waypoint3 = vm_vec_new(fg->wp3_x, fg->wp3_y, fg->wp3_z); - auto hyp = vm_vec_new(fg->hyperspace_x, fg->hyperspace_y, fg->hyperspace_z); + auto waypoint1 = vm_vec_new(fg->waypoint1_x, fg->waypoint1_y, fg->waypoint1_z); + auto waypoint2 = vm_vec_new(fg->waypoint2_x, fg->waypoint2_y, fg->waypoint2_z); + auto waypoint3 = vm_vec_new(fg->waypoint3_x, fg->waypoint3_y, fg->waypoint3_z); + auto hyperspace = vm_vec_new(fg->hyperspace_x, fg->hyperspace_y, fg->hyperspace_z); // waypoint units are in kilometers (after processing by xwinglib which handles the factor of 160), so scale them up vm_vec_scale(&start1, 1000); @@ -393,19 +402,23 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) vm_vec_scale(&waypoint1, 1000); vm_vec_scale(&waypoint2, 1000); vm_vec_scale(&waypoint3, 1000); - vm_vec_scale(&hyp, 1000); + vm_vec_scale(&hyperspace, 1000); // now configure each ship in the flight group - for (int wave_index = 0; wave_index < fg->numberInWave; wave_index++) + int wing_leader_pobj_index = -1; + for (int wing_index = 0; wing_index < number_in_wave; wing_index++) { p_object pobj; if (wingp) { - wing_bash_ship_name(pobj.name, wingp->name, wave_index + 1, nullptr); + wing_bash_ship_name(pobj.name, wingp->name, wing_index + 1, nullptr); pobj.wingnum = wingnum; - pobj.pos_in_wing = wave_index; + pobj.pos_in_wing = wing_index; pobj.arrival_cue = Locked_sexp_false; + + if (wing_index == 0) + wing_leader_pobj_index = (int)Parse_objects.size(); } else { @@ -413,7 +426,7 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) SCP_totitle(pobj.name); pobj.arrival_cue = arrival_cue; - pobj.arrival_delay = arrival_delay; + pobj.arrival_delay = fg->arrivalDelay; pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; @@ -444,33 +457,20 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) pobj.ai_class = ai_class; pobj.pos = start1; - - if (fg->arriveByHyperspace) - { - // RandomStarfighter says: - // It arrives from start point and points toward waypoint 1, if waypoint 1 is enabled. - // This also matches FG Red orientation in STARSNDB - vec3d fvec; - vm_vec_normalized_dir(&fvec, &waypoint1, &start1); - vm_vector_2_matrix_norm(&pobj.orient, &fvec); - } + if (wing_index == 0) + xwi_determine_orientation(&pobj.orient, fg, &start1, &start2, &start3, &waypoint1, &waypoint2, &waypoint3, &hyperspace); else - { - // This matches the Intrepid's orientation in STARSNDB - vec3d fvec; - vm_vec_normalized_dir(&fvec, &hyp, &start1); - vm_vector_2_matrix_norm(&pobj.orient, &fvec); - } + pobj.orient = Parse_objects[wing_leader_pobj_index].orient; - if (wingp && wave_index == fg->specialShipNumber) + if (wingp && wing_index == fg->specialShipNumber) pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); else pobj.cargo1 = (char)xwi_lookup_cargo(fg->cargo.c_str()); - if (fg->order != XWMFlightGroup::o_Hold_Steady && fg->order != XWMFlightGroup::o_Starship_Sit_And_Fire) + if (fg->craftOrder != XWMCraftOrder::o_Hold_Steady && fg->craftOrder != XWMCraftOrder::o_Starship_Sit_And_Fire) pobj.initial_velocity = 100; - if (fg->playerPos == wave_index + 1) + if (fg->playerPos == wing_index + 1) { strcpy_s(Player_start_shipname, pobj.name); pobj.flags.set(Mission::Parse_Object_Flags::OF_Player_start); @@ -478,10 +478,10 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) Player_starts++; } - if (fg->craftStatus == XWMFlightGroup::cs_no_shields) + if (fg->craftStatus == XWMCraftStatus::cs_no_shields) pobj.flags.set(Mission::Parse_Object_Flags::OF_No_shields); - if (fg->craftStatus == XWMFlightGroup::cs_no_missiles || fg->craftStatus == XWMFlightGroup::cs_half_missiles) + if (fg->craftStatus == XWMCraftStatus::cs_no_missiles || fg->craftStatus == XWMCraftStatus::cs_half_missiles) { // the only subsystem we actually need is Pilot, because everything else uses defaults pobj.subsys_index = Subsys_index; @@ -492,7 +492,7 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) for (int bank = 0; bank < MAX_SHIP_SECONDARY_BANKS; bank++) { Subsys_status[this_subsys].secondary_banks[bank] = SUBSYS_STATUS_NO_CHANGE; - Subsys_status[this_subsys].secondary_ammo[bank] = (fg->craftStatus == XWMFlightGroup::cs_no_missiles) ? 0 : 50; + Subsys_status[this_subsys].secondary_ammo[bank] = (fg->craftStatus == XWMCraftStatus::cs_no_missiles) ? 0 : 50; } } @@ -500,14 +500,14 @@ void parse_xwi_flightgroup(mission *pm, XWingMission *xwim, XWMFlightGroup *fg) } } -void parse_xwi_mission(mission *pm, XWingMission *xwim) +void parse_xwi_mission(mission *pm, const XWingMission *xwim) { int index = -1; // find player flight group for (int i = 0; i < xwim->flightgroups.size(); i++) { - if (xwim->flightgroups[i]->playerPos > 0) + if (xwim->flightgroups[i].playerPos > 0) { index = i; break; @@ -530,14 +530,14 @@ void parse_xwi_mission(mission *pm, XWingMission *xwim) sprintf(TVT_wing_names[i], "Hidden %d", i); // put the player's flight group in the default spot - strcpy_s(Starting_wing_names[0], xwim->flightgroups[Player_flight_group]->designation.c_str()); + strcpy_s(Starting_wing_names[0], xwim->flightgroups[Player_flight_group].designation.c_str()); SCP_totitle(Starting_wing_names[0]); strcpy_s(Squadron_wing_names[0], Starting_wing_names[0]); strcpy_s(TVT_wing_names[0], Starting_wing_names[0]); // load flight groups - for (auto fg : xwim->flightgroups) - parse_xwi_flightgroup(pm, xwim, fg); + for (const auto &fg : xwim->flightgroups) + parse_xwi_flightgroup(pm, xwim, &fg); } /** @@ -546,7 +546,7 @@ void parse_xwi_mission(mission *pm, XWingMission *xwim) * * NOTE: This updates the global Briefing struct with all the data necessary to drive the briefing */ -void parse_xwi_briefing(mission *pm, XWingBriefing *xwBrief) +void parse_xwi_briefing(mission *pm, const XWingBriefing *xwBrief) { brief_stage *bs; briefing *bp; diff --git a/code/mission/xwingmissionparse.h b/code/mission/xwingmissionparse.h index 65070fa9bb6..c9bd1564e51 100644 --- a/code/mission/xwingmissionparse.h +++ b/code/mission/xwingmissionparse.h @@ -4,8 +4,8 @@ struct mission; class XWingMission; -extern void parse_xwi_mission_info(mission *pm, XWingMission *xwim, bool basic = false); -extern void parse_xwi_mission(mission *pm, XWingMission *xwim); -extern void parse_xwi_briefing(mission* pm, XWingBriefing* xwBrief); +extern void parse_xwi_mission_info(mission *pm, const XWingMission *xwim); +extern void parse_xwi_mission(mission *pm, const XWingMission *xwim); +extern void parse_xwi_briefing(mission *pm, const XWingBriefing *xwBrief); #endif From 7b4ed9e5173b2dc8f20ff5df245ff9e8d029b75e Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 6 Nov 2022 14:57:54 -0500 Subject: [PATCH 011/466] more tweaks --- code/mission/xwinglib.cpp | 15 +++- code/mission/xwingmissionparse.cpp | 114 +++++++++++++++++++++-------- 2 files changed, 97 insertions(+), 32 deletions(-) diff --git a/code/mission/xwinglib.cpp b/code/mission/xwinglib.cpp index 6059e6463a6..07eec5dc7e6 100644 --- a/code/mission/xwinglib.cpp +++ b/code/mission/xwinglib.cpp @@ -79,7 +79,7 @@ int XWingMission::arrival_delay_to_seconds(int delay) // If the arrival delay is less than or equal to 20, it's in minutes. If it's over 20, it's in 6 second blocks. So 21 is 6 seconds, for example if (delay <= 20) return delay * 60; - return delay * 6; + return (delay - 20) * 6; } bool XWingMission::load(XWingMission *m, const char *data) @@ -142,6 +142,19 @@ bool XWingMission::load(XWingMission *m, const char *data) break; case 2: nfg->flightGroupType = XWMFlightGroupType::fg_Y_Wing; + + // There is a special case for Y-wings. If + // the status is 10 (decimal) or higher, the FG is interpreted as a B-wing + // CraftType instead. The status list repeats in the same order. For example, a + // Y-Wing with a status of 1 has no warheads, but with a status of 11 becomes a + // B-Wing with no warheads. + if (fg->craft_status >= 10) + { + fg->flight_group_type = 18; // B-Wing + fg->craft_status -= 10; + nfg->flightGroupType = XWMFlightGroupType::fg_B_Wing; + } + break; case 3: nfg->flightGroupType = XWMFlightGroupType::fg_A_Wing; diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 4b5942efe9e..824f1e13b76 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -1,5 +1,6 @@ #include "iff_defs/iff_defs.h" #include "mission/missionparse.h" +#include "mission/missiongoals.h" #include "missionui/redalert.h" #include "nebula/neb.h" #include "parse/parselo.h" @@ -21,9 +22,10 @@ void parse_xwi_mission_info(mission *pm, const XWingMission *xwim) strcpy_s(pm->author, "X-Wing"); strcpy_s(pm->created, "00/00/00 at 00:00:00"); + // NOTE: Y and Z are swapped and the units are in km Parse_viewer_pos.xyz.x = 1000 * xwim->flightgroups[Player_flight_group].start1_x; - Parse_viewer_pos.xyz.y = 1000 * xwim->flightgroups[Player_flight_group].start1_y + 100; - Parse_viewer_pos.xyz.z = 1000 * xwim->flightgroups[Player_flight_group].start1_z - 100; + Parse_viewer_pos.xyz.y = 1000 * xwim->flightgroups[Player_flight_group].start1_z + 100; + Parse_viewer_pos.xyz.z = 1000 * xwim->flightgroups[Player_flight_group].start1_y - 100; vm_angle_2_matrix(&Parse_viewer_orient, PI_4, 0); } @@ -50,32 +52,76 @@ bool is_wing(const XWMFlightGroup *fg) return (fg->numberInWave > 1 || fg->numberOfWaves > 1 || is_fighter_or_bomber(fg)); } +int xwi_flightgroup_lookup(const XWingMission *xwim, const XWMFlightGroup *fg) +{ + for (size_t i = 0; i < xwim->flightgroups.size(); i++) + { + if (xwim->flightgroups[i].designation == fg->designation) + return (int)i; + } + return -1; +} + +void xwi_add_attack_check(const XWingMission *xwim, const XWMFlightGroup *fg) +{ + char fg_name[NAME_LENGTH] = ""; + char event_name[NAME_LENGTH]; + char sexp_buf[SEXP_LENGTH]; + + int index = xwi_flightgroup_lookup(xwim, fg); + Assert(index >= 0); + + strcpy_s(fg_name, fg->designation.c_str()); + SCP_totitle(fg_name); + + sprintf(event_name, "FG %d Attack Check", index); + + if (mission_event_lookup(event_name) >= 0) + return; + + auto event = &Mission_events[Num_mission_events++]; + strcpy_s(event->name, event_name); + + if (is_wing(fg)) + sprintf(sexp_buf, "( when ( true ) ( fotg-wing-attacked-init \"%s\" ) )", fg_name); + else + sprintf(sexp_buf, "( when ( true ) ( fotg-ship-attacked-init \"%s\" ) )", fg_name); + Mp = sexp_buf; + event->formula = get_sexp_main(); +} + int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg) { - char name[NAME_LENGTH] = ""; - char sexp_buf[1024]; + const XWMFlightGroup *arrival_fg = nullptr; + char arrival_fg_name[NAME_LENGTH] = ""; + char sexp_buf[SEXP_LENGTH]; bool check_wing = false; if (fg->arrivalFlightGroup >= 0) { - strcpy_s(name, xwim->flightgroups[fg->arrivalFlightGroup].designation.c_str()); - SCP_totitle(name); - check_wing = is_wing(&xwim->flightgroups[fg->arrivalFlightGroup]); + arrival_fg = &xwim->flightgroups[fg->arrivalFlightGroup]; + check_wing = is_wing(arrival_fg); + strcpy_s(arrival_fg_name, arrival_fg->designation.c_str()); + SCP_totitle(arrival_fg_name); } + else + return Locked_sexp_true; if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_arrives) { - sprintf(sexp_buf, "( has-arrived-delay 0 \"%s\" )", name); + sprintf(sexp_buf, "( has-arrived-delay 0 \"%s\" )", arrival_fg_name); Mp = sexp_buf; return get_sexp_main(); } if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_attacked) { + xwi_add_attack_check(xwim, arrival_fg); + if (check_wing) - sprintf(sexp_buf, "( fotg-is-wing-attacked \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-wing-attacked \"%s\" )", arrival_fg_name); else - sprintf(sexp_buf, "( fotg-is-ship-attacked \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-ship-attacked \"%s\" )", arrival_fg_name); Mp = sexp_buf; return get_sexp_main(); } @@ -83,16 +129,16 @@ int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_boarded) { if (check_wing) - sprintf(sexp_buf, "( fotg-is-wing-boarded \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-wing-boarded \"%s\" )", arrival_fg_name); else - sprintf(sexp_buf, "( fotg-is-ship-boarded \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-ship-boarded \"%s\" )", arrival_fg_name); Mp = sexp_buf; return get_sexp_main(); } if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_destroyed) { - sprintf(sexp_buf, "( is-destroyed-delay 0 \"%s\" )", name); + sprintf(sexp_buf, "( is-destroyed-delay 0 \"%s\" )", arrival_fg_name); Mp = sexp_buf; return get_sexp_main(); } @@ -100,9 +146,9 @@ int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_disabled) { if (check_wing) - sprintf(sexp_buf, "( fotg-is-wing-disabled \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-wing-disabled \"%s\" )", arrival_fg_name); else - sprintf(sexp_buf, "( fotg-is-ship-disabled \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-ship-disabled \"%s\" )", arrival_fg_name); Mp = sexp_buf; return get_sexp_main(); } @@ -110,9 +156,9 @@ int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_identified) { if (check_wing) - sprintf(sexp_buf, "( fotg-is-wing-identified \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-wing-identified \"%s\" )", arrival_fg_name); else - sprintf(sexp_buf, "( fotg-is-ship-identified \"%s\" )", name); + sprintf(sexp_buf, "( fotg-is-ship-identified \"%s\" )", arrival_fg_name); Mp = sexp_buf; return get_sexp_main(); } @@ -298,18 +344,23 @@ void xwi_determine_orientation(matrix *orient, const XWMFlightGroup *fg, const v { vec3d fvec; + // RandomStarfighter says: + // If WP1 is disabled, it has 45 degree pitch and yaw. + if (!fg->waypoint1_enabled) + { + angles a; + a.p = PI_4; + a.b = 0; + a.h = PI_4; + vm_angles_2_matrix(orient, &a); + return; + } + // RandomStarfighter says: // It arrives from start point and points toward waypoint 1, if waypoint 1 is enabled. // This also matches FG Red orientation in STARSNDB vm_vec_normalized_dir(&fvec, waypoint1, start1); vm_vector_2_matrix_norm(orient, &fvec); - - if (!fg->arriveByHyperspace && fg->craftOrder == XWMCraftOrder::o_Starship_Sit_And_Fire) - { - // This matches the Intrepid's orientation in STARSNDB - vm_vec_normalized_dir(&fvec, hyperspace, start1); - vm_vector_2_matrix_norm(orient, &fvec); - } } void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFlightGroup *fg) @@ -387,13 +438,14 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh } // similarly for any waypoints - auto start1 = vm_vec_new(fg->start1_x, fg->start1_y, fg->start1_z); - auto start2 = vm_vec_new(fg->start2_x, fg->start2_y, fg->start2_z); - auto start3 = vm_vec_new(fg->start3_x, fg->start3_y, fg->start3_z); - auto waypoint1 = vm_vec_new(fg->waypoint1_x, fg->waypoint1_y, fg->waypoint1_z); - auto waypoint2 = vm_vec_new(fg->waypoint2_x, fg->waypoint2_y, fg->waypoint2_z); - auto waypoint3 = vm_vec_new(fg->waypoint3_x, fg->waypoint3_y, fg->waypoint3_z); - auto hyperspace = vm_vec_new(fg->hyperspace_x, fg->hyperspace_y, fg->hyperspace_z); + // NOTE: Y and Z are swapped + auto start1 = vm_vec_new(fg->start1_x, fg->start1_z, fg->start1_y); + auto start2 = vm_vec_new(fg->start2_x, fg->start2_z, fg->start2_y); + auto start3 = vm_vec_new(fg->start3_x, fg->start3_z, fg->start3_y); + auto waypoint1 = vm_vec_new(fg->waypoint1_x, fg->waypoint1_z, fg->waypoint1_y); + auto waypoint2 = vm_vec_new(fg->waypoint2_x, fg->waypoint2_z, fg->waypoint2_y); + auto waypoint3 = vm_vec_new(fg->waypoint3_x, fg->waypoint3_z, fg->waypoint3_y); + auto hyperspace = vm_vec_new(fg->hyperspace_x, fg->hyperspace_z, fg->hyperspace_y); // waypoint units are in kilometers (after processing by xwinglib which handles the factor of 160), so scale them up vm_vec_scale(&start1, 1000); From f75ccdb4b7f444a679185de3e33835de2ade06b3 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 6 Nov 2022 18:32:13 -0500 Subject: [PATCH 012/466] player start check --- code/mission/xwingmissionparse.cpp | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 824f1e13b76..3f1ceacbaf7 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -68,13 +68,13 @@ void xwi_add_attack_check(const XWingMission *xwim, const XWMFlightGroup *fg) char event_name[NAME_LENGTH]; char sexp_buf[SEXP_LENGTH]; - int index = xwi_flightgroup_lookup(xwim, fg); - Assert(index >= 0); + int fg_index = xwi_flightgroup_lookup(xwim, fg); + Assertion(fg_index >= 0, "Flight Group index must be valid"); strcpy_s(fg_name, fg->designation.c_str()); SCP_totitle(fg_name); - sprintf(event_name, "FG %d Attack Check", index); + sprintf(event_name, "FG %d Attack Check", fg_index); if (mission_event_lookup(event_name) >= 0) return; @@ -524,6 +524,21 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh if (fg->playerPos == wing_index + 1) { + // undo any previously set player + if (Player_starts > 0) + { + auto prev_player_pobjp = mission_parse_get_parse_object(Player_start_shipname); + if (prev_player_pobjp) + { + Warning(LOCATION, "This mission specifies multiple player starting ships. Skipping %s.", Player_start_shipname); + prev_player_pobjp->flags.remove(Mission::Parse_Object_Flags::OF_Player_start); + prev_player_pobjp->flags.remove(Mission::Parse_Object_Flags::SF_Cargo_known); + Player_starts--; + } + else + Warning(LOCATION, "Multiple player starts specified, but previous player start %s couldn't be found!", Player_start_shipname); + } + strcpy_s(Player_start_shipname, pobj.name); pobj.flags.set(Mission::Parse_Object_Flags::OF_Player_start); pobj.flags.set(Mission::Parse_Object_Flags::SF_Cargo_known); @@ -562,7 +577,7 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) if (xwim->flightgroups[i].playerPos > 0) { index = i; - break; + // don't break in case multiple FGs set a player - we will use the last one assigned } } if (index >= 0) From 06865eb5c762ba6426e6490a38ab295695ac9f8b Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 6 Nov 2022 23:18:08 -0500 Subject: [PATCH 013/466] tweak destroyed cue --- code/mission/xwingmissionparse.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 3f1ceacbaf7..177f4859982 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -138,7 +138,11 @@ int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_destroyed) { - sprintf(sexp_buf, "( is-destroyed-delay 0 \"%s\" )", arrival_fg_name); + // X-Wing treats destruction for arrivals slightly differently + if (check_wing) + sprintf(sexp_buf, "( and ( percent-ships-destroyed 1 \"%s\" ) ( destroyed-or-departed-delay 0 \"%s\" ) )", arrival_fg_name, arrival_fg_name); + else + sprintf(sexp_buf, "( is-destroyed-delay 0 \"%s\" )", arrival_fg_name); Mp = sexp_buf; return get_sexp_main(); } From 6cf6e3d06f5fe3228cf8b3e43705eea68647e7b9 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Mon, 7 Nov 2022 21:46:45 -0500 Subject: [PATCH 014/466] flight group formations --- code/mission/missionparse.cpp | 2 + code/mission/xwingmissionparse.cpp | 74 ++++++++++++++++++++++++++---- code/mission/xwingmissionparse.h | 1 + 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 4bf45c63920..1c467576454 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6096,6 +6096,8 @@ bool parse_mission(mission *pm, XWingMission *xwim, int flags) if (!post_process_mission(pm)) return false; + if (flags & MPF_IMPORT_XWI) + post_process_xwi_mission(pm, xwim); if ((saved_warning_count - Global_warning_count) > 10 || (saved_error_count - Global_error_count) > 0) { char text[512]; diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 177f4859982..dd8f6d82b76 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -185,6 +185,39 @@ int xwi_determine_anchor(const XWingMission *xwim, const XWMFlightGroup *fg) return -1; } +const char *xwi_determine_formation(const XWMFlightGroup *fg) +{ + switch (fg->formation) + { + case XWMFormation::f_Vic: + return "Double Vic"; + case XWMFormation::f_Finger_Four: + return "Finger Four"; + case XWMFormation::f_Line_Astern: + return "Line Astern"; + case XWMFormation::f_Line_Abreast: + return "Line Abreast"; + case XWMFormation::f_Echelon_Right: + return "Echelon Right"; + case XWMFormation::f_Echelon_Left: + return "Echelon Left"; + case XWMFormation::f_Double_Astern: + return "Double Astern"; + case XWMFormation::f_Diamond: + return "Diamond"; + case XWMFormation::f_Stacked: + return "Stacked"; + case XWMFormation::f_Spread: + return "Spread"; + case XWMFormation::f_Hi_Lo: + return "Hi-Lo"; + case XWMFormation::f_Spiral: + return "Spiral"; + } + + return nullptr; +} + const char *xwi_determine_base_ship_class(const XWMFlightGroup *fg) { switch (fg->flightGroupType) @@ -389,7 +422,16 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh strcpy_s(wingp->name, fg->designation.c_str()); SCP_totitle(wingp->name); wingp->num_waves = fg->numberOfWaves; - wingp->formation = -1; // TODO + + auto formation_name = xwi_determine_formation(fg); + if (formation_name) + { + wingp->formation = wing_formation_lookup(formation_name); + if (wingp->formation < 0) + Warning(LOCATION, "Formation %s from Flight Group %s was not found", formation_name, fg->designation.c_str()); + } + if (wingp->formation >= 0 && is_fighter_or_bomber(fg)) + wingp->formation_scale = 0.25f; wingp->arrival_cue = arrival_cue; wingp->arrival_delay = fg->arrivalDelay; @@ -460,8 +502,10 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh vm_vec_scale(&waypoint3, 1000); vm_vec_scale(&hyperspace, 1000); + matrix orient; + xwi_determine_orientation(&orient, fg, &start1, &start2, &start3, &waypoint1, &waypoint2, &waypoint3, &hyperspace); + // now configure each ship in the flight group - int wing_leader_pobj_index = -1; for (int wing_index = 0; wing_index < number_in_wave; wing_index++) { p_object pobj; @@ -472,9 +516,6 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh pobj.wingnum = wingnum; pobj.pos_in_wing = wing_index; pobj.arrival_cue = Locked_sexp_false; - - if (wing_index == 0) - wing_leader_pobj_index = (int)Parse_objects.size(); } else { @@ -513,10 +554,7 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh pobj.ai_class = ai_class; pobj.pos = start1; - if (wing_index == 0) - xwi_determine_orientation(&pobj.orient, fg, &start1, &start2, &start3, &waypoint1, &waypoint2, &waypoint3, &hyperspace); - else - pobj.orient = Parse_objects[wing_leader_pobj_index].orient; + pobj.orient = orient; if (wingp && wing_index == fg->specialShipNumber) pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); @@ -611,6 +649,24 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) parse_xwi_flightgroup(pm, xwim, &fg); } +void post_process_xwi_mission(mission *pm, const XWingMission *xwim) +{ + // we need to arrange all the flight groups into their formations, but this can't be done until the FRED objects are created from the parse objects + for (int wingnum = 0; wingnum < Num_wings; wingnum++) + { + auto wingp = &Wings[wingnum]; + auto leader_objp = &Objects[Ships[wingp->ship_index[0]].objnum]; + + for (int i = 1; i < wingp->wave_count; i++) + { + auto objp = &Objects[Ships[wingp->ship_index[i]].objnum]; + + get_absolute_wing_pos(&objp->pos, leader_objp, wingnum, i, false); + objp->orient = leader_objp->orient; + } + } +} + /** * Set up xwi briefing based on assumed .brf file in the same folder. If .brf is not there, * just use minimal xwi briefing. diff --git a/code/mission/xwingmissionparse.h b/code/mission/xwingmissionparse.h index c9bd1564e51..7e15b017f4d 100644 --- a/code/mission/xwingmissionparse.h +++ b/code/mission/xwingmissionparse.h @@ -6,6 +6,7 @@ class XWingMission; extern void parse_xwi_mission_info(mission *pm, const XWingMission *xwim); extern void parse_xwi_mission(mission *pm, const XWingMission *xwim); +extern void post_process_xwi_mission(mission *pm, const XWingMission *xwim); extern void parse_xwi_briefing(mission *pm, const XWingBriefing *xwBrief); #endif From 0186b9db3061722c68180d1b78242ca6061071b8 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Mon, 7 Nov 2022 22:27:09 -0500 Subject: [PATCH 015/466] hotkey --- code/mission/xwingmissionparse.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index dd8f6d82b76..b026760323f 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -651,12 +651,12 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) void post_process_xwi_mission(mission *pm, const XWingMission *xwim) { - // we need to arrange all the flight groups into their formations, but this can't be done until the FRED objects are created from the parse objects for (int wingnum = 0; wingnum < Num_wings; wingnum++) { auto wingp = &Wings[wingnum]; auto leader_objp = &Objects[Ships[wingp->ship_index[0]].objnum]; + // we need to arrange all the flight groups into their formations, but this can't be done until the FRED objects are created from the parse objects for (int i = 1; i < wingp->wave_count; i++) { auto objp = &Objects[Ships[wingp->ship_index[i]].objnum]; @@ -664,6 +664,16 @@ void post_process_xwi_mission(mission *pm, const XWingMission *xwim) get_absolute_wing_pos(&objp->pos, leader_objp, wingnum, i, false); objp->orient = leader_objp->orient; } + + // set the hotkeys for the starting wings + for (int i = 0; i < MAX_STARTING_WINGS; i++) + { + if (!stricmp(wingp->name, Starting_wing_names[i])) + { + wingp->hotkey = i; + break; + } + } } } From 94491acd23dd1a7ae401b24191698e8e4e06b16e Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 8 Nov 2022 08:39:44 -0500 Subject: [PATCH 016/466] non-fighters should be scaled up, rather than the other way around --- code/mission/xwingmissionparse.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index b026760323f..4e10630cc71 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -430,8 +430,8 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh if (wingp->formation < 0) Warning(LOCATION, "Formation %s from Flight Group %s was not found", formation_name, fg->designation.c_str()); } - if (wingp->formation >= 0 && is_fighter_or_bomber(fg)) - wingp->formation_scale = 0.25f; + if (wingp->formation >= 0 && !is_fighter_or_bomber(fg)) + wingp->formation_scale = 4.0f; wingp->arrival_cue = arrival_cue; wingp->arrival_delay = fg->arrivalDelay; From 0f4286f9762f840fb39bfd641b63796dfcd3c7db Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 12 Nov 2022 21:50:24 -0500 Subject: [PATCH 017/466] some tweaks and a fix --- code/mission/xwinglib.cpp | 4 ++-- code/mission/xwinglib.h | 4 ++-- code/mission/xwingmissionparse.cpp | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/code/mission/xwinglib.cpp b/code/mission/xwinglib.cpp index 07eec5dc7e6..9d3a1579b65 100644 --- a/code/mission/xwinglib.cpp +++ b/code/mission/xwinglib.cpp @@ -264,7 +264,7 @@ bool XWingMission::load(XWingMission *m, const char *data) nfg->arrivalEvent = XWMArrivalEvent::ae_mission_start; break; case 1: - nfg->arrivalEvent = XWMArrivalEvent::ae_afg_arrives; + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_arrived; break; case 2: nfg->arrivalEvent = XWMArrivalEvent::ae_afg_destroyed; @@ -273,7 +273,7 @@ bool XWingMission::load(XWingMission *m, const char *data) nfg->arrivalEvent = XWMArrivalEvent::ae_afg_attacked; break; case 4: - nfg->arrivalEvent = XWMArrivalEvent::ae_afg_boarded; + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_captured; break; case 5: nfg->arrivalEvent = XWMArrivalEvent::ae_afg_identified; diff --git a/code/mission/xwinglib.h b/code/mission/xwinglib.h index 1e150af1a06..a412bdff08b 100644 --- a/code/mission/xwinglib.h +++ b/code/mission/xwinglib.h @@ -41,10 +41,10 @@ enum class XWMCraftIFF : short enum class XWMArrivalEvent : short { ae_mission_start = 0, - ae_afg_arrives, + ae_afg_arrived, ae_afg_destroyed, ae_afg_attacked, - ae_afg_boarded, + ae_afg_captured, ae_afg_identified, ae_afg_disabled }; diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index 4e10630cc71..c5bb3b70979 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -107,7 +107,7 @@ int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg else return Locked_sexp_true; - if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_arrives) + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_arrived) { sprintf(sexp_buf, "( has-arrived-delay 0 \"%s\" )", arrival_fg_name); Mp = sexp_buf; @@ -126,12 +126,12 @@ int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg return get_sexp_main(); } - if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_boarded) + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_captured) { if (check_wing) - sprintf(sexp_buf, "( fotg-is-wing-boarded \"%s\" )", arrival_fg_name); + sprintf(sexp_buf, "( fotg-is-wing-captured \"%s\" )", arrival_fg_name); else - sprintf(sexp_buf, "( fotg-is-ship-boarded \"%s\" )", arrival_fg_name); + sprintf(sexp_buf, "( fotg-is-ship-captured \"%s\" )", arrival_fg_name); Mp = sexp_buf; return get_sexp_main(); } @@ -575,6 +575,7 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh Warning(LOCATION, "This mission specifies multiple player starting ships. Skipping %s.", Player_start_shipname); prev_player_pobjp->flags.remove(Mission::Parse_Object_Flags::OF_Player_start); prev_player_pobjp->flags.remove(Mission::Parse_Object_Flags::SF_Cargo_known); + prev_player_pobjp->flags.remove(Mission::Parse_Object_Flags::SF_Cannot_perform_scan); Player_starts--; } else @@ -584,6 +585,7 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh strcpy_s(Player_start_shipname, pobj.name); pobj.flags.set(Mission::Parse_Object_Flags::OF_Player_start); pobj.flags.set(Mission::Parse_Object_Flags::SF_Cargo_known); + pobj.flags.set(Mission::Parse_Object_Flags::SF_Cannot_perform_scan); Player_starts++; } @@ -644,6 +646,12 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) strcpy_s(Squadron_wing_names[0], Starting_wing_names[0]); strcpy_s(TVT_wing_names[0], Starting_wing_names[0]); + // indicate we are using X-Wing options + auto config_event = &Mission_events[Num_mission_events++]; + strcpy_s(config_event->name, "XWI Import"); + Mp = "( when ( true ) ( do-nothing ) )"; + config_event->formula = get_sexp_main(); + // load flight groups for (const auto &fg : xwim->flightgroups) parse_xwi_flightgroup(pm, xwim, &fg); From a70437e84cc5caf7296a8518532fbbc25e6f3d5c Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Mon, 20 Mar 2023 20:39:40 -0400 Subject: [PATCH 018/466] compilation and warning fixes --- code/mission/xwingmissionparse.cpp | 57 ++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/xwingmissionparse.cpp index c5bb3b70979..e0a07c90138 100644 --- a/code/mission/xwingmissionparse.cpp +++ b/code/mission/xwingmissionparse.cpp @@ -19,7 +19,7 @@ static int Player_flight_group = 0; // vazor222 void parse_xwi_mission_info(mission *pm, const XWingMission *xwim) { - strcpy_s(pm->author, "X-Wing"); + pm->author = "X-Wing"; strcpy_s(pm->created, "00/00/00 at 00:00:00"); // NOTE: Y and Z are swapped and the units are in km @@ -43,6 +43,8 @@ bool is_fighter_or_bomber(const XWMFlightGroup *fg) case XWMFlightGroupType::fg_Gunboat: case XWMFlightGroupType::fg_TIE_Advanced: return true; + default: + break; } return false; } @@ -66,7 +68,7 @@ void xwi_add_attack_check(const XWingMission *xwim, const XWMFlightGroup *fg) { char fg_name[NAME_LENGTH] = ""; char event_name[NAME_LENGTH]; - char sexp_buf[SEXP_LENGTH]; + char sexp_buf[NAME_LENGTH + 50]; int fg_index = xwi_flightgroup_lookup(xwim, fg); Assertion(fg_index >= 0, "Flight Group index must be valid"); @@ -79,8 +81,9 @@ void xwi_add_attack_check(const XWingMission *xwim, const XWMFlightGroup *fg) if (mission_event_lookup(event_name) >= 0) return; - auto event = &Mission_events[Num_mission_events++]; - strcpy_s(event->name, event_name); + Mission_events.emplace_back(); + auto event = &Mission_events.back(); + event->name = event_name; if (is_wing(fg)) sprintf(sexp_buf, "( when ( true ) ( fotg-wing-attacked-init \"%s\" ) )", fg_name); @@ -94,7 +97,7 @@ int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg { const XWMFlightGroup *arrival_fg = nullptr; char arrival_fg_name[NAME_LENGTH] = ""; - char sexp_buf[SEXP_LENGTH]; + char sexp_buf[NAME_LENGTH * 2 + 80]; bool check_wing = false; if (fg->arrivalFlightGroup >= 0) @@ -258,6 +261,8 @@ const char *xwi_determine_base_ship_class(const XWMFlightGroup *fg) return nullptr; case XWMFlightGroupType::fg_B_Wing: return "B-wing Starfighter"; + default: + break; } return nullptr; @@ -305,6 +310,8 @@ int xwi_determine_ship_class(const XWMFlightGroup *fg) const char *xwi_determine_team(const XWingMission *xwim, const XWMFlightGroup *fg, const ship_info *sip) { + SCP_UNUSED(sip); + auto player_iff = xwim->flightgroups[Player_flight_group].craftIFF; if (fg->craftIFF == XWMCraftIFF::iff_imperial) @@ -379,6 +386,11 @@ const char *xwi_determine_ai_class(const XWMFlightGroup *fg) void xwi_determine_orientation(matrix *orient, const XWMFlightGroup *fg, const vec3d *start1, const vec3d *start2, const vec3d *start3, const vec3d *waypoint1, const vec3d *waypoint2, const vec3d *waypoint3, const vec3d *hyperspace) { + SCP_UNUSED(start2); + SCP_UNUSED(start3); + SCP_UNUSED(waypoint2); + SCP_UNUSED(waypoint3); + SCP_UNUSED(hyperspace); vec3d fvec; // RandomStarfighter says: @@ -402,6 +414,8 @@ void xwi_determine_orientation(matrix *orient, const XWMFlightGroup *fg, const v void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFlightGroup *fg) { + SCP_UNUSED(pm); + int arrival_cue = xwi_determine_arrival_cue(xwim, fg); int number_in_wave = fg->numberInWave; @@ -472,13 +486,13 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh } // similarly for the AI + int ai_index = sip->ai_class; auto ai_name = xwi_determine_ai_class(fg); - int ai_class = 0; if (ai_name) { int index = string_lookup(ai_name, Ai_class_names, Num_ai_classes); if (index >= 0) - ai_class = index; + ai_index = index; else Warning(LOCATION, "Could not find AI class %s", ai_name); } @@ -540,7 +554,7 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh pobj.ship_class = ship_class; // initialize class-specific fields - pobj.ai_class = sip->ai_class; + pobj.ai_class = ai_index; pobj.warpin_params_index = sip->warpin_params_index; pobj.warpout_params_index = sip->warpout_params_index; pobj.ship_max_shield_strength = sip->max_shield_strength; @@ -551,8 +565,6 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh pobj.score = sip->score; pobj.team = team; - pobj.ai_class = ai_class; - pobj.pos = start1; pobj.orient = orient; @@ -614,9 +626,10 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh void parse_xwi_mission(mission *pm, const XWingMission *xwim) { int index = -1; + char sexp_buf[35]; // find player flight group - for (int i = 0; i < xwim->flightgroups.size(); i++) + for (int i = 0; i < (int)xwim->flightgroups.size(); i++) { if (xwim->flightgroups[i].playerPos > 0) { @@ -647,9 +660,12 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) strcpy_s(TVT_wing_names[0], Starting_wing_names[0]); // indicate we are using X-Wing options - auto config_event = &Mission_events[Num_mission_events++]; - strcpy_s(config_event->name, "XWI Import"); - Mp = "( when ( true ) ( do-nothing ) )"; + Mission_events.emplace_back(); + auto config_event = &Mission_events.back(); + config_event->name = "XWI Import"; + + sprintf(sexp_buf, "( when ( true ) ( do-nothing ) )"); + Mp = sexp_buf; config_event->formula = get_sexp_main(); // load flight groups @@ -659,6 +675,9 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) void post_process_xwi_mission(mission *pm, const XWingMission *xwim) { + SCP_UNUSED(pm); + SCP_UNUSED(xwim); + for (int wingnum = 0; wingnum < Num_wings; wingnum++) { auto wingp = &Wings[wingnum]; @@ -693,12 +712,12 @@ void post_process_xwi_mission(mission *pm, const XWingMission *xwim) */ void parse_xwi_briefing(mission *pm, const XWingBriefing *xwBrief) { - brief_stage *bs; - briefing *bp; + SCP_UNUSED(pm); + SCP_UNUSED(xwBrief); - bp = &Briefings[0]; + auto bp = &Briefings[0]; bp->num_stages = 1; // xwing briefings only have one stage - bs = &bp->stages[0]; + auto bs = &bp->stages[0]; /* if (xwBrief != NULL) @@ -713,7 +732,7 @@ void parse_xwi_briefing(mission *pm, const XWingBriefing *xwBrief) bs->text = "Prepare for the next X-Wing mission!"; strcpy_s(bs->voice, "none.wav"); vm_vec_zero(&bs->camera_pos); - bs->camera_orient = SCALE_IDENTITY_VECTOR; + bs->camera_orient = IDENTITY_MATRIX; bs->camera_time = 500; bs->num_lines = 0; bs->num_icons = 0; From 791cf1591a1bef7462bc3a14fc5bbcc3b277c578 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Fri, 21 Apr 2023 22:12:05 -0400 Subject: [PATCH 019/466] address feedback --- code/mission/{ => import}/xwingbrflib.cpp | 0 code/mission/{ => import}/xwingbrflib.h | 0 code/mission/{ => import}/xwinglib.cpp | 0 code/mission/{ => import}/xwinglib.h | 0 code/mission/{ => import}/xwingmissionparse.cpp | 0 code/mission/{ => import}/xwingmissionparse.h | 0 code/mission/missionparse.cpp | 6 +++--- code/source_groups.cmake | 15 +++++++++------ 8 files changed, 12 insertions(+), 9 deletions(-) rename code/mission/{ => import}/xwingbrflib.cpp (100%) rename code/mission/{ => import}/xwingbrflib.h (100%) rename code/mission/{ => import}/xwinglib.cpp (100%) rename code/mission/{ => import}/xwinglib.h (100%) rename code/mission/{ => import}/xwingmissionparse.cpp (100%) rename code/mission/{ => import}/xwingmissionparse.h (100%) diff --git a/code/mission/xwingbrflib.cpp b/code/mission/import/xwingbrflib.cpp similarity index 100% rename from code/mission/xwingbrflib.cpp rename to code/mission/import/xwingbrflib.cpp diff --git a/code/mission/xwingbrflib.h b/code/mission/import/xwingbrflib.h similarity index 100% rename from code/mission/xwingbrflib.h rename to code/mission/import/xwingbrflib.h diff --git a/code/mission/xwinglib.cpp b/code/mission/import/xwinglib.cpp similarity index 100% rename from code/mission/xwinglib.cpp rename to code/mission/import/xwinglib.cpp diff --git a/code/mission/xwinglib.h b/code/mission/import/xwinglib.h similarity index 100% rename from code/mission/xwinglib.h rename to code/mission/import/xwinglib.h diff --git a/code/mission/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp similarity index 100% rename from code/mission/xwingmissionparse.cpp rename to code/mission/import/xwingmissionparse.cpp diff --git a/code/mission/xwingmissionparse.h b/code/mission/import/xwingmissionparse.h similarity index 100% rename from code/mission/xwingmissionparse.h rename to code/mission/import/xwingmissionparse.h diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 1c467576454..904f17f7b11 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -43,9 +43,9 @@ #include "mission/missionlog.h" #include "mission/missionmessage.h" #include "mission/missionparse.h" -#include "mission/xwingbrflib.h" -#include "mission/xwinglib.h" -#include "mission/xwingmissionparse.h" +#include "mission/import/xwingbrflib.h" +#include "mission/import/xwinglib.h" +#include "mission/import/xwingmissionparse.h" #include "missionui/fictionviewer.h" #include "missionui/missioncmdbrief.h" #include "missionui/redalert.h" diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 682a7caa6f8..1b7efe84b82 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -823,12 +823,15 @@ add_file_folder("Mission" mission/missiontraining.cpp mission/missiontraining.h mission/mission_flags.h - mission/xwingbrflib.cpp - mission/xwingbrflib.h - mission/xwinglib.cpp - mission/xwinglib.h - mission/xwingmissionparse.cpp - mission/xwingmissionparse.h +) + +add_file_folder("Mission\\\\Import" + mission/import/xwingbrflib.cpp + mission/import/xwingbrflib.h + mission/import/xwinglib.cpp + mission/import/xwinglib.h + mission/import/xwingmissionparse.cpp + mission/import/xwingmissionparse.h ) # MissionUI files From fa96ea0dfc6a09f5afbb97e12106276bf3d2e78b Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Fri, 21 Apr 2023 22:15:55 -0400 Subject: [PATCH 020/466] disambiguate IDs --- fred2/fred.rc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fred2/fred.rc b/fred2/fred.rc index f4b6bc0690f..d9733c89677 100644 --- a/fred2/fred.rc +++ b/fred2/fred.rc @@ -219,7 +219,7 @@ BEGIN POPUP "&Import" BEGIN MENUITEM "&FreeSpace 1 mission...", 33074 - MENUITEM "&X-Wing mission...", 33102 + MENUITEM "&X-Wing mission...", ID_IMPORT_XWIMISSION END MENUITEM SEPARATOR MENUITEM "&Run FreeSpace", 32985 From 46cb6aea700e02c74106624fc55748f42fd7f5c7 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Tue, 17 Oct 2023 22:00:34 +0100 Subject: [PATCH 021/466] lib files should be close missionparse is not close --- code/mission/import/xwinglib.cpp | 173 ++++++++++++++++++++++ code/mission/import/xwinglib.h | 61 ++++++++ code/mission/import/xwingmissionparse.cpp | 138 +++++++++++++++++ 3 files changed, 372 insertions(+) diff --git a/code/mission/import/xwinglib.cpp b/code/mission/import/xwinglib.cpp index 9d3a1579b65..a8c5225b7c0 100644 --- a/code/mission/import/xwinglib.cpp +++ b/code/mission/import/xwinglib.cpp @@ -74,6 +74,23 @@ struct xwi_flightgroup { }; #pragma pack(pop) +struct xwi_objectgroup { + char designation[16]; // ignored? + char cargo[16]; // ignored? + char special_cargo[16]; // ignored? + short special_object_number; // ignored? + short object_type; + short object_iff; + short object_formation; + short number_of_objects; + short object_x; + short object_y; + short object_z; + short object_yaw; + short object_pitch; + short object_roll; +}; + int XWingMission::arrival_delay_to_seconds(int delay) { // If the arrival delay is less than or equal to 20, it's in minutes. If it's over 20, it's in 6 second blocks. So 21 is 6 seconds, for example @@ -582,5 +599,161 @@ bool XWingMission::load(XWingMission *m, const char *data) m->flightgroups.push_back(*nfg); } + for (int n = 0; n < h->number_of_objects; n++) { + xwi_objectgroup *oj = + (xwi_objectgroup*)(data + sizeof(xwi_header) + sizeof(xwi_flightgroup) + sizeof(xwi_objectgroup) * n); + XWMObject noj_buf; + XWMObject *noj = &noj_buf; + + noj->designation = oj->designation; + noj->cargo = oj->cargo; + noj->specialCargo = oj->special_cargo; + noj->specialObjectNumber = oj->special_object_number; + + switch (oj->object_type) { + case 0: + noj->objectType = XWMObjectType::oj_Mine1; + break; + case 1: + noj->objectType = XWMObjectType::oj_Mine2; + break; + case 2: + noj->objectType = XWMObjectType::oj_Mine3; + break; + case 3: + noj->objectType = XWMObjectType::oj_Mine4; + break; + case 4: + noj->objectType = XWMObjectType::oj_Satellite; + break; + case 5: + noj->objectType = XWMObjectType::oj_Nav_Buoy; + break; + case 6: + noj->objectType = XWMObjectType::oj_Probe; + break; + case 7: + noj->objectType = XWMObjectType::oj_Platform; + break; + case 8: + noj->objectType = XWMObjectType::oj_Asteroid1; + break; + case 9: + noj->objectType = XWMObjectType::oj_Asteroid2; + break; + case 10: + noj->objectType = XWMObjectType::oj_Asteroid3; + break; + case 11: + noj->objectType = XWMObjectType::oj_Asteroid4; + break; + case 12: + noj->objectType = XWMObjectType::oj_Asteroid5; + break; + case 13: + noj->objectType = XWMObjectType::oj_Asteroid6; + break; + case 14: + noj->objectType = XWMObjectType::oj_Asteroid7; + break; + case 15: + noj->objectType = XWMObjectType::oj_Asteroid8; + break; + case 16: + noj->objectType = XWMObjectType::oj_Rock_World; + break; + case 17: + noj->objectType = XWMObjectType::oj_Gray_Ring_World; + break; + case 18: + noj->objectType = XWMObjectType::oj_Gray_World; + break; + case 19: + noj->objectType = XWMObjectType::oj_Brown_World; + break; + case 20: + noj->objectType = XWMObjectType::oj_Gray_World2; + break; + case 21: + noj->objectType = XWMObjectType::oj_Planet_and_Moon; + break; + case 22: + noj->objectType = XWMObjectType::oj_Gray_Crescent; + break; + case 23: + noj->objectType = XWMObjectType::oj_Orange_Crescent1; + break; + case 24: + noj->objectType = XWMObjectType::oj_Orange_Crescent2; + break; + case 25: + noj->objectType = XWMObjectType::oj_Orange_Crescent3; + break; + case 26: + noj->objectType = XWMObjectType::oj_Orange_Crescent4; + break; + case 27: + noj->objectType = XWMObjectType::oj_Orange_Crescent5; + break; + case 28: + noj->objectType = XWMObjectType::oj_Orange_Crescent6; + break; + case 29: + noj->objectType = XWMObjectType::oj_Orange_Crescent7; + break; + case 30: + noj->objectType = XWMObjectType::oj_Orange_Crescent8; + break; + case 31: + noj->objectType = XWMObjectType::oj_Death_Star; + break; + default: + return false; + } + + switch (oj->object_iff) { // seemingly not used + case 0: + noj->objectIFF = XWMCraftIFF::iff_default; + break; + case 1: + noj->objectIFF = XWMCraftIFF::iff_rebel; + break; + case 2: + noj->objectIFF = XWMCraftIFF::iff_imperial; + break; + case 3: + noj->objectIFF = XWMCraftIFF::iff_neutral; + break; + } + + switch (oj->object_formation) { + case 0: + noj->formation = XWMObjectFormation::ojf_Flat; + break; + case 1: + noj->formation = XWMObjectFormation::ojf_Edge; + break; + case 2: + noj->formation = XWMObjectFormation::ojf_Broadside; + break; + case 3: + noj->formation = XWMObjectFormation::ojf_Scattered; + break; + case 4: + noj->formation = XWMObjectFormation::ojf_Destroy; + break; + default: + return false; + } + + noj->numberOfObjects = oj->number_of_objects; + + noj->object_x = oj->object_x / 160.0f; + noj->object_y = oj->object_y / 160.0f; + noj->object_z = oj->object_z / 160.0f; + noj->object_yaw = oj->object_yaw / 160.0f; + noj->object_pitch = oj->object_pitch / 160.0f; + noj->object_roll = oj->object_roll / 160.0f; + } return true; } diff --git a/code/mission/import/xwinglib.h b/code/mission/import/xwinglib.h index a412bdff08b..4f5813dbc72 100644 --- a/code/mission/import/xwinglib.h +++ b/code/mission/import/xwinglib.h @@ -138,6 +138,51 @@ enum class XWMObjective : short o_Arrive }; +enum class XWMObjectType : short +{ + oj_Mine1, + oj_Mine2, + oj_Mine3, + oj_Mine4, + oj_Satellite, + oj_Nav_Buoy, + oj_Probe, + oj_Platform, // guessing it's a (training) platform here - ?? + oj_Asteroid1, + oj_Asteroid2, + oj_Asteroid3, + oj_Asteroid4, + oj_Asteroid5, + oj_Asteroid6, + oj_Asteroid7, + oj_Asteroid8, + oj_Rock_World, + oj_Gray_Ring_World, + oj_Gray_World, + oj_Brown_World, + oj_Gray_World2, + oj_Planet_and_Moon, + oj_Gray_Crescent, + oj_Orange_Crescent1, + oj_Orange_Crescent2, + oj_Orange_Crescent3, + oj_Orange_Crescent4, + oj_Orange_Crescent5, + oj_Orange_Crescent6, + oj_Orange_Crescent7, + oj_Orange_Crescent8, + oj_Death_Star +}; + +enum class XWMObjectFormation : short +{ + ojf_Flat, + ojf_Edge, + ojf_Broadside, + ojf_Scattered, + ojf_Destroy +}; + class XWMFlightGroup { @@ -210,6 +255,22 @@ class XWMObject { public: + std::string designation; + std::string cargo; + std::string specialCargo; + + int specialObjectNumber; + + XWMObjectType objectType; + + XWMCraftIFF objectIFF; // shares with FlightGroup + + XWMObjectFormation formation; + + int numberOfObjects; + + float object_x, object_y, object_z; + float object_yaw, object_pitch, object_roll; }; diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index e0a07c90138..256ddf9150b 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -623,6 +623,144 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh } } +void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObject *oj) +{ + SCP_UNUSED(pm); + + int number_of_objects = oj->numberOfObjects; // needs to be fixed in FSO for large minefields... + if (number_of_objects > MAX_SHIPS_PER_WING) { + Warning(LOCATION, + "Too many ships in Flight Group %s. FreeSpace supports up to a maximum of %d.", + fg->designation.c_str(), + MAX_SHIPS_PER_WING); + number_of_objects = MAX_SHIPS_PER_WING; + } + + // see if this flight group is what FreeSpace would treat as a wing... not sure if we need for objects? + wing* wingp = nullptr; + int wingnum = -1; + if (is_wing(fg)) { + wingnum = Num_wings++; + wingp = &Wings[wingnum]; + + strcpy_s(wingp->name, fg->designation.c_str()); + SCP_totitle(wingp->name); + wingp->num_waves = fg->numberOfWaves; + + auto formation_name = xwi_determine_formation(fg); + if (formation_name) { + wingp->formation = wing_formation_lookup(formation_name); + if (wingp->formation < 0) + Warning(LOCATION, + "Formation %s from Flight Group %s was not found", + formation_name, + fg->designation.c_str()); + } + if (wingp->formation >= 0 && !is_fighter_or_bomber(fg)) + wingp->formation_scale = 4.0f; + + wingp->arrival_cue = arrival_cue; + wingp->arrival_delay = fg->arrivalDelay; + wingp->arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; + wingp->arrival_anchor = xwi_determine_anchor(xwim, fg); + wingp->departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; + wingp->departure_anchor = wingp->arrival_anchor; + + // if a wing doesn't have an anchor, make sure it is at-location + // (flight groups present at mission start will have arriveByHyperspace set to false) + if (wingp->arrival_anchor < 0) + wingp->arrival_location = ARRIVE_AT_LOCATION; + if (wingp->departure_anchor < 0) + wingp->departure_location = DEPART_AT_LOCATION; + + } + + // all ships in the flight group share a class, so determine that here..... not sure if we need for objects + int ship_class = xwi_determine_ship_class(fg); + if (ship_class < 0) { + Warning(LOCATION, "Unable to determine ship class for Flight Group %s", fg->designation.c_str()); + ship_class = 0; + } + auto sip = &Ship_info[ship_class]; + + // similarly for the team....... needed for objects? Apparently in XW the iff isn't utilised for objects?? + auto team_name = xwi_determine_team(xwim, oj, sip); + int team = Species_info[sip->species].default_iff; + if (team_name) { + int index = iff_lookup(team_name); + if (index >= 0) + team = index; + else + Warning(LOCATION, "Could not find iff %s", team_name); + } + + // object position and orientation + // NOTE: Y and Z are swapped + auto ojxyz = vm_vec_new(oj->object_x, oj->object_z, oj->object_y); + auto okPRB = vm_vec_new(fg->start2_x, fg->start2_z, fg->start2_y); //change to yaw pitch roll + + // units are in kilometers (after processing by xwinglib which handles the factor of 160), so scale them up + vm_vec_scale(&ojxyz, 1000); + + matrix orient; // we have yaw pitch and roll so these can be input directly here for object instead of orient + xwi_determine_orientation(&orient, fg, &start1, &start2, &start3, &waypoint1, &waypoint2, &waypoint3, &hyperspace); + + // now configure each ship in the flight group..... not sure what needs to be done here exactly + for (int wing_index = 0; wing_index < number_in_wave; wing_index++) { + p_object pobj; + + if (wingp) { + wing_bash_ship_name(pobj.name, wingp->name, wing_index + 1, nullptr); + pobj.wingnum = wingnum; + pobj.pos_in_wing = wing_index; + pobj.arrival_cue = Locked_sexp_false; + } else { + strcpy_s(pobj.name, fg->designation.c_str()); + SCP_totitle(pobj.name); + + pobj.arrival_cue = arrival_cue; + pobj.arrival_delay = fg->arrivalDelay; + pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; + pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); + pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; + pobj.departure_anchor = pobj.arrival_anchor; + + // if a ship doesn't have an anchor, make sure it is at-location + // (flight groups present at mission start will have arriveByHyperspace set to false) + if (pobj.arrival_anchor < 0) + pobj.arrival_location = ARRIVE_AT_LOCATION; + if (pobj.departure_anchor < 0) + pobj.departure_location = DEPART_AT_LOCATION; + } + + pobj.ship_class = ship_class; + + // initialize class-specific fields + pobj.ai_class = ai_index; + pobj.warpin_params_index = sip->warpin_params_index; + pobj.warpout_params_index = sip->warpout_params_index; + pobj.ship_max_shield_strength = sip->max_shield_strength; + pobj.ship_max_hull_strength = sip->max_hull_strength; + Assert( + pobj.ship_max_hull_strength > 0.0f); // Goober5000: div-0 check (not shield because we might not have one) + pobj.max_shield_recharge = sip->max_shield_recharge; + pobj.replacement_textures = + sip->replacement_textures; // initialize our set with the ship class set, which may be empty + pobj.score = sip->score; + + pobj.team = team; + pobj.pos = start1; + pobj.orient = orient; + + if (wingp && wing_index == fg->specialShipNumber) + pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); + else + pobj.cargo1 = (char)xwi_lookup_cargo(fg->cargo.c_str()); + + Parse_objects.push_back(pobj); + } +} + void parse_xwi_mission(mission *pm, const XWingMission *xwim) { int index = -1; From b495da68ccae41d6a9155ef76d234a3606f4e294 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:41:24 +0100 Subject: [PATCH 022/466] lib file updates and corrections for Objects Change missionparse.cpp so this is purely updates to the xwinglib.cpp and xwinglib.h files in relation to the import of objects. Remove redundant entries and values that do not apply to objects. Update naming conventions in line with more recent XWI docs. Separate goals from formation. --- code/mission/import/xwinglib.cpp | 148 +++++++++++++--------- code/mission/import/xwinglib.h | 40 +++--- code/mission/import/xwingmissionparse.cpp | 138 -------------------- 3 files changed, 110 insertions(+), 216 deletions(-) diff --git a/code/mission/import/xwinglib.cpp b/code/mission/import/xwinglib.cpp index a8c5225b7c0..8eb65dcff74 100644 --- a/code/mission/import/xwinglib.cpp +++ b/code/mission/import/xwinglib.cpp @@ -72,16 +72,12 @@ struct xwi_flightgroup { short primary_target; short secondary_target; }; -#pragma pack(pop) struct xwi_objectgroup { - char designation[16]; // ignored? - char cargo[16]; // ignored? - char special_cargo[16]; // ignored? - short special_object_number; // ignored? short object_type; short object_iff; short object_formation; + short object_goal; short number_of_objects; short object_x; short object_y; @@ -90,6 +86,7 @@ struct xwi_objectgroup { short object_pitch; short object_roll; }; +#pragma pack(pop) int XWingMission::arrival_delay_to_seconds(int delay) { @@ -601,146 +598,171 @@ bool XWingMission::load(XWingMission *m, const char *data) for (int n = 0; n < h->number_of_objects; n++) { xwi_objectgroup *oj = - (xwi_objectgroup*)(data + sizeof(xwi_header) + sizeof(xwi_flightgroup) + sizeof(xwi_objectgroup) * n); + (xwi_objectgroup *)(data + sizeof(xwi_header) + (sizeof(xwi_flightgroup) * h->number_of_flight_groups) + + sizeof(xwi_objectgroup) * n); XWMObject noj_buf; XWMObject *noj = &noj_buf; - noj->designation = oj->designation; - noj->cargo = oj->cargo; - noj->specialCargo = oj->special_cargo; - noj->specialObjectNumber = oj->special_object_number; - switch (oj->object_type) { - case 0: + case 18: noj->objectType = XWMObjectType::oj_Mine1; break; - case 1: + case 19: noj->objectType = XWMObjectType::oj_Mine2; break; - case 2: + case 20: noj->objectType = XWMObjectType::oj_Mine3; break; - case 3: + case 21: noj->objectType = XWMObjectType::oj_Mine4; break; - case 4: + case 22: noj->objectType = XWMObjectType::oj_Satellite; break; - case 5: + case 23: noj->objectType = XWMObjectType::oj_Nav_Buoy; break; - case 6: + case 24: noj->objectType = XWMObjectType::oj_Probe; break; - case 7: - noj->objectType = XWMObjectType::oj_Platform; - break; - case 8: + case 26: noj->objectType = XWMObjectType::oj_Asteroid1; break; - case 9: + case 27: noj->objectType = XWMObjectType::oj_Asteroid2; break; - case 10: + case 28: noj->objectType = XWMObjectType::oj_Asteroid3; break; - case 11: + case 29: noj->objectType = XWMObjectType::oj_Asteroid4; break; - case 12: + case 30: noj->objectType = XWMObjectType::oj_Asteroid5; break; - case 13: + case 31: noj->objectType = XWMObjectType::oj_Asteroid6; break; - case 14: + case 32: noj->objectType = XWMObjectType::oj_Asteroid7; break; - case 15: + case 33: noj->objectType = XWMObjectType::oj_Asteroid8; break; - case 16: + case 34: noj->objectType = XWMObjectType::oj_Rock_World; break; - case 17: + case 35: noj->objectType = XWMObjectType::oj_Gray_Ring_World; break; - case 18: + case 36: noj->objectType = XWMObjectType::oj_Gray_World; break; - case 19: + case 37: noj->objectType = XWMObjectType::oj_Brown_World; break; - case 20: + case 38: noj->objectType = XWMObjectType::oj_Gray_World2; break; - case 21: + case 39: noj->objectType = XWMObjectType::oj_Planet_and_Moon; break; - case 22: + case 40: noj->objectType = XWMObjectType::oj_Gray_Crescent; break; - case 23: + case 41: noj->objectType = XWMObjectType::oj_Orange_Crescent1; break; - case 24: + case 42: noj->objectType = XWMObjectType::oj_Orange_Crescent2; break; - case 25: + case 43: noj->objectType = XWMObjectType::oj_Orange_Crescent3; break; - case 26: + case 44: noj->objectType = XWMObjectType::oj_Orange_Crescent4; break; - case 27: + case 45: noj->objectType = XWMObjectType::oj_Orange_Crescent5; break; - case 28: + case 46: noj->objectType = XWMObjectType::oj_Orange_Crescent6; break; - case 29: + case 47: noj->objectType = XWMObjectType::oj_Orange_Crescent7; break; - case 30: + case 48: noj->objectType = XWMObjectType::oj_Orange_Crescent8; break; - case 31: + case 49: noj->objectType = XWMObjectType::oj_Death_Star; break; + case 58: + noj->objectType = XWMObjectType::oj_Training_Platform1; + break; + case 59: + noj->objectType = XWMObjectType::oj_Training_Platform2; + break; + case 60: + noj->objectType = XWMObjectType::oj_Training_Platform3; + break; + case 61: + noj->objectType = XWMObjectType::oj_Training_Platform4; + break; + case 62: + noj->objectType = XWMObjectType::oj_Training_Platform5; + break; + case 63: + noj->objectType = XWMObjectType::oj_Training_Platform6; + break; + case 64: + noj->objectType = XWMObjectType::oj_Training_Platform7; + break; + case 65: + noj->objectType = XWMObjectType::oj_Training_Platform8; + break; + case 66: + noj->objectType = XWMObjectType::oj_Training_Platform9; + break; + case 67: + noj->objectType = XWMObjectType::oj_Training_Platform10; + break; + case 68: + noj->objectType = XWMObjectType::oj_Training_Platform11; + break; + case 69: + noj->objectType = XWMObjectType::oj_Training_Platform12; + break; default: return false; } - switch (oj->object_iff) { // seemingly not used + switch (oj->object_formation) { case 0: - noj->objectIFF = XWMCraftIFF::iff_default; + noj->formation = XWMObjectFormation::ojf_FloorXY; break; case 1: - noj->objectIFF = XWMCraftIFF::iff_rebel; + noj->formation = XWMObjectFormation::ojf_SideYZ; break; case 2: - noj->objectIFF = XWMCraftIFF::iff_imperial; + noj->formation = XWMObjectFormation::ojf_FrontXZ; break; case 3: - noj->objectIFF = XWMCraftIFF::iff_neutral; + noj->formation = XWMObjectFormation::ojf_Scattered; break; + default: + return false; } - switch (oj->object_formation) { + switch (oj->object_goal) { case 0: - noj->formation = XWMObjectFormation::ojf_Flat; + noj->objectGoal = XWMObjectGoal::ojg_Neither; break; case 1: - noj->formation = XWMObjectFormation::ojf_Edge; + noj->objectGoal = XWMObjectGoal::ojg_Destroyed; break; case 2: - noj->formation = XWMObjectFormation::ojf_Broadside; - break; - case 3: - noj->formation = XWMObjectFormation::ojf_Scattered; - break; - case 4: - noj->formation = XWMObjectFormation::ojf_Destroy; + noj->objectGoal = XWMObjectGoal::ojg_Survive; break; default: return false; @@ -751,9 +773,9 @@ bool XWingMission::load(XWingMission *m, const char *data) noj->object_x = oj->object_x / 160.0f; noj->object_y = oj->object_y / 160.0f; noj->object_z = oj->object_z / 160.0f; - noj->object_yaw = oj->object_yaw / 160.0f; - noj->object_pitch = oj->object_pitch / 160.0f; - noj->object_roll = oj->object_roll / 160.0f; + noj->object_yaw = oj->object_yaw; + noj->object_pitch = oj->object_pitch; + noj->object_roll = oj->object_roll - 90.0f; } return true; } diff --git a/code/mission/import/xwinglib.h b/code/mission/import/xwinglib.h index 4f5813dbc72..b7299c70c68 100644 --- a/code/mission/import/xwinglib.h +++ b/code/mission/import/xwinglib.h @@ -147,7 +147,6 @@ enum class XWMObjectType : short oj_Satellite, oj_Nav_Buoy, oj_Probe, - oj_Platform, // guessing it's a (training) platform here - ?? oj_Asteroid1, oj_Asteroid2, oj_Asteroid3, @@ -171,18 +170,35 @@ enum class XWMObjectType : short oj_Orange_Crescent6, oj_Orange_Crescent7, oj_Orange_Crescent8, - oj_Death_Star + oj_Death_Star, + oj_Training_Platform1, + oj_Training_Platform2, + oj_Training_Platform3, + oj_Training_Platform4, + oj_Training_Platform5, + oj_Training_Platform6, + oj_Training_Platform7, + oj_Training_Platform8, + oj_Training_Platform9, + oj_Training_Platform10, + oj_Training_Platform11, + oj_Training_Platform12 }; enum class XWMObjectFormation : short { - ojf_Flat, - ojf_Edge, - ojf_Broadside, - ojf_Scattered, - ojf_Destroy + ojf_FloorXY, + ojf_SideYZ, + ojf_FrontXZ, + ojf_Scattered // may be buggy - undefined locations }; +enum class XWMObjectGoal : short +{ + ojg_Survive, + ojg_Destroyed, + ojg_Neither +}; class XWMFlightGroup { @@ -255,18 +271,12 @@ class XWMObject { public: - std::string designation; - std::string cargo; - std::string specialCargo; - - int specialObjectNumber; - XWMObjectType objectType; - XWMCraftIFF objectIFF; // shares with FlightGroup - XWMObjectFormation formation; + XWMObjectGoal objectGoal; + int numberOfObjects; float object_x, object_y, object_z; diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 256ddf9150b..e0a07c90138 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -623,144 +623,6 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh } } -void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObject *oj) -{ - SCP_UNUSED(pm); - - int number_of_objects = oj->numberOfObjects; // needs to be fixed in FSO for large minefields... - if (number_of_objects > MAX_SHIPS_PER_WING) { - Warning(LOCATION, - "Too many ships in Flight Group %s. FreeSpace supports up to a maximum of %d.", - fg->designation.c_str(), - MAX_SHIPS_PER_WING); - number_of_objects = MAX_SHIPS_PER_WING; - } - - // see if this flight group is what FreeSpace would treat as a wing... not sure if we need for objects? - wing* wingp = nullptr; - int wingnum = -1; - if (is_wing(fg)) { - wingnum = Num_wings++; - wingp = &Wings[wingnum]; - - strcpy_s(wingp->name, fg->designation.c_str()); - SCP_totitle(wingp->name); - wingp->num_waves = fg->numberOfWaves; - - auto formation_name = xwi_determine_formation(fg); - if (formation_name) { - wingp->formation = wing_formation_lookup(formation_name); - if (wingp->formation < 0) - Warning(LOCATION, - "Formation %s from Flight Group %s was not found", - formation_name, - fg->designation.c_str()); - } - if (wingp->formation >= 0 && !is_fighter_or_bomber(fg)) - wingp->formation_scale = 4.0f; - - wingp->arrival_cue = arrival_cue; - wingp->arrival_delay = fg->arrivalDelay; - wingp->arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; - wingp->arrival_anchor = xwi_determine_anchor(xwim, fg); - wingp->departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; - wingp->departure_anchor = wingp->arrival_anchor; - - // if a wing doesn't have an anchor, make sure it is at-location - // (flight groups present at mission start will have arriveByHyperspace set to false) - if (wingp->arrival_anchor < 0) - wingp->arrival_location = ARRIVE_AT_LOCATION; - if (wingp->departure_anchor < 0) - wingp->departure_location = DEPART_AT_LOCATION; - - } - - // all ships in the flight group share a class, so determine that here..... not sure if we need for objects - int ship_class = xwi_determine_ship_class(fg); - if (ship_class < 0) { - Warning(LOCATION, "Unable to determine ship class for Flight Group %s", fg->designation.c_str()); - ship_class = 0; - } - auto sip = &Ship_info[ship_class]; - - // similarly for the team....... needed for objects? Apparently in XW the iff isn't utilised for objects?? - auto team_name = xwi_determine_team(xwim, oj, sip); - int team = Species_info[sip->species].default_iff; - if (team_name) { - int index = iff_lookup(team_name); - if (index >= 0) - team = index; - else - Warning(LOCATION, "Could not find iff %s", team_name); - } - - // object position and orientation - // NOTE: Y and Z are swapped - auto ojxyz = vm_vec_new(oj->object_x, oj->object_z, oj->object_y); - auto okPRB = vm_vec_new(fg->start2_x, fg->start2_z, fg->start2_y); //change to yaw pitch roll - - // units are in kilometers (after processing by xwinglib which handles the factor of 160), so scale them up - vm_vec_scale(&ojxyz, 1000); - - matrix orient; // we have yaw pitch and roll so these can be input directly here for object instead of orient - xwi_determine_orientation(&orient, fg, &start1, &start2, &start3, &waypoint1, &waypoint2, &waypoint3, &hyperspace); - - // now configure each ship in the flight group..... not sure what needs to be done here exactly - for (int wing_index = 0; wing_index < number_in_wave; wing_index++) { - p_object pobj; - - if (wingp) { - wing_bash_ship_name(pobj.name, wingp->name, wing_index + 1, nullptr); - pobj.wingnum = wingnum; - pobj.pos_in_wing = wing_index; - pobj.arrival_cue = Locked_sexp_false; - } else { - strcpy_s(pobj.name, fg->designation.c_str()); - SCP_totitle(pobj.name); - - pobj.arrival_cue = arrival_cue; - pobj.arrival_delay = fg->arrivalDelay; - pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; - pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); - pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; - pobj.departure_anchor = pobj.arrival_anchor; - - // if a ship doesn't have an anchor, make sure it is at-location - // (flight groups present at mission start will have arriveByHyperspace set to false) - if (pobj.arrival_anchor < 0) - pobj.arrival_location = ARRIVE_AT_LOCATION; - if (pobj.departure_anchor < 0) - pobj.departure_location = DEPART_AT_LOCATION; - } - - pobj.ship_class = ship_class; - - // initialize class-specific fields - pobj.ai_class = ai_index; - pobj.warpin_params_index = sip->warpin_params_index; - pobj.warpout_params_index = sip->warpout_params_index; - pobj.ship_max_shield_strength = sip->max_shield_strength; - pobj.ship_max_hull_strength = sip->max_hull_strength; - Assert( - pobj.ship_max_hull_strength > 0.0f); // Goober5000: div-0 check (not shield because we might not have one) - pobj.max_shield_recharge = sip->max_shield_recharge; - pobj.replacement_textures = - sip->replacement_textures; // initialize our set with the ship class set, which may be empty - pobj.score = sip->score; - - pobj.team = team; - pobj.pos = start1; - pobj.orient = orient; - - if (wingp && wing_index == fg->specialShipNumber) - pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); - else - pobj.cargo1 = (char)xwi_lookup_cargo(fg->cargo.c_str()); - - Parse_objects.push_back(pobj); - } -} - void parse_xwi_mission(mission *pm, const XWingMission *xwim) { int index = -1; From e8787975ec4b46a3daeca7adf27f8e81b2fe736d Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Fri, 20 Oct 2023 19:27:47 +0100 Subject: [PATCH 023/466] Update do BitWise operations to lib files Update the xwinglib.cpp so that struct xwi_objectgroup would contain all the necessary entries. Also update the formation and goal BitWise reads so that they are read correctly. --- code/mission/import/xwinglib.cpp | 29 +++++++++++++---------------- code/mission/import/xwinglib.h | 4 ++-- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/code/mission/import/xwinglib.cpp b/code/mission/import/xwinglib.cpp index 8eb65dcff74..e8734bbc4da 100644 --- a/code/mission/import/xwinglib.cpp +++ b/code/mission/import/xwinglib.cpp @@ -74,10 +74,13 @@ struct xwi_flightgroup { }; struct xwi_objectgroup { + char designation[16]; // ignored? + char cargo[16]; // ignored? + char special_cargo[16]; // ignored? + short special_object_number; short object_type; short object_iff; short object_formation; - short object_goal; short number_of_objects; short object_x; short object_y; @@ -737,7 +740,15 @@ bool XWingMission::load(XWingMission *m, const char *data) return false; } - switch (oj->object_formation) { + if (oj->object_formation & 0x4) { + noj->objectGoal = XWMObjectGoal::ojg_Destroyed; + } else if (oj->object_formation & 0x8) { + noj->objectGoal = XWMObjectGoal::ojg_Survive; + } else { + noj->objectGoal = XWMObjectGoal::ojg_Neither; + } + + switch (oj->object_formation & ~(0x4 | 0x8)) { case 0: noj->formation = XWMObjectFormation::ojf_FloorXY; break; @@ -754,20 +765,6 @@ bool XWingMission::load(XWingMission *m, const char *data) return false; } - switch (oj->object_goal) { - case 0: - noj->objectGoal = XWMObjectGoal::ojg_Neither; - break; - case 1: - noj->objectGoal = XWMObjectGoal::ojg_Destroyed; - break; - case 2: - noj->objectGoal = XWMObjectGoal::ojg_Survive; - break; - default: - return false; - } - noj->numberOfObjects = oj->number_of_objects; noj->object_x = oj->object_x / 160.0f; diff --git a/code/mission/import/xwinglib.h b/code/mission/import/xwinglib.h index b7299c70c68..76f15840b58 100644 --- a/code/mission/import/xwinglib.h +++ b/code/mission/import/xwinglib.h @@ -195,9 +195,9 @@ enum class XWMObjectFormation : short enum class XWMObjectGoal : short { - ojg_Survive, + ojg_Neither = 0, ojg_Destroyed, - ojg_Neither + ojg_Survive }; class XWMFlightGroup From 92d8ddc2596e3633a0ddb7678a3ffca496396da5 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Sun, 22 Oct 2023 15:35:23 +0100 Subject: [PATCH 024/466] Update xwinglib.cpp in prep for trainingplatforms Object formation is split between training platforms and other objects. This avoids redundant processing of formations for traning platforms finding formations which they shouldn't have. A comment TODO also prepares for later evaluting the object_formation to determine gun placements and timing countdown for training platforms. --- code/mission/import/xwinglib.cpp | 49 ++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/code/mission/import/xwinglib.cpp b/code/mission/import/xwinglib.cpp index e8734bbc4da..5d4e9f91c6d 100644 --- a/code/mission/import/xwinglib.cpp +++ b/code/mission/import/xwinglib.cpp @@ -740,29 +740,36 @@ bool XWingMission::load(XWingMission *m, const char *data) return false; } - if (oj->object_formation & 0x4) { + if (oj->object_formation >= 58) { + noj->objectGoal = XWMObjectGoal::ojg_Neither; + noj->formation = XWMObjectFormation::ojf_FloorXY; + // TODO : If the object is a Training Platform then the object_formation determines + // which guns are present and also how many seconds on the clock for the missions. + } else { + if (oj->object_formation & 0x4) { noj->objectGoal = XWMObjectGoal::ojg_Destroyed; - } else if (oj->object_formation & 0x8) { + } else if (oj->object_formation & 0x8) { noj->objectGoal = XWMObjectGoal::ojg_Survive; - } else { - noj->objectGoal = XWMObjectGoal::ojg_Neither; - } - - switch (oj->object_formation & ~(0x4 | 0x8)) { - case 0: - noj->formation = XWMObjectFormation::ojf_FloorXY; - break; - case 1: - noj->formation = XWMObjectFormation::ojf_SideYZ; - break; - case 2: - noj->formation = XWMObjectFormation::ojf_FrontXZ; - break; - case 3: - noj->formation = XWMObjectFormation::ojf_Scattered; - break; - default: - return false; + } else { + noj->objectGoal = XWMObjectGoal::ojg_Neither; + } + + switch (oj->object_formation & ~(0x4 | 0x8)) { + case 0: + noj->formation = XWMObjectFormation::ojf_FloorXY; + break; + case 1: + noj->formation = XWMObjectFormation::ojf_SideYZ; + break; + case 2: + noj->formation = XWMObjectFormation::ojf_FrontXZ; + break; + case 3: + noj->formation = XWMObjectFormation::ojf_Scattered; + break; + default: + return false; + } } noj->numberOfObjects = oj->number_of_objects; From 584af23b5efce1719b410d4fc232f4ce30047324 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:03:03 +0100 Subject: [PATCH 025/466] Missionparse work for objects Very much a work in progress. I have tried to comment as much as possible to make my thinking clearer. When the asteroids are tabled in FotG we can point the cases to them. The closest mine is currently the ION mine but the weapon can be changed in FRED. As for the backgrounds. It would probably be best to not process these at all from the game and perhaps at a later stage, add FotG backgrounds depending on where the location of the mission is - from the briefing. For the mine field, because it is in a 2d plane with each mine having coordinates (a,b) I felt the best way was to have two for loops... the first would move one step along one axis, then the inner for loop would populate the mines along the perpendicular axis. The grid is populated starting on the initial mine as the origin however in XWing the mine is in the centre of the grid. Therefore the second part of the calculation is to shift the mine back so centre the grid on the given position. This second part of the calculation could be done before the for loops which means it would only have to be done once and would also help make the code clearer. --- code/mission/import/xwinglib.cpp | 2 +- code/mission/import/xwinglib.h | 2 +- code/mission/import/xwingmissionparse.cpp | 225 ++++++++++++++++++++++ 3 files changed, 227 insertions(+), 2 deletions(-) diff --git a/code/mission/import/xwinglib.cpp b/code/mission/import/xwinglib.cpp index 5d4e9f91c6d..b4b8d96c38b 100644 --- a/code/mission/import/xwinglib.cpp +++ b/code/mission/import/xwinglib.cpp @@ -740,7 +740,7 @@ bool XWingMission::load(XWingMission *m, const char *data) return false; } - if (oj->object_formation >= 58) { + if (oj->object_type >= 58) { noj->objectGoal = XWMObjectGoal::ojg_Neither; noj->formation = XWMObjectFormation::ojf_FloorXY; // TODO : If the object is a Training Platform then the object_formation determines diff --git a/code/mission/import/xwinglib.h b/code/mission/import/xwinglib.h index 76f15840b58..f657c5fcf1f 100644 --- a/code/mission/import/xwinglib.h +++ b/code/mission/import/xwinglib.h @@ -171,7 +171,7 @@ enum class XWMObjectType : short oj_Orange_Crescent7, oj_Orange_Crescent8, oj_Death_Star, - oj_Training_Platform1, + oj_Training_Platform1, // = 58; ? oj_Training_Platform2, oj_Training_Platform3, oj_Training_Platform4, diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index e0a07c90138..4a3ab7507cc 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -623,6 +623,231 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh } } +bool space_object = true; // true for objects, false for backgrounds - needs to be checked +const char *xwi_determine_object_type(const XWMObject *oj) +{ + switch (oj->objectType) { + case XWMObjectType::oj_Mine1: + return "Defense_Mine#Ion"; + case XWMObjectType::oj_Mine2: + return "Defense_Mine#Ion"; + case XWMObjectType::oj_Mine3: + return "Defense_Mine#Ion"; + case XWMObjectType::oj_Mine4: + return "Defense_Mine#Ion"; + case XWMObjectType::oj_Satellite: + return "Sensor_Satellite#Imp"; + case XWMObjectType::oj_Nav_Buoy: + return "Nav_Buoy#real"; + case XWMObjectType::oj_Probe: + return "Sensor_Probe"; + case XWMObjectType::oj_Asteroid1: + return " "; + case XWMObjectType::oj_Asteroid2: + return " "; + case XWMObjectType::oj_Asteroid3: + return " "; + case XWMObjectType::oj_Asteroid4: + return " "; + case XWMObjectType::oj_Asteroid5: + return " "; + case XWMObjectType::oj_Asteroid6: + return " "; + case XWMObjectType::oj_Asteroid7: + return " "; + case XWMObjectType::oj_Asteroid8: + return " "; + case XWMObjectType::oj_Rock_World: // should I just remove these cases for the backgrounds? + return "Planet_Bespin"; + case XWMObjectType::oj_Gray_Ring_World: + return "Planet_Gas"; + case XWMObjectType::oj_Gray_World: + return "Planet_Moon01"; + case XWMObjectType::oj_Brown_World: + return "Planet_HosnianPrime"; + case XWMObjectType::oj_Gray_World2: + return "Planet_Moon02"; + case XWMObjectType::oj_Planet_and_Moon: + return "Planet_Generic02"; + case XWMObjectType::oj_Gray_Crescent: + return "Planet_Ice"; + case XWMObjectType::oj_Orange_Crescent1: + return "Planet_Ryloth"; + case XWMObjectType::oj_Orange_Crescent2: + return "Planet_Swamp"; + case XWMObjectType::oj_Orange_Crescent3: + return "Planet_Desert"; + case XWMObjectType::oj_Orange_Crescent4: + return "Planet_Mud"; + case XWMObjectType::oj_Orange_Crescent5: + return "Planet_Utapau"; + case XWMObjectType::oj_Orange_Crescent6: + return "Planet_RhenVar"; + case XWMObjectType::oj_Orange_Crescent7: + return "Planet_Malastare"; + case XWMObjectType::oj_Orange_Crescent8: + return "Planet_Generic02"; + case XWMObjectType::oj_Death_Star: + return "Planet_Moon02"; + case XWMObjectType::oj_Training_Platform1: + return nullptr; + case XWMObjectType::oj_Training_Platform2: + return nullptr; + case XWMObjectType::oj_Training_Platform3: + return nullptr; + case XWMObjectType::oj_Training_Platform4: + return nullptr; + case XWMObjectType::oj_Training_Platform5: + return nullptr; + case XWMObjectType::oj_Training_Platform6: + return nullptr; + case XWMObjectType::oj_Training_Platform7: + return nullptr; + case XWMObjectType::oj_Training_Platform8: + return nullptr; + case XWMObjectType::oj_Training_Platform9: + return nullptr; + case XWMObjectType::oj_Training_Platform10: + return nullptr; + case XWMObjectType::oj_Training_Platform11: + return nullptr; + case XWMObjectType::oj_Training_Platform12: + return nullptr; + default: + break; + } + return nullptr; +} + +vec3d xwi_determine_mine_formation_position(const XWMObject* oj, float objectPosX, float objectPosY, float objectPosZ, + float objectPosA, float objectPosB) +{ + switch (oj->formation) { // Y and Z axes must be switched for FSO + case XWMObjectFormation::ojf_FloorXY: + return vm_vec_new((objectPosX + objectPosA), objectPosZ, (objectPosY + objectPosB)); + case XWMObjectFormation::ojf_SideYZ: + return vm_vec_new(objectPosX, (objectPosZ + objectPosB), (objectPosY + objectPosA)); + case XWMObjectFormation::ojf_FrontXZ: + return vm_vec_new((objectPosX + objectPosA), (objectPosZ + objectPosB), objectPosY); + case XWMObjectFormation::ojf_Scattered: + return vm_vec_new(objectPosX, objectPosZ, objectPosY); + default: + break; + } + return vm_vec_new(objectPosX, objectPosZ, objectPosY); +} + +void xwi_determine_object_pbh(matrix* orient, const XWMObject* oj) +{ + angles a; + a.p = oj->object_pitch; + a.b = oj->object_roll; + a.h = oj->object_yaw; + vm_angles_2_matrix(orient, &a); + return; +} + + +void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObject* oj) +{ + if (space_object) { // For parsing objects not backgrounds + + SCP_UNUSED(pm); + + int number_of_objects = oj->numberOfObjects; + /** + NumberOfCraft Holds various parameter values depending on object types.For mines, + this determines how wide the minefield is.Minefields are always a square formation, + centered over their starting point.For example, + a value of 3 will create a 3x3 grid of mines.**/ + + SCP_string object_type = xwi_determine_object_type(oj); + + // object position and orientation + // NOTE: Y and Z are swapped + + float objectPosX = oj->object_x; + float objectPosY = oj->object_y; + float objectPosZ = oj->object_z; + + float objectPosA; // These are the two planes of the 2d minefield grid + float objectPosB; + + matrix orient; + xwi_determine_object_pbh(&orient, oj); + p_object pobj; + + // now configure each object in the group (mines multiple) + + std::string suffix; // for naming + int mine_dist = 100; // change this to change the distance between the mines + for (int i = 0; i < number_of_objects; i++) { // make an a-b 2d grid from the mines along one plane a + objectPosA += (mine_dist * i) - (mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) + for (int m = 0; m < number_of_objects; m++) { // for each increment along the a plane, add mines along b plane + objectPosB += (mine_dist * m) - (mine_dist / 2 * (number_of_objects - 1)); + + auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, objectPosA, objectPosB); + vm_vec_scale(&ojxyz, 1000); // units are in kilometers (after processing by xwinglib which handles the + // factor of 160), so scale them up + + suffix = object_type.c_str() + (i * m); // ok so not sure how to name them... add suffix? + strcpy_s(pobj.name, suffix.c_str()); // add a suffix to the name...?? + SCP_totitle(pobj.name); + + pobj.orient = orient; + pobj.pos = ojxyz; + + /** Not sure if all these fields are needed? + pobj.ship_class = 1; + + { + pobj.ship_class = 0; + } + for (int wing_index = 0; wing_index < number_in_wave; wing_index++) { + p_object pobj; + + if (wingp) { + wing_bash_ship_name(pobj.name, wingp->name, wing_index + 1, nullptr); + pobj.wingnum = wingnum; + pobj.pos_in_wing = wing_index; + pobj.arrival_cue = Locked_sexp_false; + } else { + strcpy_s(pobj.name, fg->designation.c_str()); + SCP_totitle(pobj.name); + + // if a ship doesn't have an anchor, make sure it is at-location + // (flight groups present at mission start will have arriveByHyperspace set to false) + if (pobj.arrival_anchor < 0) + pobj.arrival_location = ARRIVE_AT_LOCATION; + if (pobj.departure_anchor < 0) + pobj.departure_location = DEPART_AT_LOCATION; + } + pobj.ai_class = ai_index; + pobj.warpin_params_index = sip->warpin_params_index; + pobj.warpout_params_index = sip->warpout_params_index; + pobj.ship_max_shield_strength = sip->max_shield_strength; + pobj.ship_max_hull_strength = sip->max_hull_strength; + Assert(pobj.ship_max_hull_strength > + 0.0f); // Goober5000: div-0 check (not shield because we might not have one) + pobj.max_shield_recharge = sip->max_shield_recharge; + pobj.replacement_textures = + sip->replacement_textures; // initialize our set with the ship class set, which may be empty + pobj.score = sip->score; + pobj.team = team; + + if (wingp && wing_index == fg->specialShipNumber) + pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); + else + pobj.cargo1 = (char)xwi_lookup_cargo(fg->cargo.c_str()); **/ + + Parse_objects.push_back(pobj); + } + } + } else { + // now sort out the backgrounds and add them in game... or not... + } +} + void parse_xwi_mission(mission *pm, const XWingMission *xwim) { int index = -1; From deafd8da8ab056022ef8e07c48f55559562de874 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Tue, 24 Oct 2023 13:21:28 +0100 Subject: [PATCH 026/466] MissionParse Object update to make 2d minefiled I changed the code for the calculation of the minefiled just to make it a little bit clearer as well as removing some redundancy. The centre of the grid is now calucluated before the minefield is populated rather than before when the whole minefield was shifted afterwards. --- code/mission/import/xwingmissionparse.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 4a3ab7507cc..3b6af08fdb3 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -770,8 +770,10 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec float objectPosY = oj->object_y; float objectPosZ = oj->object_z; - float objectPosA; // These are the two planes of the 2d minefield grid - float objectPosB; + int mine_dist = 100; // change this to change the distance between the mines + /** The minefield is a square 2d along two planes (a,b) centred on the given position **/ + float objectPosA = 0 -(mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) + float objectPosB = 0 -(mine_dist / 2 * (number_of_objects - 1)); matrix orient; xwi_determine_object_pbh(&orient, oj); @@ -780,12 +782,13 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec // now configure each object in the group (mines multiple) std::string suffix; // for naming - int mine_dist = 100; // change this to change the distance between the mines - for (int i = 0; i < number_of_objects; i++) { // make an a-b 2d grid from the mines along one plane a - objectPosA += (mine_dist * i) - (mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) + + for (int i = 0; i < number_of_objects; i++) { // make an a-b 2d grid from the mines + objectPosA += (mine_dist * i); // add a new row to the grid for (int m = 0; m < number_of_objects; m++) { // for each increment along the a plane, add mines along b plane - objectPosB += (mine_dist * m) - (mine_dist / 2 * (number_of_objects - 1)); + objectPosB += (mine_dist * m); // for each new row populate the column + /** Now convert the grid (a,b) to the relavenat formation ie. (x,y) or (z,y) etc **/ auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, objectPosA, objectPosB); vm_vec_scale(&ojxyz, 1000); // units are in kilometers (after processing by xwinglib which handles the // factor of 160), so scale them up From 71a1585721b381c99a64efbdda05319790dbac9d Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 25 Oct 2023 00:52:19 -0400 Subject: [PATCH 027/466] check this arrival enum, just in case --- code/mission/import/xwingmissionparse.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index e0a07c90138..dc82bbcf636 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -110,6 +110,9 @@ int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg else return Locked_sexp_true; + if (fg->arrivalEvent == XWMArrivalEvent::ae_mission_start) + return Locked_sexp_true; + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_arrived) { sprintf(sexp_buf, "( has-arrived-delay 0 \"%s\" )", arrival_fg_name); From 4614d02a58afa4b9870eab7bba52b93d8e49138b Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Thu, 26 Oct 2023 12:43:31 +0100 Subject: [PATCH 028/466] Update enum indices and unique naming of objects Add values to enums in XWMObjectType in xwinglib.h to rectify gaps between various object types. In xwingmissionparse.cpp add code to give unique names to each object and each object in a group using suffixes. Other cleanups. --- code/mission/import/xwinglib.h | 8 +- code/mission/import/xwingmissionparse.cpp | 278 +++++++++++----------- 2 files changed, 146 insertions(+), 140 deletions(-) diff --git a/code/mission/import/xwinglib.h b/code/mission/import/xwinglib.h index f657c5fcf1f..20943d5236a 100644 --- a/code/mission/import/xwinglib.h +++ b/code/mission/import/xwinglib.h @@ -140,14 +140,14 @@ enum class XWMObjective : short enum class XWMObjectType : short { - oj_Mine1, + oj_Mine1 = 0x12, oj_Mine2, oj_Mine3, oj_Mine4, oj_Satellite, oj_Nav_Buoy, oj_Probe, - oj_Asteroid1, + oj_Asteroid1 = 0x1A, oj_Asteroid2, oj_Asteroid3, oj_Asteroid4, @@ -171,7 +171,7 @@ enum class XWMObjectType : short oj_Orange_Crescent7, oj_Orange_Crescent8, oj_Death_Star, - oj_Training_Platform1, // = 58; ? + oj_Training_Platform1 = 0x3A, oj_Training_Platform2, oj_Training_Platform3, oj_Training_Platform4, @@ -187,7 +187,7 @@ enum class XWMObjectType : short enum class XWMObjectFormation : short { - ojf_FloorXY, + ojf_FloorXY = 0, ojf_SideYZ, ojf_FrontXZ, ojf_Scattered // may be buggy - undefined locations diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 3b6af08fdb3..5913e9ee912 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -109,6 +109,9 @@ int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg } else return Locked_sexp_true; + + if (fg->arrivalEvent == XWMArrivalEvent::ae_mission_start) + return Locked_sexp_true; if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_arrived) { @@ -623,72 +626,39 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh } } -bool space_object = true; // true for objects, false for backgrounds - needs to be checked const char *xwi_determine_object_type(const XWMObject *oj) { switch (oj->objectType) { case XWMObjectType::oj_Mine1: - return "Defense_Mine#Ion"; + return "Defense Mine#Ion"; case XWMObjectType::oj_Mine2: - return "Defense_Mine#Ion"; + return "Defense Mine#Ion"; case XWMObjectType::oj_Mine3: - return "Defense_Mine#Ion"; + return "Defense Mine#Ion"; case XWMObjectType::oj_Mine4: - return "Defense_Mine#Ion"; + return "Defense Mine#Ion"; case XWMObjectType::oj_Satellite: - return "Sensor_Satellite#Imp"; + return "Sensor Satellite#Imp"; case XWMObjectType::oj_Nav_Buoy: - return "Nav_Buoy#real"; + return "Nav Buoy#real"; case XWMObjectType::oj_Probe: - return "Sensor_Probe"; + return "Sensor Probe"; case XWMObjectType::oj_Asteroid1: - return " "; + return "Asteroid#Small01"; case XWMObjectType::oj_Asteroid2: - return " "; + return "Asteroid#Small02"; case XWMObjectType::oj_Asteroid3: - return " "; + return "Asteroid#Medium01"; case XWMObjectType::oj_Asteroid4: - return " "; + return "Asteroid#Medium02"; case XWMObjectType::oj_Asteroid5: - return " "; + return "Asteroid#Medium03"; case XWMObjectType::oj_Asteroid6: - return " "; + return "Asteroid#Big01"; case XWMObjectType::oj_Asteroid7: - return " "; + return "Asteroid#Big02"; case XWMObjectType::oj_Asteroid8: - return " "; - case XWMObjectType::oj_Rock_World: // should I just remove these cases for the backgrounds? - return "Planet_Bespin"; - case XWMObjectType::oj_Gray_Ring_World: - return "Planet_Gas"; - case XWMObjectType::oj_Gray_World: - return "Planet_Moon01"; - case XWMObjectType::oj_Brown_World: - return "Planet_HosnianPrime"; - case XWMObjectType::oj_Gray_World2: - return "Planet_Moon02"; - case XWMObjectType::oj_Planet_and_Moon: - return "Planet_Generic02"; - case XWMObjectType::oj_Gray_Crescent: - return "Planet_Ice"; - case XWMObjectType::oj_Orange_Crescent1: - return "Planet_Ryloth"; - case XWMObjectType::oj_Orange_Crescent2: - return "Planet_Swamp"; - case XWMObjectType::oj_Orange_Crescent3: - return "Planet_Desert"; - case XWMObjectType::oj_Orange_Crescent4: - return "Planet_Mud"; - case XWMObjectType::oj_Orange_Crescent5: - return "Planet_Utapau"; - case XWMObjectType::oj_Orange_Crescent6: - return "Planet_RhenVar"; - case XWMObjectType::oj_Orange_Crescent7: - return "Planet_Malastare"; - case XWMObjectType::oj_Orange_Crescent8: - return "Planet_Generic02"; - case XWMObjectType::oj_Death_Star: - return "Planet_Moon02"; + return "Asteroid#Big03"; case XWMObjectType::oj_Training_Platform1: return nullptr; case XWMObjectType::oj_Training_Platform2: @@ -720,15 +690,15 @@ const char *xwi_determine_object_type(const XWMObject *oj) } vec3d xwi_determine_mine_formation_position(const XWMObject* oj, float objectPosX, float objectPosY, float objectPosZ, - float objectPosA, float objectPosB) + float offsetAxisA, float offsetAxisB) { switch (oj->formation) { // Y and Z axes must be switched for FSO case XWMObjectFormation::ojf_FloorXY: - return vm_vec_new((objectPosX + objectPosA), objectPosZ, (objectPosY + objectPosB)); + return vm_vec_new((objectPosX + offsetAxisA), objectPosZ, (objectPosY + offsetAxisB)); case XWMObjectFormation::ojf_SideYZ: - return vm_vec_new(objectPosX, (objectPosZ + objectPosB), (objectPosY + objectPosA)); + return vm_vec_new(objectPosX, (objectPosZ + offsetAxisA), (objectPosY + offsetAxisB)); case XWMObjectFormation::ojf_FrontXZ: - return vm_vec_new((objectPosX + objectPosA), (objectPosZ + objectPosB), objectPosY); + return vm_vec_new((objectPosX + offsetAxisA), (objectPosZ + offsetAxisB), objectPosY); case XWMObjectFormation::ojf_Scattered: return vm_vec_new(objectPosX, objectPosZ, objectPosY); default: @@ -737,7 +707,7 @@ vec3d xwi_determine_mine_formation_position(const XWMObject* oj, float objectPos return vm_vec_new(objectPosX, objectPosZ, objectPosY); } -void xwi_determine_object_pbh(matrix* orient, const XWMObject* oj) +void xwi_determine_object_orient(matrix* orient, const XWMObject* oj) { angles a; a.p = oj->object_pitch; @@ -747,107 +717,143 @@ void xwi_determine_object_pbh(matrix* orient, const XWMObject* oj) return; } - void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObject* oj) { - if (space_object) { // For parsing objects not backgrounds + SCP_UNUSED(pm); + auto object_type = xwi_determine_object_type(oj); + if (object_type == nullptr) + return; - SCP_UNUSED(pm); + int number_of_objects = oj->numberOfObjects; + if (number_of_objects < 1) + return; - int number_of_objects = oj->numberOfObjects; - /** - NumberOfCraft Holds various parameter values depending on object types.For mines, - this determines how wide the minefield is.Minefields are always a square formation, - centered over their starting point.For example, - a value of 3 will create a 3x3 grid of mines.**/ + /** + NumberOfCraft Holds various parameter values depending on object types.For mines, + this determines how wide the minefield is.Minefields are always a square formation, + centered over their starting point.For example, + a value of 3 will create a 3x3 grid of mines.**/ - SCP_string object_type = xwi_determine_object_type(oj); + - // object position and orientation - // NOTE: Y and Z are swapped + // object position and orientation + // NOTE: Y and Z are swapped after all operartions are perfomed - float objectPosX = oj->object_x; - float objectPosY = oj->object_y; - float objectPosZ = oj->object_z; + float objectPosX = oj->object_x; + float objectPosY = oj->object_y; + float objectPosZ = oj->object_z; - int mine_dist = 100; // change this to change the distance between the mines - /** The minefield is a square 2d along two planes (a,b) centred on the given position **/ - float objectPosA = 0 -(mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) - float objectPosB = 0 -(mine_dist / 2 * (number_of_objects - 1)); + int mine_dist = 400; // change this to change the distance between the mines + /** The minefield is a square 2d along two planes (a,b) centred on the given position **/ + float offsetAxisA = 0 -(mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) + float offsetAxisB = 0 -(mine_dist / 2 * (number_of_objects - 1)); - matrix orient; - xwi_determine_object_pbh(&orient, oj); - p_object pobj; + matrix orient; + xwi_determine_object_orient(&orient, oj); + p_object pobj; - // now configure each object in the group (mines multiple) - - std::string suffix; // for naming + // Copy objects in Parse_objects to set for name checking below + // This only needs to be done fully once per object group then can be added to after each new object + SCP_set objectSet; + for (int n = 0; n < Parse_objects.size(); n++) { + objectSet.insert(Parse_objects[n].name); + } - for (int i = 0; i < number_of_objects; i++) { // make an a-b 2d grid from the mines - objectPosA += (mine_dist * i); // add a new row to the grid - for (int m = 0; m < number_of_objects; m++) { // for each increment along the a plane, add mines along b plane - objectPosB += (mine_dist * m); // for each new row populate the column - - /** Now convert the grid (a,b) to the relavenat formation ie. (x,y) or (z,y) etc **/ - auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, objectPosA, objectPosB); - vm_vec_scale(&ojxyz, 1000); // units are in kilometers (after processing by xwinglib which handles the - // factor of 160), so scale them up + // now being to configure each object in the group (mines multiple) - suffix = object_type.c_str() + (i * m); // ok so not sure how to name them... add suffix? - strcpy_s(pobj.name, suffix.c_str()); // add a suffix to the name...?? - SCP_totitle(pobj.name); + for (int a = 0; a < number_of_objects; a++) { // make an a-b 2d grid from the mines + offsetAxisA += (mine_dist * a); // add a new row to the grid + for (int b = 0; b < number_of_objects; b++) { // for each increment along the a plane, add mines along b plane + offsetAxisB += (mine_dist * b); // for each new row populate the column + + /** Now convert the grid (a,b) to the relavenat formation ie. (x,y) or (z,y) etc **/ + auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, offsetAxisA, offsetAxisB); + vm_vec_scale(&ojxyz, 1000); // units are in kilometers (after processing by xwinglib which handles the + // factor of 160), so scale them up - pobj.orient = orient; - pobj.pos = ojxyz; + pobj.orient = orient; + pobj.pos = ojxyz; - /** Not sure if all these fields are needed? - pobj.ship_class = 1; + // regular name, regular suffix - { - pobj.ship_class = 0; - } - for (int wing_index = 0; wing_index < number_in_wave; wing_index++) { - p_object pobj; - - if (wingp) { - wing_bash_ship_name(pobj.name, wingp->name, wing_index + 1, nullptr); - pobj.wingnum = wingnum; - pobj.pos_in_wing = wing_index; - pobj.arrival_cue = Locked_sexp_false; - } else { - strcpy_s(pobj.name, fg->designation.c_str()); - SCP_totitle(pobj.name); - - // if a ship doesn't have an anchor, make sure it is at-location - // (flight groups present at mission start will have arriveByHyperspace set to false) - if (pobj.arrival_anchor < 0) - pobj.arrival_location = ARRIVE_AT_LOCATION; - if (pobj.departure_anchor < 0) - pobj.departure_location = DEPART_AT_LOCATION; - } - pobj.ai_class = ai_index; - pobj.warpin_params_index = sip->warpin_params_index; - pobj.warpout_params_index = sip->warpout_params_index; - pobj.ship_max_shield_strength = sip->max_shield_strength; - pobj.ship_max_hull_strength = sip->max_hull_strength; - Assert(pobj.ship_max_hull_strength > - 0.0f); // Goober5000: div-0 check (not shield because we might not have one) - pobj.max_shield_recharge = sip->max_shield_recharge; - pobj.replacement_textures = - sip->replacement_textures; // initialize our set with the ship class set, which may be empty - pobj.score = sip->score; - pobj.team = team; - - if (wingp && wing_index == fg->specialShipNumber) - pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); - else - pobj.cargo1 = (char)xwi_lookup_cargo(fg->cargo.c_str()); **/ + char base_name[NAME_LENGTH]; + char suffix[NAME_LENGTH]; + char *newArray = new char[std::strlen(base_name) + std::strlen(suffix) + 1]; + strcpy_s(base_name, object_type); - Parse_objects.push_back(pobj); + end_string_at_first_hash_symbol(base_name); + + // find lowest unique n for the suffix + int n = 1; + sprintf(suffix, NOX(" %d"), n); + + // objects don't have designations so they are limited to the name from the object_type and suffix + // names will be hidden in game so to avoid iterating multiple times, allow for suffix 999 (+4) + int char_overflow = static_cast(strlen(base_name) + 4) - (NAME_LENGTH - 1); + if (char_overflow > 0) { + base_name[strlen(base_name) - static_cast(char_overflow)] = '\0'; + } + char croppedName[NAME_LENGTH]; + strcpy_s(croppedName, base_name); // save base_name incase suffix needs changed + + strcat_s(croppedName, suffix); + // Now check the name against the objectSet of names + auto fin = objectSet.end(); + while (objectSet.find(croppedName) != fin) { + n++; + sprintf(suffix, NOX(" %d"), n); + strcpy_s(croppedName, base_name); + strcat_s(croppedName, suffix); } + + strcpy_s(pobj.name, croppedName); + + objectSet.insert(pobj.name); // add the new name to the objectSet + SCP_totitle(pobj.name); + Parse_objects.push_back(pobj); + + /** Is any of this needed below? + + pobj.arrival_cue = arrival_cue; + pobj.arrival_delay = fg->arrivalDelay; + pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; + pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); + pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; + pobj.departure_anchor = pobj.arrival_anchor; + + // if a ship doesn't have an anchor, make sure it is at-location + // (flight groups present at mission start will have arriveByHyperspace set to false) + if (pobj.arrival_anchor < 0) + pobj.arrival_location = ARRIVE_AT_LOCATION; + if (pobj.departure_anchor < 0) + pobj.departure_location = DEPART_AT_LOCATION; + } + + // initialize class-specific fields + pobj.ai_class = ai_index; + pobj.warpin_params_index = sip->warpin_params_index; + pobj.warpout_params_index = sip->warpout_params_index; + pobj.ship_max_shield_strength = sip->max_shield_strength; + pobj.ship_max_hull_strength = sip->max_hull_strength; + Assert(pobj.ship_max_hull_strength > + 0.0f); // Goober5000: div-0 check (not shield because we might not have one) + pobj.max_shield_recharge = sip->max_shield_recharge; + pobj.replacement_textures = + sip->replacement_textures; // initialize our set with the ship class set, which may be empty + pobj.score = sip->score; + + pobj.team = team; + + pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); + else + pobj.cargo1 = (char)xwi_lookup_cargo(fg->cargo.c_str()); + + if (fg->craftOrder != XWMCraftOrder::o_Hold_Steady && + fg->craftOrder != XWMCraftOrder::o_Starship_Sit_And_Fire) + pobj.initial_velocity = 0; **/ + + } - } else { - // now sort out the backgrounds and add them in game... or not... } } From fe98663af51c94def55d77d4c91c8fe494848335 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Fri, 27 Oct 2023 21:20:46 +0100 Subject: [PATCH 029/466] Separate the function to check for unique suffix For the unique name check for the object, put this into a separate function. Update missionparse to add default values for the object initialisation. Add a goto for now in case the search for a unique name gets stuck in an infinte loop. This can potentially be removed in a later PR. --- code/mission/import/xwingmissionparse.cpp | 181 ++++++++++------------ 1 file changed, 86 insertions(+), 95 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 5913e9ee912..9f5e6f2a5e5 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -109,9 +109,6 @@ int xwi_determine_arrival_cue(const XWingMission *xwim, const XWMFlightGroup *fg } else return Locked_sexp_true; - - if (fg->arrivalEvent == XWMArrivalEvent::ae_mission_start) - return Locked_sexp_true; if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_arrived) { @@ -659,30 +656,6 @@ const char *xwi_determine_object_type(const XWMObject *oj) return "Asteroid#Big02"; case XWMObjectType::oj_Asteroid8: return "Asteroid#Big03"; - case XWMObjectType::oj_Training_Platform1: - return nullptr; - case XWMObjectType::oj_Training_Platform2: - return nullptr; - case XWMObjectType::oj_Training_Platform3: - return nullptr; - case XWMObjectType::oj_Training_Platform4: - return nullptr; - case XWMObjectType::oj_Training_Platform5: - return nullptr; - case XWMObjectType::oj_Training_Platform6: - return nullptr; - case XWMObjectType::oj_Training_Platform7: - return nullptr; - case XWMObjectType::oj_Training_Platform8: - return nullptr; - case XWMObjectType::oj_Training_Platform9: - return nullptr; - case XWMObjectType::oj_Training_Platform10: - return nullptr; - case XWMObjectType::oj_Training_Platform11: - return nullptr; - case XWMObjectType::oj_Training_Platform12: - return nullptr; default: break; } @@ -717,6 +690,24 @@ void xwi_determine_object_orient(matrix* orient, const XWMObject* oj) return; } +// find lowest unique n for the suffix should return true when a unique name is found else false +bool unique_object_suffix(SCP_set objectNameSet, char base_name[NAME_LENGTH], char suffix[NAME_LENGTH]) +{ + + // Check that the name including suffix is not longer than the allowed NAME_LENGTH + int char_overflow = static_cast(strlen(base_name) + strlen(suffix)) - (NAME_LENGTH - 1); + if (char_overflow > 0) { + base_name[strlen(base_name) - static_cast(char_overflow)] = '\0'; + } + strcat(base_name, suffix); + + // Now check the name against the objectSet of names + if (objectNameSet.count(base_name)) { + return true; + } else + return false; +} + void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObject* oj) { SCP_UNUSED(pm); @@ -728,6 +719,27 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec if (number_of_objects < 1) return; + if (number_of_objects > 1) { + switch (oj->objectType) { + case XWMObjectType::oj_Mine1: + case XWMObjectType::oj_Mine2: + case XWMObjectType::oj_Mine3: + case XWMObjectType::oj_Mine4: + break; + default: + number_of_objects = 1; + Warning(LOCATION, "There should only be NumberOfCraft of %s", object_type); + break; + } + } + + int ship_class = ship_info_lookup(object_type); + if (ship_class < 0) { + Warning(LOCATION, "Unable to determine ship class for Object Group with type %s", object_type); + ship_class = 0; + } + auto sip = &Ship_info[ship_class]; + /** NumberOfCraft Holds various parameter values depending on object types.For mines, this determines how wide the minefield is.Minefields are always a square formation, @@ -750,16 +762,15 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec matrix orient; xwi_determine_object_orient(&orient, oj); - p_object pobj; // Copy objects in Parse_objects to set for name checking below // This only needs to be done fully once per object group then can be added to after each new object - SCP_set objectSet; + SCP_set objectNameSet; for (int n = 0; n < Parse_objects.size(); n++) { - objectSet.insert(Parse_objects[n].name); + objectNameSet.insert(Parse_objects[n].name); } - // now being to configure each object in the group (mines multiple) + // now begin to configure each object in the group (mines multiple) for (int a = 0; a < number_of_objects; a++) { // make an a-b 2d grid from the mines offsetAxisA += (mine_dist * a); // add a new row to the grid @@ -771,91 +782,71 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec vm_vec_scale(&ojxyz, 1000); // units are in kilometers (after processing by xwinglib which handles the // factor of 160), so scale them up + p_object pobj; + SCP_totitle(pobj.name); pobj.orient = orient; pobj.pos = ojxyz; + + int team = 0; //default civilian/team none + if (number_of_objects > 1) + team = 2; //mines are always hostile + + int ship_class = ship_info_lookup(object_type); + if (ship_class < 0) { + Warning(LOCATION, "Unable to determine ship class for Object Group with type %s", object_type); + ship_class = 0; + } + auto sip = &Ship_info[ship_class]; // regular name, regular suffix - char base_name[NAME_LENGTH]; char suffix[NAME_LENGTH]; - char *newArray = new char[std::strlen(base_name) + std::strlen(suffix) + 1]; strcpy_s(base_name, object_type); end_string_at_first_hash_symbol(base_name); - // find lowest unique n for the suffix int n = 1; + char suffix[NAME_LENGTH]; sprintf(suffix, NOX(" %d"), n); - // objects don't have designations so they are limited to the name from the object_type and suffix - // names will be hidden in game so to avoid iterating multiple times, allow for suffix 999 (+4) - int char_overflow = static_cast(strlen(base_name) + 4) - (NAME_LENGTH - 1); - if (char_overflow > 0) { - base_name[strlen(base_name) - static_cast(char_overflow)] = '\0'; - } - char croppedName[NAME_LENGTH]; - strcpy_s(croppedName, base_name); // save base_name incase suffix needs changed - - strcat_s(croppedName, suffix); - // Now check the name against the objectSet of names - auto fin = objectSet.end(); - while (objectSet.find(croppedName) != fin) { + // Keep searching through the names until a unique one is discovered + while (!unique_object_suffix(objectNameSet, base_name, suffix)) + { n++; sprintf(suffix, NOX(" %d"), n); - strcpy_s(croppedName, base_name); - strcat_s(croppedName, suffix); + if (n > objectNameSet.size()) { // if Size = all the elements + null then a name should be found + goto noNameError; + } } - - strcpy_s(pobj.name, croppedName); - objectSet.insert(pobj.name); // add the new name to the objectSet - SCP_totitle(pobj.name); - Parse_objects.push_back(pobj); - - /** Is any of this needed below? - - pobj.arrival_cue = arrival_cue; - pobj.arrival_delay = fg->arrivalDelay; - pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; - pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); - pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; - pobj.departure_anchor = pobj.arrival_anchor; - - // if a ship doesn't have an anchor, make sure it is at-location - // (flight groups present at mission start will have arriveByHyperspace set to false) - if (pobj.arrival_anchor < 0) - pobj.arrival_location = ARRIVE_AT_LOCATION; - if (pobj.departure_anchor < 0) - pobj.departure_location = DEPART_AT_LOCATION; - } - - // initialize class-specific fields - pobj.ai_class = ai_index; - pobj.warpin_params_index = sip->warpin_params_index; - pobj.warpout_params_index = sip->warpout_params_index; - pobj.ship_max_shield_strength = sip->max_shield_strength; - pobj.ship_max_hull_strength = sip->max_hull_strength; - Assert(pobj.ship_max_hull_strength > - 0.0f); // Goober5000: div-0 check (not shield because we might not have one) - pobj.max_shield_recharge = sip->max_shield_recharge; - pobj.replacement_textures = - sip->replacement_textures; // initialize our set with the ship class set, which may be empty - pobj.score = sip->score; - - pobj.team = team; - - pobj.cargo1 = (char)xwi_lookup_cargo(fg->specialCargo.c_str()); - else - pobj.cargo1 = (char)xwi_lookup_cargo(fg->cargo.c_str()); - - if (fg->craftOrder != XWMCraftOrder::o_Hold_Steady && - fg->craftOrder != XWMCraftOrder::o_Starship_Sit_And_Fire) - pobj.initial_velocity = 0; **/ + strcpy_s(pobj.name, base_name); + pobj.arrival_cue = Locked_sexp_true; + pobj.arrival_location = ARRIVE_AT_LOCATION; + pobj.departure_cue = Locked_sexp_false; + pobj.departure_location = DEPART_AT_LOCATION; + pobj.ai_class = sip->ai_class; + pobj.warpin_params_index = sip->warpin_params_index; + pobj.warpout_params_index = sip->warpout_params_index; + pobj.ship_max_shield_strength = sip->max_shield_strength; + pobj.ship_max_hull_strength = sip->max_hull_strength; + Assert(pobj.ship_max_hull_strength > 0.0f); // Goober5000: div-0 check (not shield because we might not have one) + pobj.max_shield_recharge = sip->max_shield_recharge; + pobj.replacement_textures = sip->replacement_textures; // initialize our set with the ship class set, which may be empty + pobj.score = sip->score; + + pobj.team = team; + pobj.initial_velocity = 0; + + objectNameSet.insert(pobj.name); // add the new name to the objectSet + Parse_objects.push_back(pobj); } } -} +noNameError: +Warning(LOCATION, "Unable to find a suffix for %s", object_type); +return; +} void parse_xwi_mission(mission *pm, const XWingMission *xwim) { From 051997cd28f38b6a5b08975f223884b9d4414637 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Sun, 29 Oct 2023 00:04:59 +0100 Subject: [PATCH 030/466] Update to object naming function and cleanup Move entirety of the object naming to it's own function XWI_determine_space_object_name. Cleanup the XWI_determine_object_type to remove redundant case returns. Four cases for the mine have now been grouped to have a single return. --- code/mission/import/xwingmissionparse.cpp | 117 +++++++++------------- 1 file changed, 47 insertions(+), 70 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 9f5e6f2a5e5..b07393f4bfc 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -627,11 +627,8 @@ const char *xwi_determine_object_type(const XWMObject *oj) { switch (oj->objectType) { case XWMObjectType::oj_Mine1: - return "Defense Mine#Ion"; case XWMObjectType::oj_Mine2: - return "Defense Mine#Ion"; case XWMObjectType::oj_Mine3: - return "Defense Mine#Ion"; case XWMObjectType::oj_Mine4: return "Defense Mine#Ion"; case XWMObjectType::oj_Satellite: @@ -691,21 +688,38 @@ void xwi_determine_object_orient(matrix* orient, const XWMObject* oj) } // find lowest unique n for the suffix should return true when a unique name is found else false -bool unique_object_suffix(SCP_set objectNameSet, char base_name[NAME_LENGTH], char suffix[NAME_LENGTH]) +const char* xwi_determine_space_object_name(SCP_set& objectNameSet, const char* object_type) { - - // Check that the name including suffix is not longer than the allowed NAME_LENGTH - int char_overflow = static_cast(strlen(base_name) + strlen(suffix)) - (NAME_LENGTH - 1); - if (char_overflow > 0) { - base_name[strlen(base_name) - static_cast(char_overflow)] = '\0'; - } - strcat(base_name, suffix); - - // Now check the name against the objectSet of names - if (objectNameSet.count(base_name)) { - return true; - } else - return false; + char base_name[NAME_LENGTH]; + char suffix[NAME_LENGTH]; + strcpy_s(base_name, object_type); + end_string_at_first_hash_symbol(base_name); + + // we'll need to try suffixes starting at 1 and going until we find a unique name + int n = 1; + char object_name[NAME_LENGTH]; + do { + sprintf(suffix, NOX(" %d"), n++); + + // start building name + strcpy_s(object_name, base_name); + + // if generated name will be longer than allowable name, truncate the class section of the name by the overflow + int char_overflow = static_cast(strlen(base_name) + strlen(suffix)) - (NAME_LENGTH - 1); + if (char_overflow > 0) { + object_name[strlen(base_name) - static_cast(char_overflow)] = '\0'; + } + + // complete building the name by adding suffix number and converting case + strcat_s(object_name, suffix); + SCP_totitle(object_name); + + // continue as long as we find the name in our set + } while (objectNameSet.find(object_name) != objectNameSet.end()); + + // name does not yet exist in the set, so it's valid; add it and return + auto iter = objectNameSet.insert(object_name); + return iter.first->c_str(); } void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObject* oj) @@ -727,8 +741,8 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec case XWMObjectType::oj_Mine4: break; default: + Warning(LOCATION, "NumberOfCraft of %s", object_type, " was %d", number_of_objects, " but must be 1."); number_of_objects = 1; - Warning(LOCATION, "There should only be NumberOfCraft of %s", object_type); break; } } @@ -740,20 +754,12 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec } auto sip = &Ship_info[ship_class]; - /** - NumberOfCraft Holds various parameter values depending on object types.For mines, - this determines how wide the minefield is.Minefields are always a square formation, - centered over their starting point.For example, - a value of 3 will create a 3x3 grid of mines.**/ - - - // object position and orientation // NOTE: Y and Z are swapped after all operartions are perfomed - - float objectPosX = oj->object_x; - float objectPosY = oj->object_y; - float objectPosZ = oj->object_z; + // units are in kilometers (after processing by xwinglib which handles the factor of 160), so scale them up + float objectPosX = oj->object_x*1000; + float objectPosY = oj->object_y*1000; + float objectPosZ = oj->object_z*1000; int mine_dist = 400; // change this to change the distance between the mines /** The minefield is a square 2d along two planes (a,b) centred on the given position **/ @@ -775,51 +781,26 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec for (int a = 0; a < number_of_objects; a++) { // make an a-b 2d grid from the mines offsetAxisA += (mine_dist * a); // add a new row to the grid for (int b = 0; b < number_of_objects; b++) { // for each increment along the a plane, add mines along b plane - offsetAxisB += (mine_dist * b); // for each new row populate the column - + offsetAxisB += (mine_dist * b); // for each new row populate the column + /** Now convert the grid (a,b) to the relavenat formation ie. (x,y) or (z,y) etc **/ auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, offsetAxisA, offsetAxisB); - vm_vec_scale(&ojxyz, 1000); // units are in kilometers (after processing by xwinglib which handles the - // factor of 160), so scale them up p_object pobj; - SCP_totitle(pobj.name); + strcpy_s(pobj.name, xwi_determine_space_object_name(objectNameSet, object_type)); pobj.orient = orient; pobj.pos = ojxyz; - int team = 0; //default civilian/team none - if (number_of_objects > 1) - team = 2; //mines are always hostile - - int ship_class = ship_info_lookup(object_type); - if (ship_class < 0) { - Warning(LOCATION, "Unable to determine ship class for Object Group with type %s", object_type); - ship_class = 0; - } - auto sip = &Ship_info[ship_class]; - - // regular name, regular suffix - char base_name[NAME_LENGTH]; - char suffix[NAME_LENGTH]; - strcpy_s(base_name, object_type); - - end_string_at_first_hash_symbol(base_name); + int team = Species_info[sip->species].default_iff; - int n = 1; - char suffix[NAME_LENGTH]; - sprintf(suffix, NOX(" %d"), n); - - // Keep searching through the names until a unique one is discovered - while (!unique_object_suffix(objectNameSet, base_name, suffix)) - { - n++; - sprintf(suffix, NOX(" %d"), n); - if (n > objectNameSet.size()) { // if Size = all the elements + null then a name should be found - goto noNameError; - } + if (number_of_objects > 1) { + auto team_name = "Hostile"; + int index = iff_lookup(team_name); + if (index >= 0) + team = index; + else + Warning(LOCATION, "Could not find iff %s", team_name); } - - strcpy_s(pobj.name, base_name); pobj.arrival_cue = Locked_sexp_true; pobj.arrival_location = ARRIVE_AT_LOCATION; @@ -839,13 +820,9 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec pobj.team = team; pobj.initial_velocity = 0; - objectNameSet.insert(pobj.name); // add the new name to the objectSet Parse_objects.push_back(pobj); } } -noNameError: -Warning(LOCATION, "Unable to find a suffix for %s", object_type); -return; } void parse_xwi_mission(mission *pm, const XWingMission *xwim) From fbcb8c45adde2547cd38192b1dd7e7fcede42811 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Wed, 1 Nov 2023 00:52:21 +0000 Subject: [PATCH 031/466] Update mine weapon subsustem and other fixes Update the mine weapon subsystem from ion to laser since the default in FotG is ion as well as other code fixes and some cleanup. --- code/mission/import/xwingmissionparse.cpp | 68 ++++++++++++++++++----- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index b07393f4bfc..e2a9918aaef 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -7,6 +7,7 @@ #include "ship/ship.h" #include "species_defs/species_defs.h" #include "starfield/starfield.h" +#include "weapon/weapon.h" #include "xwingbrflib.h" #include "xwinglib.h" @@ -687,7 +688,8 @@ void xwi_determine_object_orient(matrix* orient, const XWMObject* oj) return; } -// find lowest unique n for the suffix should return true when a unique name is found else false +// Determine the unique name from the object comprised of the object type and suffix. +// Add the new name to the objectNameSet and return it to parse_xwi_objectgroup const char* xwi_determine_space_object_name(SCP_set& objectNameSet, const char* object_type) { char base_name[NAME_LENGTH]; @@ -741,7 +743,8 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec case XWMObjectType::oj_Mine4: break; default: - Warning(LOCATION, "NumberOfCraft of %s", object_type, " was %d", number_of_objects, " but must be 1."); + Warning(LOCATION, + "NumberOfCraft of '%s' was %d but must be 1.", object_type, number_of_objects); number_of_objects = 1; break; } @@ -769,10 +772,25 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec matrix orient; xwi_determine_object_orient(&orient, oj); + int team = Species_info[sip->species].default_iff; + + switch (oj->objectType) { + case XWMObjectType::oj_Mine1: + case XWMObjectType::oj_Mine2: + case XWMObjectType::oj_Mine3: + case XWMObjectType::oj_Mine4: + auto team_name = "Hostile"; + int index = iff_lookup(team_name); + if (index >= 0) + team = index; + else + Warning(LOCATION, "Could not find iff %s", team_name); + } + // Copy objects in Parse_objects to set for name checking below // This only needs to be done fully once per object group then can be added to after each new object SCP_set objectNameSet; - for (int n = 0; n < Parse_objects.size(); n++) { + for (int n = 0; n < (int)Parse_objects.size(); n++) { objectNameSet.insert(Parse_objects[n].name); } @@ -786,21 +804,13 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec /** Now convert the grid (a,b) to the relavenat formation ie. (x,y) or (z,y) etc **/ auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, offsetAxisA, offsetAxisB); + // Defense Mine#Ion needs to have it's weapon changed from ion to laser + int mine_laser_index = weapon_info_lookup("T&B KX-5#imp"); + p_object pobj; strcpy_s(pobj.name, xwi_determine_space_object_name(objectNameSet, object_type)); pobj.orient = orient; pobj.pos = ojxyz; - - int team = Species_info[sip->species].default_iff; - - if (number_of_objects > 1) { - auto team_name = "Hostile"; - int index = iff_lookup(team_name); - if (index >= 0) - team = index; - else - Warning(LOCATION, "Could not find iff %s", team_name); - } pobj.arrival_cue = Locked_sexp_true; pobj.arrival_location = ARRIVE_AT_LOCATION; @@ -814,6 +824,36 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec pobj.ship_max_hull_strength = sip->max_hull_strength; Assert(pobj.ship_max_hull_strength > 0.0f); // Goober5000: div-0 check (not shield because we might not have one) pobj.max_shield_recharge = sip->max_shield_recharge; + + switch (oj->objectType) { + case XWMObjectType::oj_Mine1: + case XWMObjectType::oj_Mine2: + case XWMObjectType::oj_Mine3: + case XWMObjectType::oj_Mine4: + pobj.subsys_index = Subsys_index; + int this_subsys = allocate_subsys_status(); + pobj.subsys_count++; + strcpy_s(Subsys_status[this_subsys].name, NOX("Pilot")); + + for (int n = 0; n < sip->n_subsystems; n++) { + auto subsys = &sip->subsystems[n]; + if (subsys->type == SUBSYSTEM_TURRET) { + this_subsys = allocate_subsys_status(); + pobj.subsys_count++; + strcpy_s(Subsys_status[this_subsys].name, sip->subsystems[n].name); + + for (int bank = 0; bank < MAX_SHIP_PRIMARY_BANKS; bank++) { + if (subsys->primary_banks[bank] >= 0) { + Subsys_status[this_subsys].primary_banks[bank] = mine_laser_index; + } + } + } + } + break; + default: + break; + } + pobj.replacement_textures = sip->replacement_textures; // initialize our set with the ship class set, which may be empty pobj.score = sip->score; From 56ca3db133a00eed91faec31bd88d67a011e2282 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Wed, 1 Nov 2023 01:10:09 +0000 Subject: [PATCH 032/466] Correct switch statement for iff_lookup The iff_lookup switch statement should include a break and default case. --- code/mission/import/xwingmissionparse.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index e2a9918aaef..6fc85b8202a 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -785,6 +785,9 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec team = index; else Warning(LOCATION, "Could not find iff %s", team_name); + break; + default: + break; } // Copy objects in Parse_objects to set for name checking below From 5f32d72972b449fbbd26039c191be6894db0bcf8 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Thu, 2 Nov 2023 00:50:35 +0000 Subject: [PATCH 033/466] Add objects to parse_xwi_mission and fixes Add lines to parse_xwi_mission so that the objects would be included. Also include some other fixes and code cleanup. --- code/mission/import/xwingmissionparse.cpp | 68 ++++++++++------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 6fc85b8202a..657e399b15a 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -624,7 +624,7 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh } } -const char *xwi_determine_object_type(const XWMObject *oj) +const char *xwi_determine_object_class(const XWMObject *oj) { switch (oj->objectType) { case XWMObjectType::oj_Mine1: @@ -690,11 +690,11 @@ void xwi_determine_object_orient(matrix* orient, const XWMObject* oj) // Determine the unique name from the object comprised of the object type and suffix. // Add the new name to the objectNameSet and return it to parse_xwi_objectgroup -const char* xwi_determine_space_object_name(SCP_set& objectNameSet, const char* object_type) +const char* xwi_determine_space_object_name(SCP_set& objectNameSet, const char* class_name) { char base_name[NAME_LENGTH]; char suffix[NAME_LENGTH]; - strcpy_s(base_name, object_type); + strcpy_s(base_name, class_name); end_string_at_first_hash_symbol(base_name); // we'll need to try suffixes starting at 1 and going until we find a unique name @@ -727,50 +727,34 @@ const char* xwi_determine_space_object_name(SCP_set& objectNameSet, void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObject* oj) { SCP_UNUSED(pm); - auto object_type = xwi_determine_object_type(oj); - if (object_type == nullptr) + auto class_name = xwi_determine_object_class(oj); + if (class_name == nullptr) return; int number_of_objects = oj->numberOfObjects; if (number_of_objects < 1) return; - if (number_of_objects > 1) { - switch (oj->objectType) { - case XWMObjectType::oj_Mine1: - case XWMObjectType::oj_Mine2: - case XWMObjectType::oj_Mine3: - case XWMObjectType::oj_Mine4: - break; - default: - Warning(LOCATION, - "NumberOfCraft of '%s' was %d but must be 1.", object_type, number_of_objects); - number_of_objects = 1; - break; - } - } - - int ship_class = ship_info_lookup(object_type); - if (ship_class < 0) { - Warning(LOCATION, "Unable to determine ship class for Object Group with type %s", object_type); - ship_class = 0; - } - auto sip = &Ship_info[ship_class]; - // object position and orientation // NOTE: Y and Z are swapped after all operartions are perfomed // units are in kilometers (after processing by xwinglib which handles the factor of 160), so scale them up float objectPosX = oj->object_x*1000; float objectPosY = oj->object_y*1000; float objectPosZ = oj->object_z*1000; + float offsetAxisA = 0; + float offsetAxisB = 0; int mine_dist = 400; // change this to change the distance between the mines - /** The minefield is a square 2d along two planes (a,b) centred on the given position **/ - float offsetAxisA = 0 -(mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) - float offsetAxisB = 0 -(mine_dist / 2 * (number_of_objects - 1)); - + int mine_laser_index = weapon_info_lookup("T&B KX-5#imp"); // "Defense Mine#Ion" needs to have its weapon changed to laser matrix orient; xwi_determine_object_orient(&orient, oj); + + int ship_class = ship_info_lookup(class_name); + if (ship_class < 0) { + Warning(LOCATION, "Unable to determine ship class for Object Group with type %s", class_name); + ship_class = 0; + } + auto sip = &Ship_info[ship_class]; int team = Species_info[sip->species].default_iff; @@ -785,8 +769,16 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec team = index; else Warning(LOCATION, "Could not find iff %s", team_name); + if (number_of_objects > 1) { + offsetAxisA -= (mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) + offsetAxisB -= (mine_dist / 2 * (number_of_objects - 1)); + } break; default: + if (number_of_objects > 1) { + Warning(LOCATION, "NumberOfCraft of '%s' was %d but must be 1.", class_name, number_of_objects); + number_of_objects = 1; + } break; } @@ -797,21 +789,17 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec objectNameSet.insert(Parse_objects[n].name); } - // now begin to configure each object in the group (mines multiple) - + // Now begin to configure each object in the group (mines multiple) for (int a = 0; a < number_of_objects; a++) { // make an a-b 2d grid from the mines offsetAxisA += (mine_dist * a); // add a new row to the grid for (int b = 0; b < number_of_objects; b++) { // for each increment along the a plane, add mines along b plane offsetAxisB += (mine_dist * b); // for each new row populate the column - /** Now convert the grid (a,b) to the relavenat formation ie. (x,y) or (z,y) etc **/ + // Now convert the grid (a,b) to the relavenat formation ie. (x,y) or (z,y) etc auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, offsetAxisA, offsetAxisB); - // Defense Mine#Ion needs to have it's weapon changed from ion to laser - int mine_laser_index = weapon_info_lookup("T&B KX-5#imp"); - p_object pobj; - strcpy_s(pobj.name, xwi_determine_space_object_name(objectNameSet, object_type)); + strcpy_s(pobj.name, xwi_determine_space_object_name(objectNameSet, class_name)); pobj.orient = orient; pobj.pos = ojxyz; @@ -916,6 +904,10 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) // load flight groups for (const auto &fg : xwim->flightgroups) parse_xwi_flightgroup(pm, xwim, &fg); + + // load objects + for (const auto &obj : xwim->objects) + parse_xwi_objectgroup(pm, xwim, &obj); } void post_process_xwi_mission(mission *pm, const XWingMission *xwim) From 504896854b2b5b6bd1ea92ab7b7aebf0ee4c69e2 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Fri, 3 Nov 2023 00:09:43 +0000 Subject: [PATCH 034/466] Add warning for weapon change plus fixes Ensure that the weapon change for mines is valid and only apply the change to subsystems if valid. Add line spacing to improve clarity and readability. --- code/mission/import/xwingmissionparse.cpp | 27 ++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 657e399b15a..158e599b7cb 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -746,6 +746,9 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec int mine_dist = 400; // change this to change the distance between the mines int mine_laser_index = weapon_info_lookup("T&B KX-5#imp"); // "Defense Mine#Ion" needs to have its weapon changed to laser + if (mine_laser_index < 1) + Warning(LOCATION, "Weapon 'T&B KX-5#imp' could not be found."); + matrix orient; xwi_determine_object_orient(&orient, oj); @@ -769,6 +772,7 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec team = index; else Warning(LOCATION, "Could not find iff %s", team_name); + if (number_of_objects > 1) { offsetAxisA -= (mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) offsetAxisB -= (mine_dist / 2 * (number_of_objects - 1)); @@ -826,19 +830,22 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec pobj.subsys_count++; strcpy_s(Subsys_status[this_subsys].name, NOX("Pilot")); - for (int n = 0; n < sip->n_subsystems; n++) { - auto subsys = &sip->subsystems[n]; - if (subsys->type == SUBSYSTEM_TURRET) { - this_subsys = allocate_subsys_status(); - pobj.subsys_count++; - strcpy_s(Subsys_status[this_subsys].name, sip->subsystems[n].name); - - for (int bank = 0; bank < MAX_SHIP_PRIMARY_BANKS; bank++) { - if (subsys->primary_banks[bank] >= 0) { - Subsys_status[this_subsys].primary_banks[bank] = mine_laser_index; + if (mine_laser_index > 0) { + for (int n = 0; n < sip->n_subsystems; n++) { + auto subsys = &sip->subsystems[n]; + if (subsys->type == SUBSYSTEM_TURRET) { + this_subsys = allocate_subsys_status(); + pobj.subsys_count++; + strcpy_s(Subsys_status[this_subsys].name, sip->subsystems[n].name); + + for (int bank = 0; bank < MAX_SHIP_PRIMARY_BANKS; bank++) { + if (subsys->primary_banks[bank] >= 0) { + Subsys_status[this_subsys].primary_banks[bank] = mine_laser_index; + } } } } + } break; default: From 4abe283517ad64472ceae142024a247cd77e2053 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Sat, 4 Nov 2023 16:10:24 +0000 Subject: [PATCH 035/466] Update mine_laser_index mine_laser_index should be treated similar to team_name index and ship_class with a warning to notify if the index int is not valid/less than 0. Add auto weapon_name so that the warning can display the name of the weapon in the output. --- code/mission/import/xwingmissionparse.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 158e599b7cb..c0ade48f21e 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -745,9 +745,10 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec float offsetAxisB = 0; int mine_dist = 400; // change this to change the distance between the mines - int mine_laser_index = weapon_info_lookup("T&B KX-5#imp"); // "Defense Mine#Ion" needs to have its weapon changed to laser - if (mine_laser_index < 1) - Warning(LOCATION, "Weapon 'T&B KX-5#imp' could not be found."); + auto weapon_name = "T&B KX-5#imp"; + int mine_laser_index = weapon_info_lookup(weapon_name); // "Defense Mine#Ion" needs to have its weapon changed to laser + if (mine_laser_index < 0) + Warning(LOCATION, "Could not find weapon %s", weapon_name); matrix orient; xwi_determine_object_orient(&orient, oj); @@ -830,7 +831,7 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec pobj.subsys_count++; strcpy_s(Subsys_status[this_subsys].name, NOX("Pilot")); - if (mine_laser_index > 0) { + if (mine_laser_index >= 0) { for (int n = 0; n < sip->n_subsystems; n++) { auto subsys = &sip->subsystems[n]; if (subsys->type == SUBSYSTEM_TURRET) { From 8d63be0819dff5b888d1b797c2be249375fd10a2 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Mon, 6 Nov 2023 22:43:09 +0000 Subject: [PATCH 036/466] Fix switch cases to be in a block Encase the declared variables in the switch cases inside blocks to avoid compile errors. --- code/mission/import/xwingmissionparse.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index c0ade48f21e..8768cb05358 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -766,7 +766,7 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec case XWMObjectType::oj_Mine1: case XWMObjectType::oj_Mine2: case XWMObjectType::oj_Mine3: - case XWMObjectType::oj_Mine4: + case XWMObjectType::oj_Mine4: { auto team_name = "Hostile"; int index = iff_lookup(team_name); if (index >= 0) @@ -779,6 +779,7 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec offsetAxisB -= (mine_dist / 2 * (number_of_objects - 1)); } break; + } default: if (number_of_objects > 1) { Warning(LOCATION, "NumberOfCraft of '%s' was %d but must be 1.", class_name, number_of_objects); @@ -825,7 +826,7 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec case XWMObjectType::oj_Mine1: case XWMObjectType::oj_Mine2: case XWMObjectType::oj_Mine3: - case XWMObjectType::oj_Mine4: + case XWMObjectType::oj_Mine4: { pobj.subsys_index = Subsys_index; int this_subsys = allocate_subsys_status(); pobj.subsys_count++; @@ -846,9 +847,9 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec } } } - } break; + } default: break; } From 4c8b6bf0e74c541c06a6c8015ee16f0a00e7363d Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Mon, 6 Nov 2023 21:42:23 -0500 Subject: [PATCH 037/466] let's make a quick detour --- code/mission/missionparse.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index e2a037da3c5..d5b08421e6c 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6027,7 +6027,7 @@ void parse_sexp_containers() } } -bool parse_mission(mission *pm, XWingMission *xwim, int flags) +bool parse_mission(mission *pm, int flags) { int saved_warning_count = Global_warning_count; int saved_error_count = Global_error_count; @@ -6073,12 +6073,11 @@ bool parse_mission(mission *pm, XWingMission *xwim, int flags) parse_events(pm); parse_goals(pm); parse_waypoints_and_jumpnodes(pm); - parse_messages(pm, flags); - parse_reinforcements(pm); - parse_bitmaps(pm); - parse_asteroid_fields(pm); - parse_music(pm, flags); - } + parse_messages(pm, flags); + parse_reinforcements(pm); + parse_bitmaps(pm); + parse_asteroid_fields(pm); + parse_music(pm, flags); // if we couldn't load some mod data if ((Num_unknown_ship_classes > 0) || ( Num_unknown_loadout_classes > 0 )) { From d29a8fefc81902329e0502a4586f986698a7ee53 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Mon, 6 Nov 2023 23:56:09 -0500 Subject: [PATCH 038/466] Revert "let's make a quick detour" This reverts commit 4c8b6bf0e74c541c06a6c8015ee16f0a00e7363d, fixing the FotG repository after the automatic sync. --- code/mission/missionparse.cpp | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index dcb2135711e..74277050ade 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6119,7 +6119,7 @@ void apply_default_custom_data(mission* pm) } } -bool parse_mission(mission *pm, int flags) +bool parse_mission(mission *pm, XWingMission *xwim, int flags) { int saved_warning_count = Global_warning_count; int saved_error_count = Global_error_count; @@ -6165,12 +6165,13 @@ bool parse_mission(mission *pm, int flags) parse_events(pm); parse_goals(pm); parse_waypoints_and_jumpnodes(pm); - parse_messages(pm, flags); - parse_reinforcements(pm); - parse_bitmaps(pm); - parse_asteroid_fields(pm); - parse_music(pm, flags); - parse_custom_data(pm); + parse_messages(pm, flags); + parse_reinforcements(pm); + parse_bitmaps(pm); + parse_asteroid_fields(pm); + parse_music(pm, flags); + parse_custom_data(pm); + } // if we couldn't load some mod data if ((Num_unknown_ship_classes > 0) || ( Num_unknown_loadout_classes > 0 )) { From a8e08f020b44eb84a356fa1a925a8695544b49ff Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 7 Nov 2023 01:08:07 -0500 Subject: [PATCH 039/466] FRED import fixes This is the equivalent of scp-fs2open#5776 for the X-Wing importer. There's also a touch of scp-fs2open#5377 because the `OnFileImportFSM` changes from that PR were not carried over to `OnFileImportXWI`. Finally, there is an additional try/catch block when parsing the .BRF file because we don't want the import to fail if the briefing is missing. --- code/mission/missionparse.cpp | 15 +++++++++++---- fred2/freddoc.cpp | 29 +++++++++++++++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index dcb2135711e..f7291deef75 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6799,10 +6799,17 @@ bool parse_main(const char *mission_name, int flags) if (rval) { strcpy(ch, ".BRF"); - read_file_bytes(temp_filename, CF_TYPE_ANY); - XWingBriefing xwib; - XWingBriefing::load(&xwib, Parse_text_raw); - parse_xwi_briefing(&The_mission, &xwib); + try + { + read_file_bytes(temp_filename, CF_TYPE_ANY); + XWingBriefing xwib; + XWingBriefing::load(&xwib, Parse_text_raw); + parse_xwi_briefing(&The_mission, &xwib); + } + catch (const parse::ParseException& e) + { + mprintf(("MISSIONS: Unable to parse '%s' (the briefing file for '%s')! Error message = %s.\n", temp_filename, mission_name, e.what())); + } } } // regular mission load diff --git a/fred2/freddoc.cpp b/fred2/freddoc.cpp index f2eb76e9a21..5de2f3f0a7b 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -564,11 +564,10 @@ void CFREDDoc::OnFileImportXWI() memset(dest_directory, 0, sizeof(dest_directory)); - // get location to save to -#if ( _MFC_VER >= 0x0700 ) + // get location to save to BROWSEINFO bi; bi.hwndOwner = theApp.GetMainWnd()->GetSafeHwnd(); - bi.pidlRoot = nullptr; + bi.pidlRoot = NULL; bi.pszDisplayName = dest_directory; bi.lpszTitle = "Select a location to save in"; bi.ulFlags = 0; @@ -582,13 +581,9 @@ void CFREDDoc::OnFileImportXWI() return; SHGetPathFromIDList(ret_val, dest_directory); -#else - CFolderDialog dlgFolder(_T("Select a location to save in"), fs2_mission_path, NULL); - if (dlgFolder.DoModal() != IDOK) - return; - strcpy_s(dest_directory, dlgFolder.GetFolderPath()); -#endif + if (*dest_directory == '\0') + return; // clean things up first if (Briefing_dialog) @@ -597,7 +592,8 @@ void CFREDDoc::OnFileImportXWI() clear_mission(); int num_files = 0; - char dest_path[MAX_PATH_LEN]; + int successes = 0; + char dest_path[MAX_PATH_LEN] = ""; // process all missions POSITION pos(dlgFile.GetStartPosition()); @@ -668,6 +664,7 @@ void CFREDDoc::OnFileImportXWI() continue; // success + successes++; } if (num_files > 1) @@ -677,16 +674,20 @@ void CFREDDoc::OnFileImportXWI() } else if (num_files == 1) { - SetModifiedFlag(FALSE); + if (successes == 1) + SetModifiedFlag(FALSE); if (Briefing_dialog) { Briefing_dialog->restore_editor_state(); Briefing_dialog->update_data(1); } - // these aren't done automatically for imports - theApp.AddToRecentFileList((LPCTSTR)dest_path); - SetTitle((LPCTSTR)Mission_filename); + if (successes == 1) + { + // these aren't done automatically for imports + theApp.AddToRecentFileList((LPCTSTR)dest_path); + SetTitle((LPCTSTR)Mission_filename); + } } recreate_dialogs(); From 1a9928bd2747dc4c829bc355aa6ae009b1da6b23 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 7 Nov 2023 15:20:09 -0500 Subject: [PATCH 040/466] add tug and container in consultation with wookieejedi --- code/mission/import/xwingmissionparse.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 7ad38cc11f6..efe29a73cee 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -248,9 +248,9 @@ const char *xwi_determine_base_ship_class(const XWMFlightGroup *fg) case XWMFlightGroupType::fg_Shuttle: return "Lambda-class T-4a Shuttle"; case XWMFlightGroupType::fg_Tug: - return nullptr; + return "DV-3 Cargo Freighter"; case XWMFlightGroupType::fg_Container: - return nullptr; + return "BFF-1 Container"; case XWMFlightGroupType::fg_Freighter: return "BFF-1 Bulk Freighter"; case XWMFlightGroupType::fg_Calamari_Cruiser: From f6543fd605081bba2621b814bfee0af41a2d9042 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 7 Nov 2023 19:27:42 -0500 Subject: [PATCH 041/466] suppress benign warning Rather than redundantly handle the loadout when objects are being created, let's just allow the importer to fix the weaponry pool here. --- fred2/freddoc.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fred2/freddoc.cpp b/fred2/freddoc.cpp index 43f86b95a9b..035552d1be1 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -325,7 +325,10 @@ bool CFREDDoc::load_mission(const char *pathname, int flags) { // double check the used pool is empty for (j = 0; j < weapon_info_size(); j++) { if (used_pool[j] != 0) { - Warning(LOCATION, "%s is used in wings of team %d but was not in the loadout. Fixing now", Weapon_info[j].name, i + 1); + // suppress the warning when importing an X-Wing mission, since this is as good a place as any to fix the loadout + if (!(flags & MPF_IMPORT_XWI)) { + Warning(LOCATION, "%s is used in wings of team %d but was not in the loadout. Fixing now", Weapon_info[j].name, i + 1); + } // add the weapon as a new entry Team_data[i].weaponry_pool[Team_data[i].num_weapon_choices] = j; From de207dd32b784820753cd36d12310514b083497c Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 7 Nov 2023 19:53:15 -0500 Subject: [PATCH 042/466] set mission specs flags --- code/mission/import/xwingmissionparse.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index efe29a73cee..13dd0798e6e 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -1,6 +1,7 @@ #include "iff_defs/iff_defs.h" #include "mission/missionparse.h" #include "mission/missiongoals.h" +#include "mission/missionmessage.h" #include "missionui/redalert.h" #include "nebula/neb.h" #include "parse/parselo.h" @@ -913,6 +914,20 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) Mp = sexp_buf; config_event->formula = get_sexp_main(); + // this seems like a sensible default + auto command_persona_name = "Flight Computer"; + pm->command_persona = message_persona_name_lookup(command_persona_name); + if (pm->command_persona >= 0) + { + strcpy_s(pm->command_sender, command_persona_name); // it works as a sender too! + pm->flags.set(Mission::Mission_Flags::Override_hashcommand); + } + else + Warning(LOCATION, "Unable to find the persona '%s'", command_persona_name); + + // other mission flags + pm->support_ships.max_support_ships = 0; + // load flight groups for (const auto &fg : xwim->flightgroups) parse_xwi_flightgroup(pm, xwim, &fg); From 854897fd2228073729e47ce3580270a30edecd24 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 7 Nov 2023 19:53:27 -0500 Subject: [PATCH 043/466] only set variants for fighter/bomber --- code/mission/import/xwingmissionparse.cpp | 55 ++++++++++++----------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 13dd0798e6e..a3ad0063552 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -280,37 +280,42 @@ int xwi_determine_ship_class(const XWMFlightGroup *fg) if (class_name == nullptr) return -1; - SCP_string variant_class = class_name; - bool variant = false; - - // now see if we have any variants - if (fg->craftColor == XWMCraftColor::c_Red) - { - variant_class += "#red"; - variant = true; - } - else if (fg->craftColor == XWMCraftColor::c_Gold) - { - variant_class += "#gold"; - variant = true; - } - else if (fg->craftColor == XWMCraftColor::c_Blue) + // let's only look for variant classes on flyable ships + int base_class = ship_info_lookup(class_name); + if (base_class >= 0 && Ship_info[base_class].is_fighter_bomber()) { - variant_class += "#blue"; - variant = true; - } + SCP_string variant_name = class_name; + bool variant = false; - if (variant) - { - int ship_class = ship_info_lookup(variant_class.c_str()); - if (ship_class >= 0) - return ship_class; + // see if we have any variants + if (fg->craftColor == XWMCraftColor::c_Red) + { + variant_name += "#red"; + variant = true; + } + else if (fg->craftColor == XWMCraftColor::c_Gold) + { + variant_name += "#gold"; + variant = true; + } + else if (fg->craftColor == XWMCraftColor::c_Blue) + { + variant_name += "#blue"; + variant = true; + } + + if (variant) + { + int variant_class = ship_info_lookup(variant_name.c_str()); + if (variant_class >= 0) + return variant_class; - Warning(LOCATION, "Could not find variant ship class %s for Flight Group %s. Using base class instead.", variant_class.c_str(), fg->designation.c_str()); + Warning(LOCATION, "Could not find variant ship class %s for Flight Group %s. Using base class instead.", variant_name.c_str(), fg->designation.c_str()); + } } // no variant, or we're just going with the base class - return ship_info_lookup(class_name); + return base_class; } const char *xwi_determine_team(const XWingMission *xwim, const XWMFlightGroup *fg, const ship_info *sip) From 196913e55cd465b0ae02187ad3bca6d3ebe6287d Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 7 Nov 2023 20:50:03 -0500 Subject: [PATCH 044/466] fix some errors in the space object import 1. actually add objects to the list 2. assign the ship class --- code/mission/import/xwinglib.cpp | 3 +++ code/mission/import/xwingmissionparse.cpp | 1 + 2 files changed, 4 insertions(+) diff --git a/code/mission/import/xwinglib.cpp b/code/mission/import/xwinglib.cpp index b4b8d96c38b..bcd1da03e8b 100644 --- a/code/mission/import/xwinglib.cpp +++ b/code/mission/import/xwinglib.cpp @@ -780,6 +780,9 @@ bool XWingMission::load(XWingMission *m, const char *data) noj->object_yaw = oj->object_yaw; noj->object_pitch = oj->object_pitch; noj->object_roll = oj->object_roll - 90.0f; + + m->objects.push_back(*noj); } + return true; } diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index a3ad0063552..081f4d0a5de 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -815,6 +815,7 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec p_object pobj; strcpy_s(pobj.name, xwi_determine_space_object_name(objectNameSet, class_name)); + pobj.ship_class = ship_class; pobj.orient = orient; pobj.pos = ojxyz; From c883f62c69c3557b383c90c9ad1b98dd860f5cbc Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 7 Nov 2023 21:37:11 -0500 Subject: [PATCH 045/466] fix wing and ship departure cues --- code/mission/import/xwingmissionparse.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 081f4d0a5de..86c97a84b85 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -461,6 +461,7 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh wingp->arrival_delay = fg->arrivalDelay; wingp->arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; wingp->arrival_anchor = xwi_determine_anchor(xwim, fg); + wingp->departure_cue = Locked_sexp_false; wingp->departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; wingp->departure_anchor = wingp->arrival_anchor; @@ -540,6 +541,7 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh pobj.wingnum = wingnum; pobj.pos_in_wing = wing_index; pobj.arrival_cue = Locked_sexp_false; + pobj.departure_cue = Locked_sexp_false; } else { @@ -550,6 +552,7 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh pobj.arrival_delay = fg->arrivalDelay; pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); + pobj.departure_cue = Locked_sexp_false; pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; pobj.departure_anchor = pobj.arrival_anchor; From 785450093753f8dd286a5231f934fe9da5e71551 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 7 Nov 2023 21:25:23 -0500 Subject: [PATCH 046/466] space object enhancements 1. name space objects with a letter corresponding to their group, as well as the index within their group 2. move `pos` and `orient` assignments to a place corresponding to flight groups 3. set the "hide ship name" flag since it seems appropriate --- code/mission/import/xwingmissionparse.cpp | 32 +++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 86c97a84b85..15857fe398c 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -702,13 +702,28 @@ void xwi_determine_object_orient(matrix* orient, const XWMObject* oj) // Determine the unique name from the object comprised of the object type and suffix. // Add the new name to the objectNameSet and return it to parse_xwi_objectgroup -const char* xwi_determine_space_object_name(SCP_set& objectNameSet, const char* class_name) +const char *xwi_determine_space_object_name(SCP_set &objectNameSet, const char *class_name, const int og_index) { char base_name[NAME_LENGTH]; char suffix[NAME_LENGTH]; strcpy_s(base_name, class_name); end_string_at_first_hash_symbol(base_name); + // try to make the object group index part of the name too + if ((strlen(base_name) < NAME_LENGTH - 7) && (og_index < 26*26)) { + strcat_s(base_name, " "); + + int offset = og_index; + if (offset >= 26) { + sprintf(suffix, NOX("%c"), 'A' + (offset / 26) - 1); + strcat_s(base_name, suffix); + offset %= 26; + } + + sprintf(suffix, NOX("%c"), 'A' + offset); + strcat_s(base_name, suffix); + } + // we'll need to try suffixes starting at 1 and going until we find a unique name int n = 1; char object_name[NAME_LENGTH]; @@ -736,9 +751,10 @@ const char* xwi_determine_space_object_name(SCP_set& objectNameSet, return iter.first->c_str(); } -void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObject* oj) +void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObject *oj) { SCP_UNUSED(pm); + auto class_name = xwi_determine_object_class(oj); if (class_name == nullptr) return; @@ -747,6 +763,9 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec if (number_of_objects < 1) return; + // determine which space object this is in our list + int og_index = static_cast(std::distance(xwim->objects.data(), oj)); + // object position and orientation // NOTE: Y and Z are swapped after all operartions are perfomed // units are in kilometers (after processing by xwinglib which handles the factor of 160), so scale them up @@ -817,10 +836,8 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, offsetAxisA, offsetAxisB); p_object pobj; - strcpy_s(pobj.name, xwi_determine_space_object_name(objectNameSet, class_name)); + strcpy_s(pobj.name, xwi_determine_space_object_name(objectNameSet, class_name, og_index)); pobj.ship_class = ship_class; - pobj.orient = orient; - pobj.pos = ojxyz; pobj.arrival_cue = Locked_sexp_true; pobj.arrival_location = ARRIVE_AT_LOCATION; @@ -871,8 +888,13 @@ void parse_xwi_objectgroup(mission* pm, const XWingMission* xwim, const XWMObjec pobj.score = sip->score; pobj.team = team; + pobj.pos = ojxyz; + pobj.orient = orient; + pobj.initial_velocity = 0; + pobj.flags.set(Mission::Parse_Object_Flags::SF_Hide_ship_name); // space objects in X-Wing don't really have names + Parse_objects.push_back(pobj); } } From 1b2424cb2f5c356b981826e8529a42eb5d659812 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 7 Nov 2023 22:02:36 -0500 Subject: [PATCH 047/466] add missing green variant --- code/mission/import/xwinglib.cpp | 3 +++ code/mission/import/xwinglib.h | 3 ++- code/mission/import/xwingmissionparse.cpp | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/code/mission/import/xwinglib.cpp b/code/mission/import/xwinglib.cpp index bcd1da03e8b..0c9c39bdeec 100644 --- a/code/mission/import/xwinglib.cpp +++ b/code/mission/import/xwinglib.cpp @@ -522,6 +522,9 @@ bool XWingMission::load(XWingMission *m, const char *data) case 2: nfg->craftColor = XWMCraftColor::c_Blue; break; + case 3: + nfg->craftColor = XWMCraftColor::c_Green; + break; default: return false; } diff --git a/code/mission/import/xwinglib.h b/code/mission/import/xwinglib.h index 20943d5236a..a642cccbd22 100644 --- a/code/mission/import/xwinglib.h +++ b/code/mission/import/xwinglib.h @@ -114,7 +114,8 @@ enum class XWMCraftColor : short { c_Red = 0, c_Gold, - c_Blue + c_Blue, + c_Green }; enum class XWMObjective : short diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 15857fe398c..c8051bfc290 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -303,6 +303,11 @@ int xwi_determine_ship_class(const XWMFlightGroup *fg) variant_name += "#blue"; variant = true; } + else if (fg->craftColor == XWMCraftColor::c_Green) + { + variant_name += "#green"; + variant = true; + } if (variant) { From a485dcabdf3bf010ed7615acae13994a5bc50ffb Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Wed, 8 Nov 2023 14:21:15 +0000 Subject: [PATCH 048/466] Fix mine formation Change how mine_dist is added for each mine in the grid so that it is a set increment. An error was causing the distance between successive mines to be cumulative. --- code/mission/import/xwingmissionparse.cpp | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index c8051bfc290..143976ad8f7 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -777,8 +777,13 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec float objectPosX = oj->object_x*1000; float objectPosY = oj->object_y*1000; float objectPosZ = oj->object_z*1000; - float offsetAxisA = 0; - float offsetAxisB = 0; + float initOffsetAxisA = 0; + float initOffsetAxisB = 0; + float offsetAxisA; + float offsetAxisB; + + // Warning(LOCATION, "Object %s : X %d, Y %d, Z %d", class_name, (int)objectPosX, (int)objectPosY, (int)objectPosZ); + int mine_dist = 400; // change this to change the distance between the mines auto weapon_name = "T&B KX-5#imp"; @@ -811,8 +816,8 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec Warning(LOCATION, "Could not find iff %s", team_name); if (number_of_objects > 1) { - offsetAxisA -= (mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) - offsetAxisB -= (mine_dist / 2 * (number_of_objects - 1)); + initOffsetAxisA -= (mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) + initOffsetAxisB -= (mine_dist / 2 * (number_of_objects - 1)); } break; } @@ -833,11 +838,11 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec // Now begin to configure each object in the group (mines multiple) for (int a = 0; a < number_of_objects; a++) { // make an a-b 2d grid from the mines - offsetAxisA += (mine_dist * a); // add a new row to the grid - for (int b = 0; b < number_of_objects; b++) { // for each increment along the a plane, add mines along b plane - offsetAxisB += (mine_dist * b); // for each new row populate the column + offsetAxisA = initOffsetAxisA + (mine_dist * a); // start a new column on the grid + for (int b = 0; b < number_of_objects; b++) { // populate the column with mines + offsetAxisB = initOffsetAxisB + (mine_dist * b); // increment distance from the start of the column - // Now convert the grid (a,b) to the relavenat formation ie. (x,y) or (z,y) etc + // Convert the mine pos. (a,b) to the relavenat formation pos. ie. (x,y) or (z,y) etc auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, offsetAxisA, offsetAxisB); p_object pobj; From 92c9fdc3b9006fc851273ba0884275365262f375 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:18:25 +0000 Subject: [PATCH 049/466] Improve mine formation code Calcluate the grid without the need of initOffsetAxisA and initOffsetAxisB. --- code/mission/import/xwingmissionparse.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 143976ad8f7..528b760fa1d 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -777,10 +777,8 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec float objectPosX = oj->object_x*1000; float objectPosY = oj->object_y*1000; float objectPosZ = oj->object_z*1000; - float initOffsetAxisA = 0; - float initOffsetAxisB = 0; - float offsetAxisA; - float offsetAxisB; + float offsetAxisA = 0; + float offsetAxisB = 0; // Warning(LOCATION, "Object %s : X %d, Y %d, Z %d", class_name, (int)objectPosX, (int)objectPosY, (int)objectPosZ); @@ -816,8 +814,8 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec Warning(LOCATION, "Could not find iff %s", team_name); if (number_of_objects > 1) { - initOffsetAxisA -= (mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) - initOffsetAxisB -= (mine_dist / 2 * (number_of_objects - 1)); + offsetAxisA -= (mine_dist / 2 * (number_of_objects - 1)); // (- the distance to centre the grid) + offsetAxisB -= (mine_dist / 2 * (number_of_objects - 1)); } break; } @@ -838,9 +836,7 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec // Now begin to configure each object in the group (mines multiple) for (int a = 0; a < number_of_objects; a++) { // make an a-b 2d grid from the mines - offsetAxisA = initOffsetAxisA + (mine_dist * a); // start a new column on the grid for (int b = 0; b < number_of_objects; b++) { // populate the column with mines - offsetAxisB = initOffsetAxisB + (mine_dist * b); // increment distance from the start of the column // Convert the mine pos. (a,b) to the relavenat formation pos. ie. (x,y) or (z,y) etc auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, offsetAxisA, offsetAxisB); @@ -906,7 +902,11 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec pobj.flags.set(Mission::Parse_Object_Flags::SF_Hide_ship_name); // space objects in X-Wing don't really have names Parse_objects.push_back(pobj); + + offsetAxisB += mine_dist; // increment distance from the start of the column } + offsetAxisA += mine_dist; // start a new column on the grid + offsetAxisB -= (mine_dist * number_of_objects); // prepare to start a new column } } From 40810314722b42ba3ba1fcb18d4bc7da22f79762 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:25:15 +0000 Subject: [PATCH 050/466] Fix to mine formation Simplify the mine formation so that the mine locations are simply determined without cumulative counting or reseting variables. --- code/mission/import/xwingmissionparse.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 528b760fa1d..77cacbde6a2 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -836,10 +836,11 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec // Now begin to configure each object in the group (mines multiple) for (int a = 0; a < number_of_objects; a++) { // make an a-b 2d grid from the mines + float initOffsetAxisB = offsetAxisB; for (int b = 0; b < number_of_objects; b++) { // populate the column with mines // Convert the mine pos. (a,b) to the relavenat formation pos. ie. (x,y) or (z,y) etc - auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, offsetAxisA, offsetAxisB); + auto ojxyz = xwi_determine_mine_formation_position(oj, objectPosX, objectPosY, objectPosZ, offsetAxisA + (mine_dist * a), offsetAxisB + (mine_dist * b)); p_object pobj; strcpy_s(pobj.name, xwi_determine_space_object_name(objectNameSet, class_name, og_index)); @@ -902,11 +903,7 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec pobj.flags.set(Mission::Parse_Object_Flags::SF_Hide_ship_name); // space objects in X-Wing don't really have names Parse_objects.push_back(pobj); - - offsetAxisB += mine_dist; // increment distance from the start of the column } - offsetAxisA += mine_dist; // start a new column on the grid - offsetAxisB -= (mine_dist * number_of_objects); // prepare to start a new column } } From 7897a8e50b00d95e82bed36e4da0559e9b2cc605 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:32:14 +0000 Subject: [PATCH 051/466] Remove comments and test code Remove commented out test code and other errors which should no longer be included. --- code/mission/import/xwingmissionparse.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 77cacbde6a2..cd2cdde289c 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -780,9 +780,6 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec float offsetAxisA = 0; float offsetAxisB = 0; - // Warning(LOCATION, "Object %s : X %d, Y %d, Z %d", class_name, (int)objectPosX, (int)objectPosY, (int)objectPosZ); - - int mine_dist = 400; // change this to change the distance between the mines auto weapon_name = "T&B KX-5#imp"; int mine_laser_index = weapon_info_lookup(weapon_name); // "Defense Mine#Ion" needs to have its weapon changed to laser @@ -836,7 +833,6 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec // Now begin to configure each object in the group (mines multiple) for (int a = 0; a < number_of_objects; a++) { // make an a-b 2d grid from the mines - float initOffsetAxisB = offsetAxisB; for (int b = 0; b < number_of_objects; b++) { // populate the column with mines // Convert the mine pos. (a,b) to the relavenat formation pos. ie. (x,y) or (z,y) etc From d72ec2f4ba98e67fbfa4a7348668a1e51c685d77 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Fri, 10 Nov 2023 20:12:11 +0000 Subject: [PATCH 052/466] Limit space objects to 64 Despite some mission files containing over 100 space objects, only 64 are loaded in the mission. This should be reflected in the FRED import too. Therefore place a limit of 64 objects to be imported into FRED. --- code/mission/import/xwingmissionparse.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index cd2cdde289c..9b78fe6f71c 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -18,6 +18,8 @@ extern int allocate_subsys_status(); static int Player_flight_group = 0; +const int MAX_SPACE_OBJECTS = 64; // To match the XWing game engine limit + // vazor222 void parse_xwi_mission_info(mission *pm, const XWingMission *xwim) { @@ -756,7 +758,7 @@ const char *xwi_determine_space_object_name(SCP_set &objectNameSet, return iter.first->c_str(); } -void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObject *oj) +void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObject *oj, int &object_count) { SCP_UNUSED(pm); @@ -899,6 +901,10 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec pobj.flags.set(Mission::Parse_Object_Flags::SF_Hide_ship_name); // space objects in X-Wing don't really have names Parse_objects.push_back(pobj); + + object_count++; + if (object_count >= MAX_SPACE_OBJECTS) + return; } } } @@ -966,9 +972,13 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) for (const auto &fg : xwim->flightgroups) parse_xwi_flightgroup(pm, xwim, &fg); - // load objects - for (const auto &obj : xwim->objects) - parse_xwi_objectgroup(pm, xwim, &obj); + // load objects - up to the maximum number of objects allowed by the XWing engine + int object_count = 0; + for (const auto& obj : xwim->objects) { + if (object_count >= MAX_SPACE_OBJECTS) + break; + parse_xwi_objectgroup(pm, xwim, &obj, object_count); + } } void post_process_xwi_mission(mission *pm, const XWingMission *xwim) From 3b1d78154b1fdebc0c510ec3a02a51cafdbb744e Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Sun, 12 Nov 2023 20:35:14 +0000 Subject: [PATCH 053/466] Update to object limit In the case of an object group total exceeding the MAX_SPACE_OBJECTS that object group should be passed over and the next object group checked. --- code/mission/import/xwingmissionparse.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 9b78fe6f71c..8150146732c 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -826,6 +826,10 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec break; } + if (object_count + (number_of_objects * number_of_objects) > MAX_SPACE_OBJECTS) + return; + object_count += (number_of_objects * number_of_objects); + // Copy objects in Parse_objects to set for name checking below // This only needs to be done fully once per object group then can be added to after each new object SCP_set objectNameSet; @@ -901,10 +905,6 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec pobj.flags.set(Mission::Parse_Object_Flags::SF_Hide_ship_name); // space objects in X-Wing don't really have names Parse_objects.push_back(pobj); - - object_count++; - if (object_count >= MAX_SPACE_OBJECTS) - return; } } } From 443868caa415e929d120391cc114c3d7b94c9528 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Sun, 12 Nov 2023 21:51:07 +0000 Subject: [PATCH 054/466] Remove redundant check for object count Remove the check for MAX_SPACE_OBJECTS in parse_xwi_mission. The check can be performed each time a new object group is parsed in parse_xwi_objectgroup. --- code/mission/import/xwingmissionparse.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 8150146732c..6f9c6917aae 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -826,6 +826,7 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec break; } + // Check that the Xwing game engine object limit is not exceeded with this object group if (object_count + (number_of_objects * number_of_objects) > MAX_SPACE_OBJECTS) return; object_count += (number_of_objects * number_of_objects); @@ -972,11 +973,9 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) for (const auto &fg : xwim->flightgroups) parse_xwi_flightgroup(pm, xwim, &fg); - // load objects - up to the maximum number of objects allowed by the XWing engine + // load object groups int object_count = 0; for (const auto& obj : xwim->objects) { - if (object_count >= MAX_SPACE_OBJECTS) - break; parse_xwi_objectgroup(pm, xwim, &obj, object_count); } } From 7067b455ffa482a24974103a126ee44427ca64e5 Mon Sep 17 00:00:00 2001 From: Dark-Visor <90473615+Dark-Visor@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:47:50 +0000 Subject: [PATCH 055/466] Cleanup - remove braces Now that load object groups only contains one line of code then remove the braces to be in keeping with the rest of the function. --- code/mission/import/xwingmissionparse.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 6f9c6917aae..40fdb7c783d 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -975,9 +975,8 @@ void parse_xwi_mission(mission *pm, const XWingMission *xwim) // load object groups int object_count = 0; - for (const auto& obj : xwim->objects) { + for (const auto& obj : xwim->objects) parse_xwi_objectgroup(pm, xwim, &obj, object_count); - } } void post_process_xwi_mission(mission *pm, const XWingMission *xwim) From 98a5ff4a231e8a3768a0e4d79b2a55a904343085 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 31 Mar 2024 12:43:08 -0400 Subject: [PATCH 056/466] Initial Dialog Not building yet. Some sort of linking error --- qtfred/source_groups.cmake | 5 + .../mission/dialogs/VariableDialogModel.cpp | 0 .../src/mission/dialogs/VariableDialogModel.h | 21 + qtfred/src/ui/FredView.cpp | 6 + qtfred/src/ui/FredView.h | 1 + qtfred/src/ui/dialogs/VariableDialog.cpp | 189 ++++++++ qtfred/src/ui/dialogs/VariableDialog.h | 61 +++ qtfred/ui/VariableDialog.ui | 429 ++++++++++++++++++ 8 files changed, 712 insertions(+) create mode 100644 qtfred/src/mission/dialogs/VariableDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/VariableDialogModel.h create mode 100644 qtfred/src/ui/dialogs/VariableDialog.cpp create mode 100644 qtfred/src/ui/dialogs/VariableDialog.h create mode 100644 qtfred/ui/VariableDialog.ui diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 7e3e41384c8..77e8c9f7f14 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -66,6 +66,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/SelectionDialogModel.h src/mission/dialogs/ShieldSystemDialogModel.cpp src/mission/dialogs/ShieldSystemDialogModel.h + src/mission/dialogs/VariableDialogModel.cpp + src/mission/dialogs/VariableDialogModel.h src/mission/dialogs/WaypointEditorDialogModel.cpp src/mission/dialogs/WaypointEditorDialogModel.h ) @@ -130,6 +132,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/SelectionDialog.h src/ui/dialogs/ShieldSystemDialog.h src/ui/dialogs/ShieldSystemDialog.cpp + src/ui/dialogs/VariableDialog.cpp + src/ui/dialogs/VariableDialog.h src/ui/dialogs/VoiceActingManager.h src/ui/dialogs/VoiceActingManager.cpp src/ui/dialogs/WaypointEditorDialog.cpp @@ -201,6 +205,7 @@ add_file_folder("UI" ui/PlayerOrdersDialog.ui ui/ShipTextureReplacementDialog.ui ui/ShipTBLViewer.ui + ui/VariableDialog.ui ) add_file_folder("Resources" diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h new file mode 100644 index 00000000000..94ee93b5815 --- /dev/null +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -0,0 +1,21 @@ +#pragma once + +#include "globalincs/pstypes.h" + +#include "AbstractDialogModel.h" + +namespace fso { +namespace fred { +namespace dialogs { + +class VariableDialogModel : public AbstractDialogModel { + public: + VariableDialogModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; +}; + +} // namespace dialogs +} // namespace fred +} // namespace fso diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 3b6fa4cf483..1ca3e5e7cbf 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include "mission/Editor.h" @@ -751,6 +752,11 @@ void FredView::on_actionLoadout_triggered(bool) { auto editorDialog = new dialogs::LoadoutDialog(this, _viewport); editorDialog->show(); } +void FredView::on_actionVariablesAndContainers_triggered(bool) { + auto editorDialog = new dialogs::VariableDialog(this, _viewport); + editorDialog->show(); +} + DialogButton FredView::showButtonDialog(DialogType type, const SCP_string& title, const SCP_string& message, diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 573c5b06be1..b11bfbfa0a3 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -91,6 +91,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionCommand_Briefing_triggered(bool); void on_actionReinforcements_triggered(bool); void on_actionLoadout_triggered(bool); + void on_actionVariablesAndContainers_triggered(bool); void on_actionSelectionLock_triggered(bool enabled); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp new file mode 100644 index 00000000000..a7b95db8844 --- /dev/null +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -0,0 +1,189 @@ +#include "VariableDialog.h" +#include "ui_VariableDialog.h" + +#include +#include + +#include +#include +//#include + +namespace fso { +namespace fred { +namespace dialogs { + +VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) + : QDialog(parent), ui(new Ui::VariableDialog()), _model(new VariableDialogModel(this, viewport)), _viewport(viewport) +{ + this->setFocus(); + + // Major Changes, like Applying the model, rejecting changes and updating the UI. + connect(_model.get(), &AbstractDialogModel::modelChanged, this, &VariableDialog::updateUI); + connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::apply); + connect(this, &QDialog::rejected, _model.get(), &VariableDialogModel::reject); + + connect(ui->variablesTable, + QOverload::of(&QTableWidget::cellChanged), + this, + &VariableDialog::onVariablesTableUpdated); + + connect(ui->variablesTable, + QOverload::of(&QTableWidget::cellChanged), + this, + &VariableDialog::onContainersTableUpdated); + + connect(ui->variablesTable, + QOverload::of(&QTableWidget::cellChanged), + this, + &VariableDialog::onContainerContentsTableUpdated); + + connect(ui->addVariableButton, + &QPushButton::clicked, + this, + &VariableDialog::onAddVariableButtonPressed); + + connect(ui->deleteVariableButton, + &QPushButton::clicked, + this, + &VariableDialog::onDeleteVariableButtonPressed); + + connect(ui->setVariableAsStringRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSetVariableAsStringRadioSelected); + + connect(ui->setVariableAsNumberRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSetVariableAsNumberRadioSelected); + + connect(ui->saveContainerOnMissionCompletedRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSaveVariableOnMissionCompleteRadioSelected); + + connect(ui->saveVariableOnMissionCloseRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSaveVariableOnMissionCloseRadioSelected); + + connect(ui->setVariableAsEternalcheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onSaveVariableAsEternalCheckboxClicked); + + connect(ui->addContainerButton, + &QPushButton::clicked, + this, + &VariableDialog::onAddContainerButtonPressed); + + connect(ui->deleteContainerButton, + &QPushButton::clicked, + this, + &VariableDialog::onDeleteContainerButtonPressed); + + connect(ui->setContainerAsMapRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSetContainerAsMapRadioSelected); + + connect(ui->setContainerAsListRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSetContainerAsListRadioSelected); + + connect(ui->setContainerAsStringRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSetContainerAsStringRadioSelected); + + connect(ui->setContainerAsNumberRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSetContainerAsNumberRadio); + + connect(ui->saveContainerOnMissionCloseRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSaveContainerOnMissionClosedRadioSelected); + + connect(ui->saveContainerOnMissionCompletedRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSaveContainerOnMissionCompletedRadioSelected); + + + connect(ui->addContainerItemButton, + &QPushButton::clicked, + this, + &VariableDialog::onAddContainerItemButtonPressed); + + connect(ui->deleteContainerItemButton, + &QPushButton::clicked, + this, + &VariableDialog::onDeleteContainerItemButtonPressed); + + connect(ui->setContainerAsEternalCheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onSetContainerAsEternalCheckboxClicked); +} + + void VariableDialog::onVariablesTableUpdated() {} + void VariableDialog::onContainersTableUpdated() {} + void VariableDialog::onContainerContentsTableUpdated() {} + void VariableDialog::onAddVariableButtonPressed() {} + void VariableDialog::onDeleteVariableButtonPressed() {} + void VariableDialog::onSetVariableAsStringRadioSelected() {} + void VariableDialog::onSetVariableAsNumberRadioSelected() {} + void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() {} + void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() {} + void VariableDialog::onSaveVariableAsEternalCheckboxClicked() {} + + void VariableDialog::onAddContainerButtonPressed() {} + void VariableDialog::onDeleteContainerButtonPressed() {} + void VariableDialog::onSetContainerAsMapRadioSelected() {} + void VariableDialog::onSetContainerAsListRadioSelected() {} + void VariableDialog::onSetContainerAsStringRadioSelected() {} + void VariableDialog::onSetContainerAsNumberRadio() {} + void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() {} + void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() {} + void VariableDialog::onNetworkContainerCheckboxClicked() {} + void VariableDialog::onSetContainerAsEternalCheckboxClicked() {} + void VariableDialog::onAddContainerItemButtonPressed() {} + void VariableDialog::onDeleteContainerItemButtonPressed() {} + +/* +containersTable +containerContentsTable +addVariableButton +deleteVariableButton +setVariableAsStringRadio +setVariableAsNumberRadio +networkVariableCheckbox +saveVariableOnMissionCompletedRaio +saveVariableOnMissionCloseRadio +setVariableAsEternalcheckbox + +addContainerButton +deleteContainerButton +setContainerAsMapRadio +setContainerAsListRadio +setContainerAsStringRadio +setContainerAsNumberRadio +saveContainerOnMissionCloseRadio +saveContainerOnMissionCompletedRadio +networkContainerCheckbox +setContainerAsEternalCheckbox + */ + + + +VariableDialog::~VariableDialog(){}; // NOLINT + +void VariableDialog::updateUI(){}; + + +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h new file mode 100644 index 00000000000..f3eec607aa6 --- /dev/null +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include + +namespace fso { +namespace fred { +namespace dialogs { + +namespace Ui { +class VariableDialog; +} + +class VariableDialog : public QDialog { + Q_OBJECT + + public: + explicit VariableDialog(FredView* parent, EditorViewport* viewport); + ~VariableDialog() override; + + private: + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + void updateUI(); + + void onVariablesTableUpdated(); + void onContainersTableUpdated(); + void onContainerContentsTableUpdated(); + void onAddVariableButtonPressed(); + void onDeleteVariableButtonPressed(); + void onSetVariableAsStringRadioSelected(); + void onSetVariableAsNumberRadioSelected(); + void onSaveVariableOnMissionCompleteRadioSelected(); + void onSaveVariableOnMissionCloseRadioSelected(); + void onSaveVariableAsEternalCheckboxClicked(); + + void onAddContainerButtonPressed(); + void onDeleteContainerButtonPressed(); + void onSetContainerAsMapRadioSelected(); + void onSetContainerAsListRadioSelected(); + void onSetContainerAsStringRadioSelected(); + void onSetContainerAsNumberRadio(); + void onSaveContainerOnMissionClosedRadioSelected(); + void onSaveContainerOnMissionCompletedRadioSelected(); + void onNetworkContainerCheckboxClicked(); + void onSetContainerAsEternalCheckboxClicked(); + void onAddContainerItemButtonPressed(); + void onDeleteContainerItemButtonPressed(); +}; + + + + + +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui new file mode 100644 index 00000000000..b3e99e727a6 --- /dev/null +++ b/qtfred/ui/VariableDialog.ui @@ -0,0 +1,429 @@ + + + fso::fred::dialogs::VariableDialog + + + + 0 + 0 + 589 + 608 + + + + Fiction Viewer Editor + + + + + + + 0 + 325 + + + + Containers + + + + + 10 + 30 + 231 + 121 + + + + + + + 10 + 160 + 331 + 161 + + + + Contents + + + + + 10 + 30 + 221 + 121 + + + + + + + 240 + 30 + 81 + 121 + + + + + + + Add + + + + + + + + 8 + + + + Delete + + + + + + + + + + 250 + 30 + 81 + 121 + + + + + + + Add + + + + + + + + 8 + + + + Delete + + + + + + + + + 360 + 180 + 201 + 141 + + + + Options + + + + + 0 + 20 + 201 + 121 + + + + + + + Save on Mission Close + + + + + + + Save on Mission Completed + + + + + + + Network-Variable + + + + + + + Eternal + + + + + + + + + + 359 + 18 + 201 + 151 + + + + + + + Container Type + + + + + 0 + 20 + 201 + 52 + + + + + + + Map + + + + + + + List + + + + + + + + + + + Data Type + + + + + 0 + 20 + 201 + 52 + + + + + + + String + + + + + + + Number + + + + + + + + + + + + + + + + 0 + 230 + + + + Variables + + + + + 260 + 30 + 175 + 61 + + + + + + + Add Variable + + + + + + + Delete Variable and References + + + + + + + + + 260 + 100 + 301 + 121 + + + + Options + + + + + 0 + 20 + 301 + 101 + + + + + + + + + String + + + + + + + Number + + + + + + + Network-Variable + + + + + + + + + + + Save on Mission Close + + + + + + + Save on Mission Completed + + + + + + + Eternal + + + + + + + + + + + + 10 + 30 + 241 + 191 + + + + + + + + + QLayout::SetMaximumSize + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 12 + + + + Variables and Containers Editor + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + From e7583ddab802dc097b706608088e30b19260778b Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 22 Dec 2023 18:41:06 -0500 Subject: [PATCH 057/466] start creating Model and helper structs --- .../mission/dialogs/VariableDialogModel.cpp | 7 +++++ .../src/mission/dialogs/VariableDialogModel.h | 31 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index e69de29bb2d..b40a90689f5 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -0,0 +1,7 @@ +#include "VariableDialogModel.h" + +VariableDialogModel::VariableDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + initializeData(); +} \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 94ee93b5815..15a7923f60c 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -8,12 +8,41 @@ namespace fso { namespace fred { namespace dialogs { +struct variable_info { + SCP_string name = ""; + bool container = false; + bool map = false; + bool string = true; + + SCP_vector number_values; + SCP_vector string_values; +}; + + +struct variable_or_container_info { + SCP_string name = ""; + bool container = false; + bool map = false; + bool string = true; + + SCP_vector keys; + SCP_vector number_values; + SCP_vector number_values; +}; + class VariableDialogModel : public AbstractDialogModel { - public: +public: VariableDialogModel(QObject* parent, EditorViewport* viewport); + void changeSelection(SCP_string name, bool container){} + void changeName(SCP_string oldName, SCP_string newName, bool container) + bool apply() override; void reject() override; + +private: + SCP_vector _items; + }; } // namespace dialogs From 5d0ee564ac2a801497a3cc46ae1678220f9e2ab4 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 29 Dec 2023 17:50:39 -0500 Subject: [PATCH 058/466] fix support structs --- qtfred/src/mission/dialogs/VariableDialogModel.h | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 15a7923f60c..d98797ecc63 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -10,24 +10,21 @@ namespace dialogs { struct variable_info { SCP_string name = ""; - bool container = false; - bool map = false; bool string = true; - SCP_vector number_values; - SCP_vector string_values; + int number_values; + SCP_string string_values; }; -struct variable_or_container_info { +struct container_info { SCP_string name = ""; - bool container = false; bool map = false; bool string = true; SCP_vector keys; SCP_vector number_values; - SCP_vector number_values; + SCP_vector number_values; }; class VariableDialogModel : public AbstractDialogModel { @@ -41,8 +38,8 @@ class VariableDialogModel : public AbstractDialogModel { void reject() override; private: - SCP_vector _items; - + SCP_vector _variableItems; + SCP_vector _containerItems; }; } // namespace dialogs From a5781b35ac90027b95ec5c4fa211f9c043322692 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 3 Jan 2024 21:55:00 -0500 Subject: [PATCH 059/466] prepare_to_sync_2024_01_03 Get the codebase into a state where it can be automatically synced with the FSO code. This commit will be rolled back after the sync. --- fred2/freddoc.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/fred2/freddoc.cpp b/fred2/freddoc.cpp index 035552d1be1..43f86b95a9b 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -325,10 +325,7 @@ bool CFREDDoc::load_mission(const char *pathname, int flags) { // double check the used pool is empty for (j = 0; j < weapon_info_size(); j++) { if (used_pool[j] != 0) { - // suppress the warning when importing an X-Wing mission, since this is as good a place as any to fix the loadout - if (!(flags & MPF_IMPORT_XWI)) { - Warning(LOCATION, "%s is used in wings of team %d but was not in the loadout. Fixing now", Weapon_info[j].name, i + 1); - } + Warning(LOCATION, "%s is used in wings of team %d but was not in the loadout. Fixing now", Weapon_info[j].name, i + 1); // add the weapon as a new entry Team_data[i].weaponry_pool[Team_data[i].num_weapon_choices] = j; From 6d458125f6cc6d7f8322f71e5c877fb4cdb4c438 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 3 Jan 2024 22:01:23 -0500 Subject: [PATCH 060/466] Revert "prepare_to_sync_2024_01_03" This reverts commit a5781b35ac90027b95ec5c4fa211f9c043322692, with appropriate conflicts resolved. --- fred2/freddoc.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fred2/freddoc.cpp b/fred2/freddoc.cpp index bb1803fe129..ae5910b0ade 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -326,7 +326,10 @@ bool CFREDDoc::load_mission(const char *pathname, int flags) { // double check the used pool is empty for (j = 0; j < weapon_info_size(); j++) { if (!Team_data[i].do_not_validate && used_pool[j] != 0) { - Warning(LOCATION, "%s is used in wings of team %d but was not in the loadout. Fixing now", Weapon_info[j].name, i + 1); + // suppress the warning when importing an X-Wing mission, since this is as good a place as any to fix the loadout + if (!(flags & MPF_IMPORT_XWI)) { + Warning(LOCATION, "%s is used in wings of team %d but was not in the loadout. Fixing now", Weapon_info[j].name, i + 1); + } // add the weapon as a new entry Team_data[i].weaponry_pool[Team_data[i].num_weapon_choices] = j; From 64b54d1022940631fc90ab099b7ec5032d9d7151 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 1 Apr 2024 17:58:07 -0400 Subject: [PATCH 061/466] Start writing apply and reject methods --- .../mission/dialogs/VariableDialogModel.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index b40a90689f5..d30af735aff 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1,7 +1,26 @@ #include "VariableDialogModel.h" +#include "parse/sexp.h" +#include "parse/sexp_container.h" + VariableDialogModel::VariableDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { initializeData(); +} + +void VariableDialogModel::reject() +{ + _variableItems.clear(); + _containerItems.clear(); +} + +void VariableDialogModel::apply() +{ + memset(Sexp_variables, 0, MAX_SEXP_VARIABLES * size_of(sexp_variable)); + + for (const auto& variable : _variableItems){ + + } + } \ No newline at end of file From 52cc1f182e41f450cd99129fb4b53c0d1e954706 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 6 Jan 2024 18:13:19 -0500 Subject: [PATCH 062/466] Make sure flags are included --- qtfred/src/mission/dialogs/VariableDialogModel.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index d98797ecc63..94ae5de0f8a 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -11,9 +11,9 @@ namespace dialogs { struct variable_info { SCP_string name = ""; bool string = true; - - int number_values; - SCP_string string_values; + int flags = 0; + int number_value; + SCP_string string_value; }; @@ -21,6 +21,7 @@ struct container_info { SCP_string name = ""; bool map = false; bool string = true; + int flags = 0; SCP_vector keys; SCP_vector number_values; From 7c15adec1ab8014376d6c3590c9f197fe7a3a0df Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 6 Jan 2024 18:38:12 -0500 Subject: [PATCH 063/466] Finish Loop for saving variables --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index d30af735aff..7a1f4a2e9b5 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -19,8 +19,17 @@ void VariableDialogModel::apply() { memset(Sexp_variables, 0, MAX_SEXP_VARIABLES * size_of(sexp_variable)); - for (const auto& variable : _variableItems){ + for (int i = 0; i < static_cast(_variableItems.size()); ++i){ + Sexp_variables[i].type = _variableItems[i].flags; + strcpy_s(Sexp_variables[i].variable_name, _variableItems[i].name.c_str()); + if (_variableItems[i].flags & SEXP_VARIABLE_STRING){ + strcpy_s(Sexp_variables[i].text, _variableItems[i].stringValue); + } else { + strcpy_s(Sexp_variables[i].text, std::to_string(_variableItems[i].numberValue).c_str()) + } } + + } \ No newline at end of file From 7ce1b5f9c5a75136c010d4f852914158c8080838 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 30 Mar 2024 16:06:31 -0400 Subject: [PATCH 064/466] Prepare to sync 2024-03-30 Get the codebase into a state where it can be automatically synced with the FSO code. This commit will be rolled back after the sync. --- code/mission/missionparse.h | 1 - 1 file changed, 1 deletion(-) diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index 0bb5e83720f..907bcfe9604 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -60,7 +60,6 @@ extern bool check_for_23_3_data(); // mission parse flags used for parse_mission() to tell what kind of information to get from the mission file #define MPF_ONLY_MISSION_INFO (1 << 0) #define MPF_IMPORT_FSM (1 << 1) -#define MPF_IMPORT_XWI (1 << 2) // bitfield definitions for missions game types #define OLD_MAX_GAME_TYPES 4 // needed for compatibility From 687229544e07184bceed3a1e6251c7f21dfe9841 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 30 Mar 2024 17:30:11 -0400 Subject: [PATCH 065/466] Revert "Prepare to sync 2024-03-30" This reverts commit 7ce1b5f9c5a75136c010d4f852914158c8080838 (PR #25), with appropriate conflicts resolved. --- code/mission/missionparse.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index 6af438b2e58..3e40e42f32b 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -60,7 +60,8 @@ extern bool check_for_23_3_data(); // mission parse flags used for parse_mission() to tell what kind of information to get from the mission file #define MPF_ONLY_MISSION_INFO (1 << 0) #define MPF_IMPORT_FSM (1 << 1) -#define MPF_FAST_RELOAD (1 << 2) // skip clearing some stuff so we can load the mission faster (usually since it's the same mission) +#define MPF_IMPORT_XWI (1 << 2) +#define MPF_FAST_RELOAD (1 << 3) // skip clearing some stuff so we can load the mission faster (usually since it's the same mission) // bitfield definitions for missions game types #define OLD_MAX_GAME_TYPES 4 // needed for compatibility From 286a96e2c3cdd6dbee98429a21dcdbdcdd044700 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 6 Apr 2024 22:25:48 -0400 Subject: [PATCH 066/466] save progress --- .../mission/dialogs/VariableDialogModel.cpp | 245 +++++++++++++++++- .../src/mission/dialogs/VariableDialogModel.h | 106 +++++++- qtfred/src/ui/dialogs/VariableDialog.cpp | 74 ++---- qtfred/src/ui/dialogs/VariableDialog.h | 2 + 4 files changed, 372 insertions(+), 55 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 7a1f4a2e9b5..a71217e9dd5 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -2,6 +2,9 @@ #include "parse/sexp.h" #include "parse/sexp_container.h" +namespace fso { +namespace fred { +namespace dialogs { VariableDialogModel::VariableDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) @@ -15,6 +18,7 @@ void VariableDialogModel::reject() _containerItems.clear(); } + void VariableDialogModel::apply() { memset(Sexp_variables, 0, MAX_SEXP_VARIABLES * size_of(sexp_variable)); @@ -30,6 +34,245 @@ void VariableDialogModel::apply() } } + // TODO! containers + + +} + +// true on string, false on number +bool VariableDialogModel::getVariableType(SCP_string name) +{ + return (auto variable = lookupVariable(name)) ? (variable->string) : false; +} + +bool VariableDialogModel::getVariableNetworkStatus(SCP_string name) +{ + // TODO! figure out the flag combination for network variable. + return (auto variable = lookupVariable(name)) ? (variable->string) : false; +} + + + +// 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) +int VariableDialogModel::getVariableOnMissionCloseOrCompleteFlag(SCP_string name) +{ + return (auto variable = lookupVariable(name)) ? (variable->flags) : 0; +} + + +bool VariableDialogModel::getVariableEternalFlag(SCP_string name) +{ + // TODO! figure out correct value for retrieving eternal. + return (auto variable = lookupVariable(name)) ? (variable->flags) : false; +} + + +SCP_string VariableDialogModel::getVariableStringValue(SCP_string name) +{ + return ((auto variable = lookupVariable(name)) && variable->string) ? (variable->stringValue) : ""; +} + +int VariableDialogModel::getVariableNumberValue(SCP_string name) +{ + return ((auto variable = lookupVariable(name)) && !variable->string) ? (variable->numberValue) : 0; +} + + + +// TODO! Need a way to clean up references. + +// true on string, false on number +bool VariableDialogModel::setVariableType(SCP_string name, bool string) +{ + auto variable = lookupVariable(name); + + // nothing to change + if (variable->string == string){ + return string; + } + + //TODO! We need a way to detect the number of references, because then we could see if we need to warn + // about references. + + // changing the type here! + // this variable is currently a string + if (variable->string) { + // no risk change, because no string was specified. + if (variable->stringValue == "") { + variable->string = string; + return variable->string; + } else { + SCP_string question; + sprintf(&question, "Changing variable %s to number variable type will make its string value irrelevant. Continue?", variable->name.c_st()); + SCP_string info; + sprintf(&info, "If the string cleanly converts to an integer and a number has not previously been set for this variable, the converted number value will be retained.") + + // if this was a misclick, let the user say so + if (!confirmAction(question)) { + return variable->string; + } + + // if there was no previous number value + if (variable->numberValue == 0){ + try { + variable->numberValue = std::stoi(variable->stringValue); + } + // nothing to do here, because that just means we can't convert. + catch {} + } + + return string; + } + + // this variable is currently a number + } else { + // safe change because there was no number value specified + if (variable->numberValue == 0){ + varaible->string = string; + return variable->string; + } else { + SCP_string question; + sprintf(&question, "Changing variable %s to a string variable type will make the number value irrelevant. Continue?", variable->name.c_st()); + SCP_string info; + sprintf(&info, "If no string value has been previously set for this variable, then the number value specified will be set as the default string value.") + + // if this was a misclick, let the user say so + if (!confirmAction(question)) { + return variable->string; + } + + // if there was no previous string value + if (variable->stringValue == ""){ + sprintf(&variable->stringValue, "%i", variable->numberValue); + } + + return string; + } + } +} + +bool VariableDialogModel::setVariableNetworkStatus(SCP_string name, bool network) +{ + +} + +int VariableDialogModel::setVariableOnMissionCloseOrCompleteFlag(SCP_string name, int flags) +{ + +} + +bool VariableDialogModel::setVariableEternalFlag(SCP_string name, bool eternal) +{ + +} + +SCP_string VariableDialogModel::setVariableStringValue(SCP_string name, SCP_string value) +{ + +} + +int VariableDialogModel::setVariableNumberValue(SCP_string name, int value) +{ + +} + +SCP_string VariableDialogModel::addNewVariable() +{ + +} + +SCP_string VariableDialogModel::changeVariableName(SCP_string oldName, SCP_string newName) +{ + +} + +SCP_string VariableDialogModel::copyVariable(SCP_string name) +{ + +} + + + +// returns whether it succeeded +bool VariableDialogModel::removeVariable(SCP_string) +{ + +} + + +// Container Section + +// true on string, false on number +bool VariableDialogModel::getContainerValueType(SCP_string name) +{ + +} + +// true on list, false on map +bool VariableDialogModel::getContainerListOrMap(SCP_string name) +{ + +} + +bool VariableDialogModel::getContainerNetworkStatus(SCP_string name) +{ + +} + +// 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) +int VariableDialogModel::getContainerOnMissionCloseOrCompleteFlag(SCP_string name) +{ + +} + +bool VariableDialogModel::getContainerEternalFlag(SCP_string name) +{ + +} + + +bool VariableDialogModel::setContainerValueType(SCP_string name, bool type) +{ + +} + +bool VariableDialogModel::setContainerListOrMap(SCP_string name, bool list) +{ + +} + +bool VariableDialogModel::setContainerNetworkStatus(SCP_string name, bool network) +{ +} + +int VariableDialogModel::setContainerOnMissionCloseOrCompleteFlag(SCP_string name, int flags) +{ + +} + +bool VariableDialogModel::setContainerEternalFlag(SCP_string name, bool eternal) +{ + +} + +SCP_string VariableDialogModel::addContainer() +{ + +} + +SCP_string VariableDialogModel::changeContainerName(SCP_string oldName, SCP_string newName) +{ + +} + +bool VariableDialogModel::removeContainer(SCP_string name) +{ + +} + + -} \ No newline at end of file +} // dialogs +} // fred +} // fso diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 94ae5de0f8a..92a1eb7ab1d 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -12,8 +12,8 @@ struct variable_info { SCP_string name = ""; bool string = true; int flags = 0; - int number_value; - SCP_string string_value; + int numberValue = 0; + SCP_string stringValue = ""; }; @@ -22,18 +22,66 @@ struct container_info { bool map = false; bool string = true; int flags = 0; - + SCP_vector keys; - SCP_vector number_values; - SCP_vector number_values; + SCP_vector numberValues; + SCP_vector stringValues; }; class VariableDialogModel : public AbstractDialogModel { public: VariableDialogModel(QObject* parent, EditorViewport* viewport); - void changeSelection(SCP_string name, bool container){} - void changeName(SCP_string oldName, SCP_string newName, bool container) + // true on string, false on number + bool getVariableType(SCP_string name); + bool getVariableNetworkStatus(SCP_string name); + // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) + int getVariableOnMissionCloseOrCompleteFlag(SCP_string name); + bool getVariableEternalFlag(SCP_string name); + + SCP_string getVariableStringValue(SCP_string name); + int getVariableNumberValue(SCP_string name); + + // !! Note an innovation: when getting a request to set a value, + // this model will return the value that sticks and then will overwrite + // the value in the dialog. This means that we don't have to have the UI + // repopulate the whole editor on each change. + + // true on string, false on number + bool setVariableType(SCP_string name, bool string); + bool setVariableNetworkStatus(SCP_string name, bool network); + int setVariableOnMissionCloseOrCompleteFlag(SCP_string name, int flags); + bool setVariableEternalFlag(SCP_string name, bool eternal); + + SCP_string setVariableStringValue(SCP_string name, SCP_string value); + int setVariableNumberValue(SCP_string name, int value); + + SCP_string addNewVariable(); + SCP_string changeVariableName(SCP_string oldName, SCP_string newName); + SCP_string copyVariable(SCP_string name); + // returns whether it succeeded + bool removeVariable(SCP_string); + + // Container Section + + // true on string, false on number + bool getContainerValueType(SCP_string name); + // true on list, false on map + bool getContainerListOrMap(SCP_string name); + bool getContainerNetworkStatus(SCP_string name); + // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) + int getContainerOnMissionCloseOrCompleteFlag(SCP_string name); + bool getContainerEternalFlag(SCP_string name); + + bool setContainerValueType(SCP_string name, bool type); + bool setContainerListOrMap(SCP_string name, bool list); + bool setContainerNetworkStatus(SCP_string name, bool network); + int setContainerOnMissionCloseOrCompleteFlag(SCP_string name, int flags); + bool setContainerEternalFlag(SCP_string name, bool eternal); + + SCP_string addContainer(); + SCP_string changeContainerName(SCP_string oldName, SCP_string newName); + bool removeContainer(SCP_string name); bool apply() override; void reject() override; @@ -41,8 +89,52 @@ class VariableDialogModel : public AbstractDialogModel { private: SCP_vector _variableItems; SCP_vector _containerItems; + + const variable_info* lookupVariable(SCP_string name){ + for (int x = 0; x < static_cast(_varaibleItems.size()); ++x){ + if (_varaibleItems[x].name == name){ + return &_varaibleItems[x]; + } + } + + return nullptr; + } + + const container_info* lookupContainer(SCP_string name){ + for (int x = 0; x < static_cast(_containerItems.size()); ++x){ + if (_containerItems[x].name == name){ + return &_containerItems[x]; + } + } + + return nullptr; + } + + // many of the controls in this editor can lead to drastic actions, so this will be very useful. + const bool confirmAction(SCP_string question, SCP_string informativeText) + { + QMessageBox msgBox; + msgBox.setText(question.c_str()); + msgBox.setInformativeText(informativeText.C_str()); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Cancel); + int ret = msgBox.exec(); + + switch (ret) { + case QMessageBox::Yes: + return true; + break; + case QMessageBox::Cancel: + return false; + break; + default: + UNREACHABLE("Bad return value from confirmation message box in the Loadout dialog editor."); + break; + } + } }; } // namespace dialogs } // namespace fred } // namespace fso + diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index a7b95db8844..6debb7c4403 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -129,53 +129,33 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) &VariableDialog::onSetContainerAsEternalCheckboxClicked); } - void VariableDialog::onVariablesTableUpdated() {} - void VariableDialog::onContainersTableUpdated() {} - void VariableDialog::onContainerContentsTableUpdated() {} - void VariableDialog::onAddVariableButtonPressed() {} - void VariableDialog::onDeleteVariableButtonPressed() {} - void VariableDialog::onSetVariableAsStringRadioSelected() {} - void VariableDialog::onSetVariableAsNumberRadioSelected() {} - void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() {} - void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() {} - void VariableDialog::onSaveVariableAsEternalCheckboxClicked() {} - - void VariableDialog::onAddContainerButtonPressed() {} - void VariableDialog::onDeleteContainerButtonPressed() {} - void VariableDialog::onSetContainerAsMapRadioSelected() {} - void VariableDialog::onSetContainerAsListRadioSelected() {} - void VariableDialog::onSetContainerAsStringRadioSelected() {} - void VariableDialog::onSetContainerAsNumberRadio() {} - void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() {} - void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() {} - void VariableDialog::onNetworkContainerCheckboxClicked() {} - void VariableDialog::onSetContainerAsEternalCheckboxClicked() {} - void VariableDialog::onAddContainerItemButtonPressed() {} - void VariableDialog::onDeleteContainerItemButtonPressed() {} - -/* -containersTable -containerContentsTable -addVariableButton -deleteVariableButton -setVariableAsStringRadio -setVariableAsNumberRadio -networkVariableCheckbox -saveVariableOnMissionCompletedRaio -saveVariableOnMissionCloseRadio -setVariableAsEternalcheckbox - -addContainerButton -deleteContainerButton -setContainerAsMapRadio -setContainerAsListRadio -setContainerAsStringRadio -setContainerAsNumberRadio -saveContainerOnMissionCloseRadio -saveContainerOnMissionCompletedRadio -networkContainerCheckbox -setContainerAsEternalCheckbox - */ +void VariableDialog::onVariablesTableUpdated() {} // could be new name or new value +void VariableDialog::onContainersTableUpdated() {} // could be new name +void VariableDialog::onContainerContentsTableUpdated() {} // could be new key or new value +void VariableDialog::onAddVariableButtonPressed() +{ + + +} +void VariableDialog::onDeleteVariableButtonPressed() {} +void VariableDialog::onSetVariableAsStringRadioSelected() {} +void VariableDialog::onSetVariableAsNumberRadioSelected() {} +void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() {} +void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() {} +void VariableDialog::onSaveVariableAsEternalCheckboxClicked() {} + +void VariableDialog::onAddContainerButtonPressed() {} +void VariableDialog::onDeleteContainerButtonPressed() {} +void VariableDialog::onSetContainerAsMapRadioSelected() {} +void VariableDialog::onSetContainerAsListRadioSelected() {} +void VariableDialog::onSetContainerAsStringRadioSelected() {} +void VariableDialog::onSetContainerAsNumberRadio() {} +void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() {} +void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() {} +void VariableDialog::onNetworkContainerCheckboxClicked() {} +void VariableDialog::onSetContainerAsEternalCheckboxClicked() {} +void VariableDialog::onAddContainerItemButtonPressed() {} +void VariableDialog::onDeleteContainerItemButtonPressed() {} diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index f3eec607aa6..600ad5dc994 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -32,6 +32,7 @@ class VariableDialog : public QDialog { void onContainerContentsTableUpdated(); void onAddVariableButtonPressed(); void onDeleteVariableButtonPressed(); + void onCopyVariableButtonPressed(); void onSetVariableAsStringRadioSelected(); void onSetVariableAsNumberRadioSelected(); void onSaveVariableOnMissionCompleteRadioSelected(); @@ -40,6 +41,7 @@ class VariableDialog : public QDialog { void onAddContainerButtonPressed(); void onDeleteContainerButtonPressed(); + void onCopyContainerButtonPressed(); void onSetContainerAsMapRadioSelected(); void onSetContainerAsListRadioSelected(); void onSetContainerAsStringRadioSelected(); From cad4f6980da26d8b695fb3b66bd5e7dc1990f191 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 7 Apr 2024 09:51:00 -0400 Subject: [PATCH 067/466] Save progress --- .../mission/dialogs/VariableDialogModel.cpp | 131 ++++++++++++++++-- .../src/mission/dialogs/VariableDialogModel.h | 18 ++- 2 files changed, 129 insertions(+), 20 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index a71217e9dd5..ae564fafdc4 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -6,6 +6,7 @@ namespace fso { namespace fred { namespace dialogs { + VariableDialogModel::VariableDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { @@ -21,6 +22,9 @@ void VariableDialogModel::reject() void VariableDialogModel::apply() { + // TODO VALIDATE! + // TODO! Look for referenced varaibles and containers. + // We actually can't fully trust what the model says.... memset(Sexp_variables, 0, MAX_SEXP_VARIABLES * size_of(sexp_variable)); for (int i = 0; i < static_cast(_variableItems.size()); ++i){ @@ -47,8 +51,7 @@ bool VariableDialogModel::getVariableType(SCP_string name) bool VariableDialogModel::getVariableNetworkStatus(SCP_string name) { - // TODO! figure out the flag combination for network variable. - return (auto variable = lookupVariable(name)) ? (variable->string) : false; + return (auto variable = lookupVariable(name)) ? (variable->flags & SEXP_VARIABLE_NETWORK > 0) : false; } @@ -86,8 +89,8 @@ bool VariableDialogModel::setVariableType(SCP_string name, bool string) { auto variable = lookupVariable(name); - // nothing to change - if (variable->string == string){ + // nothing to change, or invalid entry + if (!variable || variable->string == string){ return string; } @@ -153,50 +156,153 @@ bool VariableDialogModel::setVariableType(SCP_string name, bool string) bool VariableDialogModel::setVariableNetworkStatus(SCP_string name, bool network) { - + auto variable = lookupVariable(name); + + // nothing to change, or invalid entry + if (!variable){ + return false; + } + + // TODO! Look up setting + // if (network){ + // variable->flags |= LOLFLAG; + // } else { + // variable->flags + // } + return network; } int VariableDialogModel::setVariableOnMissionCloseOrCompleteFlag(SCP_string name, int flags) { + auto variable = lookupVariable(name); + + // nothing to change, or invalid entry + if (!variable){ + return 0; + } + + // TODO! Look up setting + + return flags; } bool VariableDialogModel::setVariableEternalFlag(SCP_string name, bool eternal) { - + auto variable = lookupVariable(name); + + // nothing to change, or invalid entry + if (!variable){ + return false; + } + + // TODO! Look up setting + + + return eternal; } SCP_string VariableDialogModel::setVariableStringValue(SCP_string name, SCP_string value) { + auto variable = lookupVariable(name); + + // nothing to change, or invalid entry + if (!variable || !variable->string){ + return ""; + } + variable->stringValue = value; } int VariableDialogModel::setVariableNumberValue(SCP_string name, int value) { + auto variable = lookupVariable(name); + + // nothing to change, or invalid entry + if (!variable || variable->string){ + return 0; + } + variable->numberValue = value; } SCP_string VariableDialogModel::addNewVariable() { - + variableInfo* variable = nullptr; + int count = 0; + SCP_string name; + + do { + name = ""; + sprintf(&name, "", count); + variable = lookupVariable(); + ++count; + } while (variable != nullptr && count < 50); + + + if (variable){ + return ""; + } + + _variableItems.emplace_back(); + _variableItems.back().name = name; + return name; } SCP_string VariableDialogModel::changeVariableName(SCP_string oldName, SCP_string newName) { - + if (newName == "") { + return ""; + } + + auto variable = lookupVariable(oldName); + + // nothing to change, or invalid entry + if (!variable){ + return ""; + } + + // We cannot have two variables with the same name, but we need to check this somewhere else (like on accept attempt). + variable->name = newName; + return newName; } SCP_string VariableDialogModel::copyVariable(SCP_string name) { - -} + auto variable = lookupVariable(name); + // nothing to change, or invalid entry + if (!variable){ + return ""; + } + int count = 0; + variableInfo* variableCopy = nullptr; + + while (variableCopy == nullptr && count < 50){ + SCP_string newName; + sprintf(&newName, "%s%i", name, count); + variableCopy = lookupVariable(newName); + if (!variableCopy){ + _variableItems.emplace_back(); + _variableItems.back().name = newName; + return newName; + } + } +} // returns whether it succeeded -bool VariableDialogModel::removeVariable(SCP_string) +bool VariableDialogModel::removeVariable(SCP_string name) { - + auto variable = lookupVariable(name); + + // nothing to change, or invalid entry + if (!variable){ + return false; + } + + variable->deleted = true; + return true; } @@ -272,7 +378,6 @@ bool VariableDialogModel::removeContainer(SCP_string name) } - } // dialogs } // fred } // fso diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 92a1eb7ab1d..7b666ccbe67 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -8,8 +8,10 @@ namespace fso { namespace fred { namespace dialogs { -struct variable_info { +struct variableInfo { SCP_string name = ""; + SCP_string original_name = ""; + bool deleted = false; bool string = true; int flags = 0; int numberValue = 0; @@ -17,8 +19,10 @@ struct variable_info { }; -struct container_info { +struct containerInfo { SCP_string name = ""; + SCP_string original_name = ""; + bool deleted = false; bool map = false; bool string = true; int flags = 0; @@ -60,7 +64,7 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string changeVariableName(SCP_string oldName, SCP_string newName); SCP_string copyVariable(SCP_string name); // returns whether it succeeded - bool removeVariable(SCP_string); + bool removeVariable(SCP_string name); // Container Section @@ -87,10 +91,10 @@ class VariableDialogModel : public AbstractDialogModel { void reject() override; private: - SCP_vector _variableItems; - SCP_vector _containerItems; + SCP_vector _variableItems; + SCP_vector _containerItems; - const variable_info* lookupVariable(SCP_string name){ + const variableInfo* lookupVariable(SCP_string name){ for (int x = 0; x < static_cast(_varaibleItems.size()); ++x){ if (_varaibleItems[x].name == name){ return &_varaibleItems[x]; @@ -100,7 +104,7 @@ class VariableDialogModel : public AbstractDialogModel { return nullptr; } - const container_info* lookupContainer(SCP_string name){ + const containerInfo* lookupContainer(SCP_string name){ for (int x = 0; x < static_cast(_containerItems.size()); ++x){ if (_containerItems[x].name == name){ return &_containerItems[x]; From 2827e93b2d8ff7ea6bdb53aef64e3206aecfb6c5 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 7 Apr 2024 16:36:29 -0400 Subject: [PATCH 068/466] Continue writing editor model functions Also, add a few declarations for List and Map contents editing. --- .../mission/dialogs/VariableDialogModel.cpp | 180 ++++++++++++++---- .../src/mission/dialogs/VariableDialogModel.h | 18 +- 2 files changed, 155 insertions(+), 43 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index ae564fafdc4..3b17203bf0c 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -23,9 +23,12 @@ void VariableDialogModel::reject() void VariableDialogModel::apply() { // TODO VALIDATE! - // TODO! Look for referenced varaibles and containers. - // We actually can't fully trust what the model says.... - memset(Sexp_variables, 0, MAX_SEXP_VARIABLES * size_of(sexp_variable)); + // TODO! Look for referenced variables and containers. + // Need a way to clean up references. I'm thinking making some pop ups to confirm replacements created in the editor. + // This means we need a way to count and replace references. + + // So, previously, I was just obliterating and overwriting, but I need to rethink this info. + /*memset(Sexp_variables, 0, MAX_SEXP_VARIABLES * size_of(sexp_variable)); for (int i = 0; i < static_cast(_variableItems.size()); ++i){ Sexp_variables[i].type = _variableItems[i].flags; @@ -37,6 +40,7 @@ void VariableDialogModel::apply() strcpy_s(Sexp_variables[i].text, std::to_string(_variableItems[i].numberValue).c_str()) } } + */ // TODO! containers @@ -46,7 +50,7 @@ void VariableDialogModel::apply() // true on string, false on number bool VariableDialogModel::getVariableType(SCP_string name) { - return (auto variable = lookupVariable(name)) ? (variable->string) : false; + return (auto variable = lookupVariable(name)) ? (variable->string) : true; } bool VariableDialogModel::getVariableNetworkStatus(SCP_string name) @@ -59,14 +63,26 @@ bool VariableDialogModel::getVariableNetworkStatus(SCP_string name) // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) int VariableDialogModel::getVariableOnMissionCloseOrCompleteFlag(SCP_string name) { - return (auto variable = lookupVariable(name)) ? (variable->flags) : 0; + auto variable = lookupVariable(name); + + if (!variable) { + return 0; + } + + int returnValue = 0; + + if (variable->flags & SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE) + returnValue = 2; + else if (variable->flags & SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS) + returnValue = 1; + + return returnValue; } bool VariableDialogModel::getVariableEternalFlag(SCP_string name) { - // TODO! figure out correct value for retrieving eternal. - return (auto variable = lookupVariable(name)) ? (variable->flags) : false; + return (auto variable = lookupVariable(name)) ? (variable->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE > 0) : false; } @@ -82,8 +98,6 @@ int VariableDialogModel::getVariableNumberValue(SCP_string name) -// TODO! Need a way to clean up references. - // true on string, false on number bool VariableDialogModel::setVariableType(SCP_string name, bool string) { @@ -94,10 +108,8 @@ bool VariableDialogModel::setVariableType(SCP_string name, bool string) return string; } - //TODO! We need a way to detect the number of references, because then we could see if we need to warn - // about references. - // changing the type here! + // Here we change the variable type! // this variable is currently a string if (variable->string) { // no risk change, because no string was specified. @@ -131,7 +143,7 @@ bool VariableDialogModel::setVariableType(SCP_string name, bool string) } else { // safe change because there was no number value specified if (variable->numberValue == 0){ - varaible->string = string; + variable->string = string; return variable->string; } else { SCP_string question; @@ -163,12 +175,11 @@ bool VariableDialogModel::setVariableNetworkStatus(SCP_string name, bool network return false; } - // TODO! Look up setting - // if (network){ - // variable->flags |= LOLFLAG; - // } else { - // variable->flags - // } + if (!(variable->flags & SEXP_VARIABLE_NETWORK) && network){ + variable->flags |= SEXP_VARIABLE_NETWORK; + } else { + variable->flags &= ~SEXP_VARIABLE_NETWORK; + } return network; } @@ -177,13 +188,19 @@ int VariableDialogModel::setVariableOnMissionCloseOrCompleteFlag(SCP_string name auto variable = lookupVariable(name); // nothing to change, or invalid entry - if (!variable){ + if (!variable || flags < 0 || flags > 2){ return 0; } - // TODO! Look up setting + if (flags == 0) { + variable->flags &= ~(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS | SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + } else if (flags == 1) { + variable->flags &= ~(SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + variable->flags |= SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS; + } else { + variable->flags |= (SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS | SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + } - return flags; } @@ -196,8 +213,11 @@ bool VariableDialogModel::setVariableEternalFlag(SCP_string name, bool eternal) return false; } - // TODO! Look up setting - + if (eternal) { + variable->flags |= SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } else { + variable->flags &= ~SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } return eternal; } @@ -229,15 +249,15 @@ int VariableDialogModel::setVariableNumberValue(SCP_string name, int value) SCP_string VariableDialogModel::addNewVariable() { variableInfo* variable = nullptr; - int count = 0; + int count = 1; SCP_string name; do { name = ""; - sprintf(&name, "", count); + sprintf(&name, "", count); variable = lookupVariable(); ++count; - } while (variable != nullptr && count < 50); + } while (variable != nullptr && count < 51); if (variable){ @@ -276,19 +296,36 @@ SCP_string VariableDialogModel::copyVariable(SCP_string name) return ""; } - int count = 0; - variableInfo* variableCopy = nullptr; + int count = 1; + variableInfo* variableSearch; - while (variableCopy == nullptr && count < 50){ + do { SCP_string newName; - sprintf(&newName, "%s%i", name, count); - variableCopy = lookupVariable(newName); - if (!variableCopy){ + sprintf(&newName, "%s_copy%i", name, count); + variableSearch = lookupVariable(newName); + + // open slot found! + if (!variableSearch){ + // create the new entry in the model _variableItems.emplace_back(); - _variableItems.back().name = newName; + + // and set everything as a copy from the original, except original name and deleted. + auto& newVariable = _variableItems.back(); + newVariable.name = newName; + newVariable.flags = variable.flags; + newVariable.string = variable.string; + + if (newVariable.string) { + newVariable.stringValue = variable.stringValue; + } else { + newVariable.numberValue = variable.numberValue; + } + return newName; } - } + } while (variableSearch != nullptr && count < 51); + + return ""; } // returns whether it succeeded @@ -301,6 +338,12 @@ bool VariableDialogModel::removeVariable(SCP_string name) return false; } + SCP_string question = "Are you sure you want to delete this variable? Any references to it will have to be replaced." + SCP_string info = ""; + if (!confirmAction(question, info)){ + return false; + } + variable->deleted = true; return true; } @@ -311,29 +354,40 @@ bool VariableDialogModel::removeVariable(SCP_string name) // true on string, false on number bool VariableDialogModel::getContainerValueType(SCP_string name) { - + return (auto container = lookupContainer(name)) ? container->string : true; } // true on list, false on map bool VariableDialogModel::getContainerListOrMap(SCP_string name) { - + return (auto contaner = lookupContainer(name)) ? container->list : true; } bool VariableDialogModel::getContainerNetworkStatus(SCP_string name) { - + return (auto container = lookupContainer(name)) ? (container->flags & SEXP_VARIABLE_NETWORK > 0) : false; } // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) int VariableDialogModel::getContainerOnMissionCloseOrCompleteFlag(SCP_string name) { - + auto container = lookupContainer(name); + + if (!container) { + return 0; + } + + if (container->flags & SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE) + return 2; + else if (container->flags & SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS) + return 1; + else + return 0; } bool VariableDialogModel::getContainerEternalFlag(SCP_string name) { - + return (auto container = lookupContainer(name)) ? (container->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE > 0) : false; } @@ -349,17 +403,59 @@ bool VariableDialogModel::setContainerListOrMap(SCP_string name, bool list) bool VariableDialogModel::setContainerNetworkStatus(SCP_string name, bool network) { - + auto container = lookupContainer(name); + + // nothing to change, or invalid entry + if (!container){ + return false; + } + + if (network) { + container->flags |= SEXP_VARIABLE_NETWORK; + } else { + container->flags &= ~SEXP_VARIABLE_NETWORK; + } + + return network; } int VariableDialogModel::setContainerOnMissionCloseOrCompleteFlag(SCP_string name, int flags) { + auto container = lookupContainer(name); + + // nothing to change, or invalid entry + if (!container || flags < 0 || flags > 2){ + return 0; + } + + if (flags == 0) { + container->flags &= ~(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS | SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + } else if (flags == 1) { + container->flags &= ~(SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + container->flags |= SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS; + } else { + container->flags |= (SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS | SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + } + return flags; } bool VariableDialogModel::setContainerEternalFlag(SCP_string name, bool eternal) { - + auto container = lookupContainer(name); + + // nothing to change, or invalid entry + if (!container){ + return false; + } + + if (eternal) { + container->flags |= SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } else { + container->flags &= ~SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } + + return eternal; } SCP_string VariableDialogModel::addContainer() diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 7b666ccbe67..cae2762ef7b 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -23,7 +23,7 @@ struct containerInfo { SCP_string name = ""; SCP_string original_name = ""; bool deleted = false; - bool map = false; + bool list = false; bool string = true; int flags = 0; @@ -87,6 +87,22 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string changeContainerName(SCP_string oldName, SCP_string newName); bool removeContainer(SCP_string name); + SCP_string addListItem(SCP_string containerName); + SCP_string addMapItem(SCP_string ContainerName); + + SCP_string copyStringListItem(SCP_string containerName, int index); + bool removeStringListItem(SCP_string containerName, int index); + int copyIntegerListItem(SCP_string containerName, int index); + int removeIntegerListItem(SCP_string containerName, int index); + + SCP_string copyMapItem(SCP_string containerName, SCP_string key); + bool removeMapItem(SCP_string containerName, SCP_string key); + + SCP_string replaceMapItemKey(SCP_string containerName, SCP_string oldKey, SCP_string newKey); + SCP_string changeMapItemStringValue(SCP_string containerName, SCP_string key, SCP_string newValue); + SCP_string changeMapItemNumberValue(SCP_string containerName, SCP_string key, int newValue); + + bool apply() override; void reject() override; From 5575ab4c950482b7ec2370dbd6fe979ea87f4bd7 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 7 Apr 2024 23:37:25 -0400 Subject: [PATCH 069/466] Finish setContainerValueType And add stubs for other model functions we'll need --- .../mission/dialogs/VariableDialogModel.cpp | 131 +++++++++++++++++- 1 file changed, 130 insertions(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 3b17203bf0c..de164f78234 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -393,9 +393,84 @@ bool VariableDialogModel::getContainerEternalFlag(SCP_string name) bool VariableDialogModel::setContainerValueType(SCP_string name, bool type) { - + auto container = lookupContainer(name); + + if (!container){ + return true; + } + + if (container->string == type){ + return container->string; + } + + if ((container->string && container->stringValues.empty()) || (!container->string && container->numberValues.empty())){ + container->string = type; + return container->type; + } + + // if the other list is not empty, then just convert. No need to confirm. + // The values will be there if they decide to switch back. + if (container->string && !container->numberValues.empty()){ + + container->string = type; + return container->string; + + } else if (!container->string && !container->stringValues.empty()){ + + container->string = type; + return container->string; + } + + // so when the other list *is* empty, then we can attempt to copy values. + if (container->string && container->numberValues.empty()){ + + SCP_string question = "Do you want to attempt conversion of these string values to number values?"; + SCP_string info = "Your string values will still be there if you convert this container back to string type."; + + if (confirmAction(question, info)) { + + bool transferable = true; + SCP_vector numbers; + + for (const auto& item : container->stringValues){ + try { + numbers.push_back(stoi(item)); + } + catch { + transferable = false; + break; + } + } + + if (transferable){ + container->numberValues = std::move(numbers); + } + } + + // now that we've handled value conversion, convert the container + container->string = type; + return container->string; + } else if (!container->string && container->stringValues.empty()){ + + SCP_string question = "Do you want to convert these number values to string values?"; + SCP_string info = "Your number values will still be there if you convert this container back to number type."; + + if (confirmAction(question, info)) { + for (const auto& item : container->numberValues){ + container->stringValues.emplace_back(item); + } + } + + // now that we've handled value conversion, convert the container + container->string = type; + return container->string; + } + + // we shouldn't get here, but if we do return the current value because that's what the model thinks, anyway. + return container->string; } +// this is the most complicated function. bool VariableDialogModel::setContainerListOrMap(SCP_string name, bool list) { @@ -473,6 +548,60 @@ bool VariableDialogModel::removeContainer(SCP_string name) } +SCP_string VariableDialogModel::addListItem(SCP_string containerName) +{ + +} + +SCP_string VariableDialogModel::addMapItem(SCP_string ContainerName) +{ + +} + +SCP_string VariableDialogModel::copyStringListItem(SCP_string containerName, int index) +{ + +} + +bool VariableDialogModel::removeStringListItem(SCP_string containerName, int index) +{ + +} + +int VariableDialogModel::copyIntegerListItem(SCP_string containerName, int index) +{ + +} + +int VariableDialogModel::removeIntegerListItem(SCP_string containerName, int index) +{ + +} + +SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string key) +{ + +} + +bool VariableDialogModel::removeMapItem(SCP_string containerName, SCP_string key) +{ + +} + +SCP_string VariableDialogModel::replaceMapItemKey(SCP_string containerName, SCP_string oldKey, SCP_string newKey) +{ + +} + +SCP_string VariableDialogModel::changeMapItemStringValue(SCP_string containerName, SCP_string key, SCP_string newValue) +{ + +} + +SCP_string VariableDialogModel::changeMapItemNumberValue(SCP_string containerName, SCP_string key, int newValue) +{ + +} } // dialogs } // fred From aea39664bab8eab54b5e171031b5be617383e3a6 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 8 Apr 2024 14:18:14 -0400 Subject: [PATCH 070/466] Add logic for addcontainer and a few other functions --- .../mission/dialogs/VariableDialogModel.cpp | 95 +++++++++++++++---- .../src/mission/dialogs/VariableDialogModel.h | 8 +- 2 files changed, 82 insertions(+), 21 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index de164f78234..5e3b3d57ef0 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -255,7 +255,7 @@ SCP_string VariableDialogModel::addNewVariable() do { name = ""; sprintf(&name, "", count); - variable = lookupVariable(); + variable = lookupVariable(name); ++count; } while (variable != nullptr && count < 51); @@ -470,7 +470,7 @@ bool VariableDialogModel::setContainerValueType(SCP_string name, bool type) return container->string; } -// this is the most complicated function. +// This is the most complicated function, because we need to query the user on what they want to do if the had already entered data. bool VariableDialogModel::setContainerListOrMap(SCP_string name, bool list) { @@ -535,52 +535,115 @@ bool VariableDialogModel::setContainerEternalFlag(SCP_string name, bool eternal) SCP_string VariableDialogModel::addContainer() { - + containerInfo* container = nullptr; + int count = 1; + SCP_string name; + + do { + name = ""; + sprintf(&name, "", count); + container = lookupContainer(name); + ++count; + } while (container != nullptr && count < 51); + + if (container){ + return ""; + } + + _containerItems.emplace_back(); + _containerItems.back().name = name; + return name; } SCP_string VariableDialogModel::changeContainerName(SCP_string oldName, SCP_string newName) { - + if (newName == "") { + return ""; + } + + auto container = lookupContainer(oldName); + + // nothing to change, or invalid entry + if (!container){ + return ""; + } + + // We cannot have two containers with the same name, but we need to check this somewhere else (like on accept attempt). + container->name = newName; + return newName; } bool VariableDialogModel::removeContainer(SCP_string name) { - + auto container = lookupContainer(oldName); + + if (!container){ + return false; + } + + container->deleted = true; } SCP_string VariableDialogModel::addListItem(SCP_string containerName) { + auto container = lookupContainer(containerName); + if (!container){ + return ""; + } + + if (container->string) { + container->stringValues.emplace_back("New_Item"); + return container->stringValues.back(); + } else { + container->numberValues.push_back(0); + return "0"; + } } -SCP_string VariableDialogModel::addMapItem(SCP_string ContainerName) +std::pair VariableDialogModel::addMapItem(SCP_string ContainerName) { - + } -SCP_string VariableDialogModel::copyStringListItem(SCP_string containerName, int index) +SCP_string VariableDialogModel::copyListItem(SCP_string containerName, int index) { + auto container = lookupContainer(containerName); -} + if (!container || index < 0 || (cotainer->string && index >= static_cast(contaner->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ + return ""; + } -bool VariableDialogModel::removeStringListItem(SCP_string containerName, int index) -{ + if (container->string) { + container->stringValues.push_back(container->stringValues[index]); + return container->stringValues.back(); + } else { + container->numberValues.push_back(container->numberValues[index]); + return "0"; + } } -int VariableDialogModel::copyIntegerListItem(SCP_string containerName, int index) +bool VariableDialogModel::removeListItem(SCP_string containerName, int index) { + auto container = lookupContainer(containerName); -} + if (!container || index < 0 || (cotainer->string && index >= static_cast(contaner->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ + return false; + } -int VariableDialogModel::removeIntegerListItem(SCP_string containerName, int index) -{ + // Most efficient, given the situation (single deletions) + if (container->string) { + container->stringValues.erase(container->stringValues.begin() + index); + } else { + container->numberValues.erase(container->numberValues.begin() + index); + } } SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string key) { - + for (const auto& ) } bool VariableDialogModel::removeMapItem(SCP_string containerName, SCP_string key) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index cae2762ef7b..4afd81a62d9 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -88,13 +88,11 @@ class VariableDialogModel : public AbstractDialogModel { bool removeContainer(SCP_string name); SCP_string addListItem(SCP_string containerName); - SCP_string addMapItem(SCP_string ContainerName); - SCP_string copyStringListItem(SCP_string containerName, int index); - bool removeStringListItem(SCP_string containerName, int index); - int copyIntegerListItem(SCP_string containerName, int index); - int removeIntegerListItem(SCP_string containerName, int index); + SCP_string copyListItem(SCP_string containerName, int index); + bool removeListItem(SCP_string containerName, int index); + SCP_string addMapItem(SCP_string ContainerName); SCP_string copyMapItem(SCP_string containerName, SCP_string key); bool removeMapItem(SCP_string containerName, SCP_string key); From fa27b8d133072f21e78836258b9d56aa8351af6a Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 10 Apr 2024 01:38:48 -0400 Subject: [PATCH 071/466] MOAR FUNCTIONS! --- .../mission/dialogs/VariableDialogModel.cpp | 140 +++++++++++++++++- .../src/mission/dialogs/VariableDialogModel.h | 2 +- 2 files changed, 139 insertions(+), 3 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 5e3b3d57ef0..b062b7bbdb6 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -641,29 +641,165 @@ bool VariableDialogModel::removeListItem(SCP_string containerName, int index) } -SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string key) +SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string keyIn) { - for (const auto& ) + auto container = lookupContainer(containerName); + + if (!container) { + return ""; + } + + for (int x = 0; x < static_cast(container->keys.size()); ++x) { + if (container->keys[x] == keyIn){ + if (container->string){ + if (x < static_cast(container->stringValues.size())){ + SCP_string copyValue = container->stringValues[x]; + SCP_string newKey; + int size = static_cast(container->keys.size()); + sprintf(&newKey, "key%i", size); + + bool found = false; + + do { + found = false; + for (y = 0; y < static_cast(container->keys.size()); ++y){ + if (container->keys[y] == newKey) { + found = true; + break; + } + } + + // attempt did not work, try next number + if (found) { + ++size; + newKey = ""; + sprintf(&newKey, "key%i", size); + } + + } while (found && size < static_cast(container->keys.size()) + 100) + + // we could not generate a new key .... somehow. + if (found){ + return ""; + } + + container->keys.push_back(newKey); + container->keys.push_back(copyValue); + + return std::make_pair(newKey, copyValue); + + } else { + return ""; + } + } else { + // TODO! Make the number value version. + } + + + + } + } } +// it's really because of this feature that we need data to only be in one or the other vector for maps. +// If we attempted to maintain data automatically and there was a deletion, deleting the data in +// both of the map's data vectors might be undesired, and not deleting takes the map immediately +// out of sync. Also, just displaying both data sets would be misleading. +// We just need to tell the user that the data cannot be maintained. bool VariableDialogModel::removeMapItem(SCP_string containerName, SCP_string key) { + auto container = lookupContainer(containerName); + if (!container){ + return false; + } + + for (int x = 0; x < static_cast(container->keys.size()); ++x) { + if (container->keys[x] == key) { + if ((container->string && x < static_cast(container->stringValues.size())) { + container->stringValues.erase(container->stringValues.begin() + x); + } else if (!container->string && x < static_cast(container->numberValues.size()))){ + container->numberValues.erase(container->numberValues.begin() + x); + } else { + return false; + } + + // if we get here, we've succeeded and it's time to bug out + container->keys.erase(container->keys.begin() + x); + // "I'm outta here!" + return true; + } + } + + // NO SPRINGS!!! HEHEHEHEHE + return false; } SCP_string VariableDialogModel::replaceMapItemKey(SCP_string containerName, SCP_string oldKey, SCP_string newKey) { + auto container = lookupContainer(containerName); + if (!container){ + return ""; + } + + for (auto& key : container->keys){ + if (key == oldKey) { + key = newKey; + return newKey; + } + } + + // Failure + return oldKey; } SCP_string VariableDialogModel::changeMapItemStringValue(SCP_string containerName, SCP_string key, SCP_string newValue) { + auto container = lookupContainer(containerName); + + if (!container || !container->string){ + return ""; + } + + for (int x = 0; x < static_cast(container->keys); ++x){ + if (container->keys[x] == oldKey) { + if (x < static_cast(container->stringValues.size())){ + container->stringValues[x] = newValue; + return newValue; + } else { + return ""; + } + } + } + // Failure + return ""; } SCP_string VariableDialogModel::changeMapItemNumberValue(SCP_string containerName, SCP_string key, int newValue) { + auto container = lookupContainer(containerName); + + if (!container || !container->string){ + return ""; + } + for (int x = 0; x < static_cast(container->keys); ++x){ + if (container->keys[x] == oldKey) { + if (x < static_cast(container->numberValues.size())){ + container->numberValues[x] = newValue; + SCP_string returnValue; + sprintf(&returnValue, "%i", newValue) + return returnValue; + } else { + return ""; + } + } + } + + // Failure + return ""; } } // dialogs diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 4afd81a62d9..3f6514f7398 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -93,7 +93,7 @@ class VariableDialogModel : public AbstractDialogModel { bool removeListItem(SCP_string containerName, int index); SCP_string addMapItem(SCP_string ContainerName); - SCP_string copyMapItem(SCP_string containerName, SCP_string key); + std::pair copyMapItem(SCP_string containerName, SCP_string key); bool removeMapItem(SCP_string containerName, SCP_string key); SCP_string replaceMapItemKey(SCP_string containerName, SCP_string oldKey, SCP_string newKey); From d665c618c1feac48e5c13b596115100d01d7454f Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 11 Apr 2024 15:12:16 -0400 Subject: [PATCH 072/466] Starting progress on initializeData And container functions --- .../mission/dialogs/VariableDialogModel.cpp | 179 +++++++++++++++++- .../src/mission/dialogs/VariableDialogModel.h | 12 +- 2 files changed, 185 insertions(+), 6 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index b062b7bbdb6..0f0c858eb0a 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -47,6 +47,50 @@ void VariableDialogModel::apply() } +void initializeData() +{ + _variableItems.clear(); + _containerItems.clear(); + + for (int i = 0; i < static_cast(_variableItems.size()); ++i){ + if (strlen(Sexp_variables[i].text)) { + _variableItems.emplace_back(); + auto& item = _variableItems.back(); + item.name = Sexp_variables[i].variable_name; + item.originalName = item.name; + + if (Sexp_variables[i].type & SEXP_VARIABLE_STRING) { + item.string = true; + item.stringValue = Sexp_variables[i].text; + item.numberValue = 0; + } else { + item.string = false; + + Sexp_variables[i].text; + try { + item.numberValue = std::stoi(Sexp_variables[i].text) + } + catch{ + item.numberValue = 0; + // TODO! Warning popup + } + + item.stringValue = ""; + } + } + } + + const auto& containers = get_all_sexp_containers(); + + for (const auto& container : containers) { + if + + _containerItems + } +} + + + // true on string, false on number bool VariableDialogModel::getVariableType(SCP_string name) { @@ -684,7 +728,7 @@ SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string } container->keys.push_back(newKey); - container->keys.push_back(copyValue); + container->stringValues.push_back(copyValue); return std::make_pair(newKey, copyValue); @@ -692,11 +736,46 @@ SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string return ""; } } else { - // TODO! Make the number value version. - } + if (x < static_cast(container->numberVales.size())){ + int copyValue = container->numberVales[x]; + SCP_string newKey; + int size = static_cast(container->keys.size()); + sprintf(&newKey, "key%i", size); + + bool found = false; + do { + found = false; + for (y = 0; y < static_cast(container->keys.size()); ++y){ + if (container->keys[y] == newKey) { + found = true; + break; + } + } + + // attempt did not work, try next number + if (found) { + ++size; + newKey = ""; + sprintf(&newKey, "key%i", size); + } + } while (found && size < static_cast(container->keys.size()) + 100) + + // we could not generate a new key .... somehow. + if (found){ + return ""; + } + container->keys.push_back(newKey); + container->numberValues.push_back(copyValue); + + return std::make_pair(newKey, copyValue); + + } else { + return ""; + } + } } } } @@ -802,6 +881,100 @@ SCP_string VariableDialogModel::changeMapItemNumberValue(SCP_string containerNam return ""; } +// These functions should only be called when the container is guaranteed to exist! +const SCP_vector& VariableDialogModel::getMapKeys(SCP_string containerName) +{ + auto container = lookupContainer(containerName); + + if (!container) { + throw std::invalid_argument("getMapKeys() found that container %s does not exist.", containerName.c_str()); + } + + if (container->list) { + throw std::invalid_argument("getMapKeys() found that container %s is not a map.", containerName.c_str()); + } + + return containerName->keys; +} + +// Only call when the container is guaranteed to exist! +const SCP_vector& VariableDialogModel::getStringValues(SCP_string containerName) +{ + auto container = lookupContainer(containerName); + + if (!container) { + throw std::invalid_argument("getStringValues() found that container %s does not exist.", containerName.c_str()); + } + + if (!container->string) { + throw std::invalid_argument("getStringValues() found that container %s does not store strings.", containerName.c_str()); + } + + return containerName->stringValues; +} + +// Only call when the container is guaranteed to exist! +const SCP_vector& VariableDialogModel::getNumberValues(SCP_string containerName) +{ + auto container = lookupContainer(containerName); + + if (!container) { + throw std::invalid_argument("getNumberValues() found that container %s does not exist.", containerName.c_str()); + } + + if (container->string) { + throw std::invalid_argument("getNumberValues() found that container %s does not store numbers.", containerName.c_str()); + } + + return containerName->numberValues; +} + +const SCP_vector VariableDialogModel::getVariableValues() +{ + SCP_vector outStrings; + + for (const auto& item : _variableItems) { + SCP_string notes = ""; + + if (item.deleted) { + notes = "Marked for Deletion"; + } else if (item.originalName == "") { + notes = "New"; + } else if (item.name != item.originalName){ + notes = "Renamed"; + } else if (item.string && item.stringValue = "") { + notes = "Defaulting to empty string"; + } + + outStrings.emplace_back(item.name, (item.string) ? item.stringValue : SCP_string(item.numberValue), notes); + } + + return outStrings; +} + + +const SCP_vector> VarigableDialogModel::getContainerNames() +{ + SCP_vector> outStrings; + + for (const auto& item : _containerItems) { + SCP_string notes = ""; + + if (item.deleted) { + notes = "Marked for Deletion"; + } else if (item.originalName == "") { + notes = "New"; + } else if (item.name != item.originalName){ + notes = "Renamed"; + } + + outStrings.emplace_back(item.name, notes); + } + + return outStrings; +} + + } // dialogs } // fred } // fso diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 3f6514f7398..53c1c451947 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -10,7 +10,7 @@ namespace dialogs { struct variableInfo { SCP_string name = ""; - SCP_string original_name = ""; + SCP_string originalName = ""; bool deleted = false; bool string = true; int flags = 0; @@ -21,9 +21,9 @@ struct variableInfo { struct containerInfo { SCP_string name = ""; - SCP_string original_name = ""; + SCP_string originalName = ""; bool deleted = false; - bool list = false; + bool list = true; bool string = true; int flags = 0; @@ -100,6 +100,12 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string changeMapItemStringValue(SCP_string containerName, SCP_string key, SCP_string newValue); SCP_string changeMapItemNumberValue(SCP_string containerName, SCP_string key, int newValue); + const SCP_vector& getMapKeys(SCP_string containerName); + const SCP_vector& getStringValues(SCP_string containerName); + const SCP_vector& getNumberValues(SCP_string containerName); + + const SCP_vector VariableDialogModel::getVariableValues(); + const SCP_vector> VarigableDialogModel::getContainerNames(); bool apply() override; void reject() override; From de54dea61acc5b8fcdf2beb0b9aacf7e59133541 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 11 Apr 2024 22:29:21 -0400 Subject: [PATCH 073/466] save progress --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 0f0c858eb0a..fe312c07c9c 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -83,9 +83,16 @@ void initializeData() const auto& containers = get_all_sexp_containers(); for (const auto& container : containers) { - if + _containerItems.emplace_back(); + auto& newContainer = _containerItems.back(); + + newContainer.name = container.container_name; + newContainer.originalName = newContainer.name; + newContainer.deleted = false; + + newContainer.string = container.C + newContainer.list = container.is_list; - _containerItems } } From a125b8b3ceb8c5e2ea636c7bada7c50c3c5f7285 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 12 Apr 2024 00:58:37 -0400 Subject: [PATCH 074/466] Fixup the Variables Dialog Add it to the editor menu (with shortcut), and fix it showing up when started --- qtfred/src/ui/FredView.cpp | 2 +- qtfred/src/ui/FredView.h | 2 +- qtfred/ui/FredView.ui | 12 + qtfred/ui/VariableDialog.ui | 747 +++++++++++++++++++----------------- 4 files changed, 414 insertions(+), 349 deletions(-) diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 1ca3e5e7cbf..81001acb97e 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -752,7 +752,7 @@ void FredView::on_actionLoadout_triggered(bool) { auto editorDialog = new dialogs::LoadoutDialog(this, _viewport); editorDialog->show(); } -void FredView::on_actionVariablesAndContainers_triggered(bool) { +void FredView::on_actionVariables_triggered(bool) { auto editorDialog = new dialogs::VariableDialog(this, _viewport); editorDialog->show(); } diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index b11bfbfa0a3..d63095f02dd 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -91,7 +91,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionCommand_Briefing_triggered(bool); void on_actionReinforcements_triggered(bool); void on_actionLoadout_triggered(bool); - void on_actionVariablesAndContainers_triggered(bool); + void on_actionVariables_triggered(bool); void on_actionSelectionLock_triggered(bool enabled); diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index eca5d7c11de..9ffff9762ad 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -166,6 +166,7 @@ + @@ -1474,6 +1475,17 @@ Shift+G + + + &Variables + + + Open Variables and Containers Editor + + + Shift+V + + diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index b3e99e727a6..b85a2fc68a9 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -1,425 +1,478 @@ - fso::fred::dialogs::VariableDialog - + fso::fred::dialogs::VariableEditorDialog + 0 0 - 589 - 608 + 600 + 649 + + + 600 + 640 + + Fiction Viewer Editor - - - - - 0 - 325 - - - - Containers - - - - - 10 - 30 - 231 - 121 - - - - - - - 10 - 160 - 331 - 161 - - - - Contents - - - - - 10 - 30 - 221 - 121 - - - - - - - 240 - 30 - 81 - 121 - + + + + + + QLayout::SetMaximumSize - - - - - Add - - - - - - - - 8 - - - - Delete - - - - - - - - - - 250 - 30 - 81 - 121 - - - - - - Add + + + Qt::Horizontal - + + + 40 + 20 + + + - + - 8 + 12 - Delete + Variables and Containers Editor - - - - - - 360 - 180 - 201 - 141 - - - - Options - - - - - 0 - 20 - 201 - 121 - - - - - - - Save on Mission Close - - - - - - - Save on Mission Completed - - - - - - - Network-Variable - - - - - - - Eternal - - - - - - - - - - 359 - 18 - 201 - 151 - - - - - - Container Type + + + Qt::Horizontal - - - - 0 - 20 - 201 - 52 - - - - - - - Map - - - - - - - List - - - - - - - - - - - Data Type + + + 40 + 20 + - - - - 0 - 20 - 201 - 52 - - - - - - - String - - - - - - - Number - - - - - - + - - - - - - - - 0 - 230 - - - - Variables - - - - - 260 - 30 - 175 - 61 - - - - - - - Add Variable + + + + + + 0 + 380 + + + + Containers + + + + + 10 + 30 + 231 + 131 + + + + + + + 10 + 170 + 341 + 191 + + + + Contents + + + + + 10 + 30 + 221 + 151 + - - - - - Delete Variable and References + + + + 240 + 30 + 91 + 91 + - - - - - - - - 260 - 100 - 301 - 121 - - - - Options - - - - - 0 - 20 - 301 - 101 - - - - - + - + - String + Add Data - + - Number + Copy Data - + + + + 8 + + - Network-Variable + Delete Data - - - + + + + + + 250 + 40 + 97 + 91 + + + + + + + Add Container + + + + + + + Copy Container + + + + + + + + 8 + + + + Delete Container + + + + + + + + + 360 + 20 + 211 + 141 + + + + Persistence Options + + + + + 10 + 20 + 201 + 121 + + + - + Save on Mission Close - + Save on Mission Completed - + Eternal + + + + Network-Variable + + + - - + + + + + + 360 + 170 + 211 + 191 + + + + Type Options + + + + + 10 + 20 + 201 + 152 + + + + + + + Container Type + + + + + + + + + List + + + + + + + Map + + + + + + + + + Key Type + + + + + + + + + Number + + + + + + + String + + + + + + + + + Data Type + + + + + + + + + Number + + + + + + + String + + + + + + + + - - - - - 10 - 30 - 241 - 191 - - - - - - - - - QLayout::SetMaximumSize - - - - - Qt::Horizontal - - + + + + - 40 - 20 + 0 + 230 - - - - - - - 12 - - - - Variables and Containers Editor + + Variables + + + + 260 + 100 + 311 + 121 + + + + Options + + + + + 10 + 20 + 300 + 101 + + + + + + + + + String + + + + + + + Number + + + + + + + Network-Variable + + + + + + + + + + + Save on Mission Close + + + + + + + Save on Mission Completed + + + + + + + Eternal + + + + + + + + + + + + 10 + 30 + 241 + 191 + + + + + + + 270 + 38 + 301 + 51 + + + + + + + Add Variable + + + + + + + Copy Variable + + + + + + + Delete Variable + + + + + - - - - Qt::Horizontal - - - - 40 - 20 - - - - From e1ed5fdd78d4cf532f18e7569f216d939533df68 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 12 Apr 2024 00:59:29 -0400 Subject: [PATCH 075/466] Fixes to allow building Edits were previously done on a machine that could not build or use a linter. --- .../mission/dialogs/VariableDialogModel.cpp | 181 ++++++++++-------- .../src/mission/dialogs/VariableDialogModel.h | 20 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 7 +- qtfred/src/ui/dialogs/VariableDialog.h | 4 +- 4 files changed, 123 insertions(+), 89 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index fe312c07c9c..28b4f09bed7 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -20,7 +20,7 @@ void VariableDialogModel::reject() } -void VariableDialogModel::apply() +bool VariableDialogModel::apply() { // TODO VALIDATE! // TODO! Look for referenced variables and containers. @@ -44,10 +44,10 @@ void VariableDialogModel::apply() // TODO! containers - + return false; } -void initializeData() +void VariableDialogModel::initializeData() { _variableItems.clear(); _containerItems.clear(); @@ -68,9 +68,9 @@ void initializeData() Sexp_variables[i].text; try { - item.numberValue = std::stoi(Sexp_variables[i].text) + item.numberValue = std::stoi(Sexp_variables[i].text); } - catch{ + catch (...) { item.numberValue = 0; // TODO! Warning popup } @@ -90,8 +90,9 @@ void initializeData() newContainer.originalName = newContainer.name; newContainer.deleted = false; - newContainer.string = container.C - newContainer.list = container.is_list; + // TODO FIXME! + newContainer.string = false; + newContainer.list = container.is_list(); } } @@ -101,12 +102,14 @@ void initializeData() // true on string, false on number bool VariableDialogModel::getVariableType(SCP_string name) { - return (auto variable = lookupVariable(name)) ? (variable->string) : true; + auto variable = lookupVariable(name); + return (variable) ? (variable->string) : true; } bool VariableDialogModel::getVariableNetworkStatus(SCP_string name) { - return (auto variable = lookupVariable(name)) ? (variable->flags & SEXP_VARIABLE_NETWORK > 0) : false; + auto variable = lookupVariable(name); + return (variable) ? ((variable->flags & SEXP_VARIABLE_NETWORK) > 0) : false; } @@ -133,18 +136,21 @@ int VariableDialogModel::getVariableOnMissionCloseOrCompleteFlag(SCP_string name bool VariableDialogModel::getVariableEternalFlag(SCP_string name) { - return (auto variable = lookupVariable(name)) ? (variable->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE > 0) : false; + auto variable = lookupVariable(name); + return (variable) ? ((variable->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) > 0) : false; } SCP_string VariableDialogModel::getVariableStringValue(SCP_string name) { - return ((auto variable = lookupVariable(name)) && variable->string) ? (variable->stringValue) : ""; + auto variable = lookupVariable(name); + return (variable && variable->string) ? (variable->stringValue) : ""; } int VariableDialogModel::getVariableNumberValue(SCP_string name) { - return ((auto variable = lookupVariable(name)) && !variable->string) ? (variable->numberValue) : 0; + auto variable = lookupVariable(name); + return (variable && !variable->string) ? (variable->numberValue) : 0; } @@ -169,12 +175,12 @@ bool VariableDialogModel::setVariableType(SCP_string name, bool string) return variable->string; } else { SCP_string question; - sprintf(&question, "Changing variable %s to number variable type will make its string value irrelevant. Continue?", variable->name.c_st()); + sprintf(question, "Changing variable %s to number variable type will make its string value irrelevant. Continue?", variable->name.c_str()); SCP_string info; - sprintf(&info, "If the string cleanly converts to an integer and a number has not previously been set for this variable, the converted number value will be retained.") + sprintf(info, "If the string cleanly converts to an integer and a number has not previously been set for this variable, the converted number value will be retained."); // if this was a misclick, let the user say so - if (!confirmAction(question)) { + if (!confirmAction(question, info)) { return variable->string; } @@ -184,7 +190,7 @@ bool VariableDialogModel::setVariableType(SCP_string name, bool string) variable->numberValue = std::stoi(variable->stringValue); } // nothing to do here, because that just means we can't convert. - catch {} + catch (...) {} } return string; @@ -198,18 +204,18 @@ bool VariableDialogModel::setVariableType(SCP_string name, bool string) return variable->string; } else { SCP_string question; - sprintf(&question, "Changing variable %s to a string variable type will make the number value irrelevant. Continue?", variable->name.c_st()); + sprintf(question, "Changing variable %s to a string variable type will make the number value irrelevant. Continue?", variable->name.c_str()); SCP_string info; - sprintf(&info, "If no string value has been previously set for this variable, then the number value specified will be set as the default string value.") + sprintf(info, "If no string value has been previously set for this variable, then the number value specified will be set as the default string value."); // if this was a misclick, let the user say so - if (!confirmAction(question)) { + if (!confirmAction(question, info)) { return variable->string; } // if there was no previous string value if (variable->stringValue == ""){ - sprintf(&variable->stringValue, "%i", variable->numberValue); + sprintf(variable->stringValue, "%i", variable->numberValue); } return string; @@ -305,7 +311,7 @@ SCP_string VariableDialogModel::addNewVariable() do { name = ""; - sprintf(&name, "", count); + sprintf(name, "", count); variable = lookupVariable(name); ++count; } while (variable != nullptr && count < 51); @@ -352,7 +358,7 @@ SCP_string VariableDialogModel::copyVariable(SCP_string name) do { SCP_string newName; - sprintf(&newName, "%s_copy%i", name, count); + sprintf(newName, "%s_copy%i", name, count); variableSearch = lookupVariable(newName); // open slot found! @@ -363,13 +369,13 @@ SCP_string VariableDialogModel::copyVariable(SCP_string name) // and set everything as a copy from the original, except original name and deleted. auto& newVariable = _variableItems.back(); newVariable.name = newName; - newVariable.flags = variable.flags; - newVariable.string = variable.string; + newVariable.flags = variableSearch->flags; + newVariable.string = variableSearch->string; if (newVariable.string) { - newVariable.stringValue = variable.stringValue; + newVariable.stringValue = variableSearch->stringValue; } else { - newVariable.numberValue = variable.numberValue; + newVariable.numberValue = variableSearch->numberValue; } return newName; @@ -389,7 +395,7 @@ bool VariableDialogModel::removeVariable(SCP_string name) return false; } - SCP_string question = "Are you sure you want to delete this variable? Any references to it will have to be replaced." + SCP_string question = "Are you sure you want to delete this variable? Any references to it will have to be replaced."; SCP_string info = ""; if (!confirmAction(question, info)){ return false; @@ -405,18 +411,21 @@ bool VariableDialogModel::removeVariable(SCP_string name) // true on string, false on number bool VariableDialogModel::getContainerValueType(SCP_string name) { - return (auto container = lookupContainer(name)) ? container->string : true; + auto container = lookupContainer(name); + return (container) ? container->string : true; } // true on list, false on map bool VariableDialogModel::getContainerListOrMap(SCP_string name) { - return (auto contaner = lookupContainer(name)) ? container->list : true; + auto container = lookupContainer(name); + return (container) ? container->list : true; } bool VariableDialogModel::getContainerNetworkStatus(SCP_string name) { - return (auto container = lookupContainer(name)) ? (container->flags & SEXP_VARIABLE_NETWORK > 0) : false; + auto container = lookupContainer(name); + return (container) ? ((container->flags & SEXP_VARIABLE_NETWORK) > 0) : false; } // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) @@ -438,7 +447,8 @@ int VariableDialogModel::getContainerOnMissionCloseOrCompleteFlag(SCP_string nam bool VariableDialogModel::getContainerEternalFlag(SCP_string name) { - return (auto container = lookupContainer(name)) ? (container->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE > 0) : false; + auto container = lookupContainer(name); + return (container) ? ((container->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) > 0) : false; } @@ -456,7 +466,7 @@ bool VariableDialogModel::setContainerValueType(SCP_string name, bool type) if ((container->string && container->stringValues.empty()) || (!container->string && container->numberValues.empty())){ container->string = type; - return container->type; + return container->string; } // if the other list is not empty, then just convert. No need to confirm. @@ -487,7 +497,7 @@ bool VariableDialogModel::setContainerValueType(SCP_string name, bool type) try { numbers.push_back(stoi(item)); } - catch { + catch(...) { transferable = false; break; } @@ -508,7 +518,7 @@ bool VariableDialogModel::setContainerValueType(SCP_string name, bool type) if (confirmAction(question, info)) { for (const auto& item : container->numberValues){ - container->stringValues.emplace_back(item); + container->stringValues.emplace_back(std::to_string(item)); } } @@ -524,7 +534,7 @@ bool VariableDialogModel::setContainerValueType(SCP_string name, bool type) // This is the most complicated function, because we need to query the user on what they want to do if the had already entered data. bool VariableDialogModel::setContainerListOrMap(SCP_string name, bool list) { - + return false; } bool VariableDialogModel::setContainerNetworkStatus(SCP_string name, bool network) @@ -592,7 +602,7 @@ SCP_string VariableDialogModel::addContainer() do { name = ""; - sprintf(&name, "", count); + sprintf(name, "", count); container = lookupContainer(name); ++count; } while (container != nullptr && count < 51); @@ -626,7 +636,7 @@ SCP_string VariableDialogModel::changeContainerName(SCP_string oldName, SCP_stri bool VariableDialogModel::removeContainer(SCP_string name) { - auto container = lookupContainer(oldName); + auto container = lookupContainer(name); if (!container){ return false; @@ -661,7 +671,7 @@ SCP_string VariableDialogModel::copyListItem(SCP_string containerName, int index { auto container = lookupContainer(containerName); - if (!container || index < 0 || (cotainer->string && index >= static_cast(contaner->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ + if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ return ""; } @@ -679,7 +689,7 @@ bool VariableDialogModel::removeListItem(SCP_string containerName, int index) { auto container = lookupContainer(containerName); - if (!container || index < 0 || (cotainer->string && index >= static_cast(contaner->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ + if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ return false; } @@ -692,12 +702,12 @@ bool VariableDialogModel::removeListItem(SCP_string containerName, int index) } -SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string keyIn) +std::pair VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string keyIn) { auto container = lookupContainer(containerName); if (!container) { - return ""; + return std::make_pair("", ""); } for (int x = 0; x < static_cast(container->keys.size()); ++x) { @@ -707,13 +717,13 @@ SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string SCP_string copyValue = container->stringValues[x]; SCP_string newKey; int size = static_cast(container->keys.size()); - sprintf(&newKey, "key%i", size); + sprintf(newKey, "key%i", size); bool found = false; do { found = false; - for (y = 0; y < static_cast(container->keys.size()); ++y){ + for (int y = 0; y < static_cast(container->keys.size()); ++y){ if (container->keys[y] == newKey) { found = true; break; @@ -724,14 +734,14 @@ SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string if (found) { ++size; newKey = ""; - sprintf(&newKey, "key%i", size); + sprintf(newKey, "key%i", size); } - } while (found && size < static_cast(container->keys.size()) + 100) + } while (found && size < static_cast(container->keys.size()) + 100); // we could not generate a new key .... somehow. if (found){ - return ""; + return std::make_pair("", ""); } container->keys.push_back(newKey); @@ -740,20 +750,20 @@ SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string return std::make_pair(newKey, copyValue); } else { - return ""; + return std::make_pair("", ""); } } else { - if (x < static_cast(container->numberVales.size())){ - int copyValue = container->numberVales[x]; + if (x < static_cast(container->numberValues.size())){ + int copyValue = container->numberValues[x]; SCP_string newKey; int size = static_cast(container->keys.size()); - sprintf(&newKey, "key%i", size); + sprintf(newKey, "key%i", size); bool found = false; do { found = false; - for (y = 0; y < static_cast(container->keys.size()); ++y){ + for (int y = 0; y < static_cast(container->keys.size()); ++y){ if (container->keys[y] == newKey) { found = true; break; @@ -764,23 +774,26 @@ SCP_string VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string if (found) { ++size; newKey = ""; - sprintf(&newKey, "key%i", size); + sprintf(newKey, "key%i", size); } - } while (found && size < static_cast(container->keys.size()) + 100) + } while (found && size < static_cast(container->keys.size()) + 100); // we could not generate a new key .... somehow. if (found){ - return ""; + return std::make_pair("", ""); } container->keys.push_back(newKey); container->numberValues.push_back(copyValue); - return std::make_pair(newKey, copyValue); + SCP_string temp; + sprintf(temp, "%i", copyValue); + + return std::make_pair(newKey, temp); } else { - return ""; + return std::make_pair("", ""); } } } @@ -802,9 +815,9 @@ bool VariableDialogModel::removeMapItem(SCP_string containerName, SCP_string key for (int x = 0; x < static_cast(container->keys.size()); ++x) { if (container->keys[x] == key) { - if ((container->string && x < static_cast(container->stringValues.size())) { + if (container->string && x < static_cast(container->stringValues.size())) { container->stringValues.erase(container->stringValues.begin() + x); - } else if (!container->string && x < static_cast(container->numberValues.size()))){ + } else if (!container->string && x < static_cast(container->numberValues.size())){ container->numberValues.erase(container->numberValues.begin() + x); } else { return false; @@ -848,8 +861,8 @@ SCP_string VariableDialogModel::changeMapItemStringValue(SCP_string containerNam return ""; } - for (int x = 0; x < static_cast(container->keys); ++x){ - if (container->keys[x] == oldKey) { + for (int x = 0; x < static_cast(container->keys.size()); ++x){ + if (container->keys[x] == key) { if (x < static_cast(container->stringValues.size())){ container->stringValues[x] = newValue; return newValue; @@ -871,12 +884,12 @@ SCP_string VariableDialogModel::changeMapItemNumberValue(SCP_string containerNam return ""; } - for (int x = 0; x < static_cast(container->keys); ++x){ - if (container->keys[x] == oldKey) { + for (int x = 0; x < static_cast(container->keys.size()); ++x){ + if (container->keys[x] == key) { if (x < static_cast(container->numberValues.size())){ container->numberValues[x] = newValue; SCP_string returnValue; - sprintf(&returnValue, "%i", newValue) + sprintf(returnValue, "%i", newValue); return returnValue; } else { return ""; @@ -894,14 +907,18 @@ const SCP_vector& VariableDialogModel::getMapKeys(SCP_string contain auto container = lookupContainer(containerName); if (!container) { - throw std::invalid_argument("getMapKeys() found that container %s does not exist.", containerName.c_str()); + SCP_string temp; + sprintf("getMapKeys() found that container %s does not exist.", containerName.c_str()); + throw std::invalid_argument(temp.c_str()); } if (container->list) { - throw std::invalid_argument("getMapKeys() found that container %s is not a map.", containerName.c_str()); + SCP_string temp; + sprintf("getMapKeys() found that container %s is not a map.", containerName.c_str()); + throw std::invalid_argument(temp); } - return containerName->keys; + return container->keys; } // Only call when the container is guaranteed to exist! @@ -910,14 +927,18 @@ const SCP_vector& VariableDialogModel::getStringValues(SCP_string co auto container = lookupContainer(containerName); if (!container) { - throw std::invalid_argument("getStringValues() found that container %s does not exist.", containerName.c_str()); + SCP_string temp; + sprintf("getStringValues() found that container %s does not exist.", containerName.c_str()); + throw std::invalid_argument(temp); } if (!container->string) { - throw std::invalid_argument("getStringValues() found that container %s does not store strings.", containerName.c_str()); + SCP_string temp; + sprintf("getStringValues() found that container %s does not store strings.", containerName.c_str()); + throw std::invalid_argument(temp); } - return containerName->stringValues; + return container->stringValues; } // Only call when the container is guaranteed to exist! @@ -926,19 +947,23 @@ const SCP_vector& VariableDialogModel::getNumberValues(SCP_string container auto container = lookupContainer(containerName); if (!container) { - throw std::invalid_argument("getNumberValues() found that container %s does not exist.", containerName.c_str()); + SCP_string temp; + sprintf("getNumberValues() found that container %s does not exist.", containerName.c_str()); + throw std::invalid_argument(temp); } if (container->string) { - throw std::invalid_argument("getNumberValues() found that container %s does not store numbers.", containerName.c_str()); + SCP_string temp; + sprintf("getNumberValues() found that container %s does not store numbers.", containerName.c_str()); + throw std::invalid_argument(temp); } - return containerName->numberValues; + return container->numberValues; } -const SCP_vector VariableDialogModel::getVariableValues() +const SCP_vector> VariableDialogModel::getVariableValues() { - SCP_vector outStrings; + SCP_vector> outStrings; for (const auto& item : _variableItems) { SCP_string notes = ""; @@ -949,18 +974,20 @@ const SCP_vector VariableDialogMo notes = "New"; } else if (item.name != item.originalName){ notes = "Renamed"; - } else if (item.string && item.stringValue = "") { + } else if (item.string && item.stringValue == "") { notes = "Defaulting to empty string"; } - outStrings.emplace_back(item.name, (item.string) ? item.stringValue : SCP_string(item.numberValue), notes); + SCP_string temp; + sprintf(temp, "%i", item.numberValue); + outStrings.emplace_back(item.name, (item.string) ? item.stringValue : temp, notes); } return outStrings; } -const SCP_vector> VarigableDialogModel::getContainerNames() +const SCP_vector> VariableDialogModel::getContainerNames() { SCP_vector> outStrings; diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 53c1c451947..adff7308548 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -3,6 +3,7 @@ #include "globalincs/pstypes.h" #include "AbstractDialogModel.h" +#include namespace fso { namespace fred { @@ -92,7 +93,7 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string copyListItem(SCP_string containerName, int index); bool removeListItem(SCP_string containerName, int index); - SCP_string addMapItem(SCP_string ContainerName); + std::pair addMapItem(SCP_string ContainerName); std::pair copyMapItem(SCP_string containerName, SCP_string key); bool removeMapItem(SCP_string containerName, SCP_string key); @@ -104,27 +105,28 @@ class VariableDialogModel : public AbstractDialogModel { const SCP_vector& getStringValues(SCP_string containerName); const SCP_vector& getNumberValues(SCP_string containerName); - const SCP_vector VariableDialogModel::getVariableValues(); - const SCP_vector> VarigableDialogModel::getContainerNames(); + const SCP_vector> getVariableValues(); + const SCP_vector> getContainerNames(); bool apply() override; void reject() override; + void initializeData(); private: SCP_vector _variableItems; SCP_vector _containerItems; - const variableInfo* lookupVariable(SCP_string name){ - for (int x = 0; x < static_cast(_varaibleItems.size()); ++x){ - if (_varaibleItems[x].name == name){ - return &_varaibleItems[x]; + variableInfo* lookupVariable(SCP_string name){ + for (int x = 0; x < static_cast(_variableItems.size()); ++x){ + if (_variableItems[x].name == name){ + return &_variableItems[x]; } } return nullptr; } - const containerInfo* lookupContainer(SCP_string name){ + containerInfo* lookupContainer(SCP_string name){ for (int x = 0; x < static_cast(_containerItems.size()); ++x){ if (_containerItems[x].name == name){ return &_containerItems[x]; @@ -139,7 +141,7 @@ class VariableDialogModel : public AbstractDialogModel { { QMessageBox msgBox; msgBox.setText(question.c_str()); - msgBox.setInformativeText(informativeText.C_str()); + msgBox.setInformativeText(informativeText.c_str()); msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Cancel); int ret = msgBox.exec(); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 6debb7c4403..d6fe9fec898 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -13,9 +13,10 @@ namespace fred { namespace dialogs { VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) - : QDialog(parent), ui(new Ui::VariableDialog()), _model(new VariableDialogModel(this, viewport)), _viewport(viewport) + : QDialog(parent), ui(new Ui::VariableEditorDialog()), _model(new VariableDialogModel(this, viewport)), _viewport(viewport) { this->setFocus(); + ui->setupUi(this); // Major Changes, like Applying the model, rejecting changes and updating the UI. connect(_model.get(), &AbstractDialogModel::modelChanged, this, &VariableDialog::updateUI); @@ -127,6 +128,10 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) &QCheckBox::clicked, this, &VariableDialog::onSetContainerAsEternalCheckboxClicked); + + updateUI(); + + resize(QDialog::sizeHint()); } void VariableDialog::onVariablesTableUpdated() {} // could be new name or new value diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 600ad5dc994..e84e3fd7056 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -10,7 +10,7 @@ namespace fred { namespace dialogs { namespace Ui { -class VariableDialog; +class VariableEditorDialog; } class VariableDialog : public QDialog { @@ -21,7 +21,7 @@ class VariableDialog : public QDialog { ~VariableDialog() override; private: - std::unique_ptr ui; + std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; From 9ef56797fe7c3ab15ea3d0626ac6a49b22c9f04d Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 12 Apr 2024 15:26:58 -0400 Subject: [PATCH 076/466] Create the Validate Function And correct the Apply function some. --- .../mission/dialogs/VariableDialogModel.cpp | 182 ++++++++++++++++-- .../src/mission/dialogs/VariableDialogModel.h | 4 +- 2 files changed, 167 insertions(+), 19 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 28b4f09bed7..e31a9f22e46 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1,6 +1,7 @@ #include "VariableDialogModel.h" #include "parse/sexp.h" #include "parse/sexp_container.h" +#include namespace fso { namespace fred { @@ -19,30 +20,155 @@ void VariableDialogModel::reject() _containerItems.clear(); } +bool VariableDialogModel::checkValidModel() +{ + std::unordered_set namesTaken; + std::unordered_set duplicates; + + for (const auto& variable : _variableItems){ + if (!namesTaken.insert(variable.name).second) { + duplicates.insert(variable.name); + } + } + + SCP_string messageOut1; + SCP_string messageOut2; + + if (!duplicates.empty()){ + for (const auto& item : duplicates){ + if (messageOut2.empty()){ + messageOut2 = "\"" + item + "\""; + } else { + messageOut2 += ", "\"" + item + "\""; + } + } + + sprintf(messageOut1, "There are %zu duplicate variables:\n", duplicates.size()); + messageOut1 += messageOut2 + "\n\n"; + } + + duplicates.clear(); + unordered_set namesTakenContainer; + SCP_vector duplicateKeys; + + for (const auto& container : _containerItems){ + if (!namesTakenContainer.insert(container.name).second) { + duplicates.insert(container.name); + } + + if (!container.list){ + unordered_set keysTakenContainer; + + for (const auto& key : container.keys){ + if (!keysTakenContainer.insert(key)) { + SCP_string temp = key + "in map" + container.name + ", "; + duplicateKeys.push_back(temp); + } + } + } + } + + messageOut2.clear(); + + if (!duplicates.empty()){ + for (const auto& item : duplicates){ + if (messageOut2.empty()){ + messageOut2 = "\"" + item + "\""; + } else { + messageOut2 += ", "\"" + item + "\""; + } + } + + SCP_string temp; + + sprintf(temp, "There are %zu duplicate containers:\n\n", duplicates.size()); + messageOut1 += messageOut2 + "\n"; + } + + messageOut2.clear(); + + if (!duplicateKeys.empty()){ + for (const auto& key : duplicateKeys){ + messageOut2 += key; + } + + SCP_string temp; + + sprintf(temp, "There are %zu duplicate map keys:\n\n", duplicateKeys.size()); + messageOut1 += messageOut2 + "\n"; + } + + if (messageOut1.empty()){ + return true; + } else { + messageOut1 = "Please correct these variable, container and key names. The editor cannot apply your changes until they are fixed:\n\n" + messageOut1; + + QMessageBox msgBox; + msgBox.setText(messageOut1.c_str()); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); + } + +} bool VariableDialogModel::apply() { - // TODO VALIDATE! - // TODO! Look for referenced variables and containers. - // Need a way to clean up references. I'm thinking making some pop ups to confirm replacements created in the editor. - // This means we need a way to count and replace references. + // TODO Change the connect statement to validate before coming here to apply. + + // what did we delete from the original list? We need to check these references and clean them. + std::unordered_set deletedVariables; + SCP_vector> nameChangedVariables; + bool found; + + // first we have to edit known variables. + for (const auto& variable : _variableItems){ + found = false; + + // set of instructions for updating variables + if (!variable.originalName.empty()) { + for (int i = 0; i < MAX_SEXP_VARIABLES; ++i) { + if (!stricmp(Sexp_variables[i].variable_name, variable.originalName)){ + if (variable.deleted) { + memset(Sexp_variables[i].variable_name, 0, NAME_LENGTH); + memset(Sexp_variables[i].text, 0, NAME_LENGTH); + Sexp_variables[i].type = 0; + + deletedVariables.insert(variable.originalName); + } else { + if (variable.name != variable.originalName) { + nameChangedVariables.emplace_back(i, variable.originalName); + } - // So, previously, I was just obliterating and overwriting, but I need to rethink this info. - /*memset(Sexp_variables, 0, MAX_SEXP_VARIABLES * size_of(sexp_variable)); + strcpy_s(Sexp_variables[i].variable_name, variable.name.c_str()); + Sexp_variables[i].flags = variable.flags; - for (int i = 0; i < static_cast(_variableItems.size()); ++i){ - Sexp_variables[i].type = _variableItems[i].flags; - strcpy_s(Sexp_variables[i].variable_name, _variableItems[i].name.c_str()); - - if (_variableItems[i].flags & SEXP_VARIABLE_STRING){ - strcpy_s(Sexp_variables[i].text, _variableItems[i].stringValue); - } else { - strcpy_s(Sexp_variables[i].text, std::to_string(_variableItems[i].numberValue).c_str()) + if (variable.flags & SEXP_VARIABLE_STRING){ + strcpy_s(Sexp_variables[i].text, variable.stringValue); + Sexp_variables[i].flags |= SEXP_VARIABLE_STRING; + } else { + strcpy_s(Sexp_variables[i].text, std::to_string(variable.numberValue).c_str()) + Sexp_variables[i].flags |= SEXP_VARIABLE_NUMBER; + } + } + + found = true; + break; + } + } + } + + if (!found) { + // TODO! Lookup how the old editor does this. (look for an empty slot maybe?) } } - */ + // TODO! containers + std::unordered_set deletedContainers; + + // TODO! Look for referenced variables and containers. + // Need a way to clean up references. I'm thinking making some pop ups to confirm replacements created in the editor. + // This means we need a way to count and replace references. return false; } @@ -90,10 +216,30 @@ void VariableDialogModel::initializeData() newContainer.originalName = newContainer.name; newContainer.deleted = false; - // TODO FIXME! - newContainer.string = false; - newContainer.list = container.is_list(); + if (container.type & ContainerType::STRING_DATA) { + newContainer.string = true; + } else if (container.type & ContainerType::NUMBER_DATA) { + newContainer.string = false; + } + // using the SEXP variable version of these values here makes things easier + if (container.type & ContainerType::SAVE_TO_PLAYER_FILE) { + newContainer.flags |= SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } + + if (container.type & ContainerType::SAVE_ON_MISSION_CLOSE) { + newContainer.flags |= SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE; + } + + if (container.type & ContainerType::SAVE_ON_MISSION_PROGRESS) { + newContainer.flags |= SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS; + } + + if (container.type & ContainerType::NETWORK) { + newContainer.flags =| SEXP_VARIABLE_NETWORK; + } + + newContainer.list = container.is_list(); } } diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index adff7308548..187633e68bd 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -22,12 +22,14 @@ struct variableInfo { struct containerInfo { SCP_string name = ""; - SCP_string originalName = ""; bool deleted = false; bool list = true; bool string = true; int flags = 0; + // this will allow us to look up the original values used in the mission previously. + SCP_string originalName = ""; + SCP_vector keys; SCP_vector numberValues; SCP_vector stringValues; From 11fac90617620b725af84e7079ffcc5fcb9fcbbd Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 12 Apr 2024 15:27:24 -0400 Subject: [PATCH 077/466] Add missing functions --- qtfred/src/ui/dialogs/VariableDialog.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index d6fe9fec898..f50cd3a9f56 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -43,6 +43,11 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onAddVariableButtonPressed); + connect(ui->copyVariableButton, + &QPushButton::clicked, + this, + &VariableDialog::onCopyVariableButtonPressed); + connect(ui->deleteVariableButton, &QPushButton::clicked, this, @@ -78,6 +83,11 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onAddContainerButtonPressed); + connect(ui->copyContainerButton, + &QPushButton::clicked, + this, + &VariableDialog::onCopyContainerButtonPressed); + connect(ui->deleteContainerButton, &QPushButton::clicked, this, @@ -142,6 +152,7 @@ void VariableDialog::onAddVariableButtonPressed() } +void VariableDialog::onCopyVariableButtonPressed(){} void VariableDialog::onDeleteVariableButtonPressed() {} void VariableDialog::onSetVariableAsStringRadioSelected() {} void VariableDialog::onSetVariableAsNumberRadioSelected() {} @@ -150,6 +161,7 @@ void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() {} void VariableDialog::onSaveVariableAsEternalCheckboxClicked() {} void VariableDialog::onAddContainerButtonPressed() {} +void VariableDialog::onCopyContainerButtonPressed() {} void VariableDialog::onDeleteContainerButtonPressed() {} void VariableDialog::onSetContainerAsMapRadioSelected() {} void VariableDialog::onSetContainerAsListRadioSelected() {} From 2934e99ddcd1792ce4beb403cf7629a62e79858e Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 12 Apr 2024 17:26:14 -0400 Subject: [PATCH 078/466] Progress on applyModel --- qtfred/src/ui/dialogs/VariableDialog.cpp | 124 +++++++++++++++++++++-- qtfred/src/ui/dialogs/VariableDialog.h | 10 +- 2 files changed, 122 insertions(+), 12 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index f50cd3a9f56..ee3a1db029d 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -28,16 +28,31 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onVariablesTableUpdated); - connect(ui->variablesTable, + connect(ui->variablesTable, + &QTableWidget::itemSelectionChanged, + this, + &VariableDialog::onVariablesSelectionChanged); + + connect(ui->containersTable, QOverload::of(&QTableWidget::cellChanged), this, &VariableDialog::onContainersTableUpdated); - connect(ui->variablesTable, + connect(ui->containersTable, + &QTableWidget::itemSelectionChanged, + this, + &VariableDialog::onContainersSelectionChanged); + + connect(ui->containerContentsTable, QOverload::of(&QTableWidget::cellChanged), this, &VariableDialog::onContainerContentsTableUpdated); + connect(ui->containerContentsTable, + &QTableWidget::itemSelectionChanged, + this, + &VariableDialog::onContainerContentsSelectionChanged); + connect(ui->addVariableButton, &QPushButton::clicked, this, @@ -123,7 +138,6 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onSaveContainerOnMissionCompletedRadioSelected); - connect(ui->addContainerItemButton, &QPushButton::clicked, this, @@ -139,19 +153,42 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onSetContainerAsEternalCheckboxClicked); - updateUI(); - resize(QDialog::sizeHint()); + + ui->variablesTable->setColumnCount(3); + ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); + ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); + ui->variablesTable->setColumnWidth(0, 200); + ui->variablesTable->setColumnWidth(1, 200); + ui->variablesTable->setColumnWidth(2, 200); + + ui->containersTable->setColumnCount(3); + ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); + ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); + ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); + ui->containersTable->setColumnWidth(0, 200); + ui->containersTable->setColumnWidth(1, 200); + ui->containersTable->setColumnWidth(2, 200); + + ui->containerContentsTable->setColumnCount(2); + + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + ui->containerContentsTable->setColumnWidth(0, 200); + ui->containerContentsTable->setColumnWidth(1, 200); + + applyModel(); } void VariableDialog::onVariablesTableUpdated() {} // could be new name or new value +void VariableDialog::onVariablesSelectionChanged() {} void VariableDialog::onContainersTableUpdated() {} // could be new name +void VariableDialog::onContainersSelectionChanged() {} void VariableDialog::onContainerContentsTableUpdated() {} // could be new key or new value -void VariableDialog::onAddVariableButtonPressed() -{ - +void VariableDialog::onContainerContentsSelectionChanged() {} -} +void VariableDialog::onAddVariableButtonPressed() {} void VariableDialog::onCopyVariableButtonPressed(){} void VariableDialog::onDeleteVariableButtonPressed() {} void VariableDialog::onSetVariableAsStringRadioSelected() {} @@ -175,10 +212,75 @@ void VariableDialog::onAddContainerItemButtonPressed() {} void VariableDialog::onDeleteContainerItemButtonPressed() {} - VariableDialog::~VariableDialog(){}; // NOLINT -void VariableDialog::updateUI(){}; +void VariableDialog::applyModel() +{ + auto variables = _model->getVariableValues(); + + for (int x = 0; x < static_cast(variables.size()); ++x){ + if (ui->variablesTable->item(x, 0)){ + ui->variablesTable->item(x, 0)->setText(variables[x]<0>.c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(variables[x]<0>.c_str()); + ui->variablesTable->setItem(x, 0, item); + } + + if (ui->variablesTable->item(x, 1)){ + ui->variablesTable->item(x, 1)->setText(variables[x]<1>.c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(variables[x]<1>.c_str()); + ui->variablesTable->setItem(x, 1, nameItem); + } + + if (ui->variablesTable->item(x, 2)){ + ui->variablesTable->item(x, 2)->setText(variables[x]<2>.c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(variables[x]<2>.c_str()); + ui->variablesTable->setItem(x, 2, item); + } + + } + + if (_currentVariable.empty()){ + if( ui->VariablesTable->item(0,0) && strlen(ui->VariablesTable->item(0,0)->text())){ + _currentVariable = ui->VariablesTable->item(0,0)->text(); + } + // TODO! Make new ui function with the following stuff. + // get type with getVariableType + // get network status with getVariableNetworkStatus + // get getVariablesOnMissionCloseOrCompleteFlag + // getVariableEternalFlag + // string or number value with getVariableStringValue or getVariableNumberValue + } + + auto containers = _model->getContainerNames(); + + // TODO! Change getContainerNames to a tuple with notes/maybe data key types? + for (x = 0; x < static_cast(containers.size()); ++x){ + if (ui->containersTable->item(x, 0)){ + ui->containersTable->item(x, 0)->setText(containers[x]<0>.c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(containers[x]<0>.c_str()); + ui->containersTable->setItem(x, 0, item); + } + + if (ui->containersTable->item(x, 1)){ + ui->containersTable->item(x, 1)->setText(containers[x]<1>.c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(containers[x]<1>.c_str()); + ui->containersTable->setItem(x, 1, nameItem); + } + + if (ui->containersTable->item(x, 2)){ + ui->containersTable->item(x, 2)->setText(containers[x]<2>.c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(containers[x]<2>.c_str()); + ui->containersTable->setItem(x, 2, item); + } + } + +}; } // namespace dialogs diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index e84e3fd7056..f40ac75f70b 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -25,11 +25,15 @@ class VariableDialog : public QDialog { std::unique_ptr _model; EditorViewport* _viewport; - void updateUI(); + // basically UpdateUI, but called when there is an inconsistency between model and UI + void applyModel(); void onVariablesTableUpdated(); + void onVariablesSelectionChanged(); void onContainersTableUpdated(); + void onContainersSelectionChanged(); void onContainerContentsTableUpdated(); + void onContainerContentsSelectionChanged(); void onAddVariableButtonPressed(); void onDeleteVariableButtonPressed(); void onCopyVariableButtonPressed(); @@ -52,6 +56,10 @@ class VariableDialog : public QDialog { void onSetContainerAsEternalCheckboxClicked(); void onAddContainerItemButtonPressed(); void onDeleteContainerItemButtonPressed(); + + SCP_string _currentVariable = ""; + SCP_string _currentContainer = ""; + SCP_string _currentContainerItem = ""; }; From e60cf650369306add8e8821148261fa71131e7f4 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 12 Apr 2024 21:04:20 -0400 Subject: [PATCH 079/466] Write part of onVariablesTableUpdated --- qtfred/src/ui/dialogs/VariableDialog.cpp | 170 +++++++++++++++++++++-- 1 file changed, 156 insertions(+), 14 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index ee3a1db029d..01780f8a6aa 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -24,7 +24,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) connect(this, &QDialog::rejected, _model.get(), &VariableDialogModel::reject); connect(ui->variablesTable, - QOverload::of(&QTableWidget::cellChanged), + &QTableWidget::itemChanged, this, &VariableDialog::onVariablesTableUpdated); @@ -34,7 +34,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) &VariableDialog::onVariablesSelectionChanged); connect(ui->containersTable, - QOverload::of(&QTableWidget::cellChanged), + &QTableWidget::itemChanged, this, &VariableDialog::onContainersTableUpdated); @@ -44,7 +44,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) &VariableDialog::onContainersSelectionChanged); connect(ui->containerContentsTable, - QOverload::of(&QTableWidget::cellChanged), + &QTableWidget::itemChanged, this, &VariableDialog::onContainerContentsTableUpdated); @@ -181,7 +181,88 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) applyModel(); } -void VariableDialog::onVariablesTableUpdated() {} // could be new name or new value +// TODO! have applyModel set a variable that has us return early on these table updated functions. +// TODO! make sure that when a variable is added that the whole model is reloaded. +void VariableDialog::onVariablesTableUpdated() +{ + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for(const auto& item : items) { + if (item->column() == 0){ + + // so if the user just removed the name, mark it as deleted *before changing the name* + if (_currentVariable != "" && !strlen(item->text.c_str())){ + if (!_model->removeVariable(item->row())) { + // marking a variable as deleted failed, resync UI + applyModel(); + return; + } + } + + auto ret = _model->changeVariableName(item->row(), item->text().toStdString()); + + // we put something in the cell, but the model couldn't process it. + if (strlen(item->text()) && ret == ""){ + // update of variable name failed, resync UI + applyModel(); + + // we had a successful rename. So update the variable we reference. + } else if (ret != "") { + item->setText(ret.c_str()); + _currentVariable = ret; + } + + // empty return and cell was handled earlier. + + // data was altered + } else if (item->column() == 0) { + if (_model->getVariableType(int->row())){ + SCP_string ret = _model->setVariableStringValue(int->row(), item->text().toStdString()); + if (ret == ""){ + applyModel(); + } + } else { + SCP_string temp; + SCP_string source = item->text().toStdString(); + + std::copy_if(s1.begin(), s1.end(), std::back_inserter(temp), + [](char c){ + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return true; + break; + default: + return false; + break; + } + } + ); + + if (temp != ) + + int ret = _model->setVariableNumberValue(int->row(), ) + } + + + // if the user somehow edited the info that comes from the model and should not be editable, reload everything. + } else { + applyModel(); + } + } +} + + void VariableDialog::onVariablesSelectionChanged() {} void VariableDialog::onContainersTableUpdated() {} // could be new name void VariableDialog::onContainersSelectionChanged() {} @@ -217,8 +298,9 @@ VariableDialog::~VariableDialog(){}; // NOLINT void VariableDialog::applyModel() { auto variables = _model->getVariableValues(); + int x, selectedRow = -1; - for (int x = 0; x < static_cast(variables.size()); ++x){ + for (x = 0; x < static_cast(variables.size()); ++x){ if (ui->variablesTable->item(x, 0)){ ui->variablesTable->item(x, 0)->setText(variables[x]<0>.c_str()); } else { @@ -226,6 +308,11 @@ void VariableDialog::applyModel() ui->variablesTable->setItem(x, 0, item); } + // check if this is the current variable. + if (!_currentVariable.empty() && variables[x]<0> == _currentVariable){ + selectedRow = x + } + if (ui->variablesTable->item(x, 1)){ ui->variablesTable->item(x, 1)->setText(variables[x]<1>.c_str()); } else { @@ -239,22 +326,44 @@ void VariableDialog::applyModel() QTableWidgetItem* item = new QTableWidgetItem(variables[x]<2>.c_str()); ui->variablesTable->setItem(x, 2, item); } + } + // TODO, try setting row count? + // This empties rows that might have previously had variables + if (x < ui->variablesTable->rowCount()) { + ++x; + for (; x < ui->variablesTable->rowCount(); ++x){ + if (ui->variablesTable->item(x, 0)){ + ui->variablesTable->item(x, 0)->setText(""); + } + + if (ui->variablesTable->item(x, 1)){ + ui->variablesTable->item(x, 1)->setText(""); + } + + if (ui->variablesTable->item(x, 2)){ + ui->variablesTable->item(x, 2)->setText(""); + } + } } - if (_currentVariable.empty()){ - if( ui->VariablesTable->item(0,0) && strlen(ui->VariablesTable->item(0,0)->text())){ - _currentVariable = ui->VariablesTable->item(0,0)->text(); + if (_currentVariable.empty() || selectedRow < 0){ + if (ui->variablesTable->item(0,0) && strlen(ui->variablesTable->item(0,0)->text())){ + _currentVariable = ui->variablesTable->item(0,0)->text(); } - // TODO! Make new ui function with the following stuff. - // get type with getVariableType - // get network status with getVariableNetworkStatus - // get getVariablesOnMissionCloseOrCompleteFlag - // getVariableEternalFlag - // string or number value with getVariableStringValue or getVariableNumberValue } + // TODO! Make new ui function with the following stuff. + // get type with getVariableType + // get network status with getVariableNetworkStatus + // get getVariablesOnMissionCloseOrCompleteFlag + // getVariableEternalFlag + // string or number value with getVariableStringValue or getVariableNumberValue + updateVariableOptions(); + + auto containers = _model->getContainerNames(); + selectedRow = -1; // TODO! Change getContainerNames to a tuple with notes/maybe data key types? for (x = 0; x < static_cast(containers.size()); ++x){ @@ -265,6 +374,12 @@ void VariableDialog::applyModel() ui->containersTable->setItem(x, 0, item); } + // check if this is the current variable. + if (!_currentVariable.empty() && containers[x]<0> == _currentVariable){ + selectedRow = x; + } + + if (ui->containersTable->item(x, 1)){ ui->containersTable->item(x, 1)->setText(containers[x]<1>.c_str()); } else { @@ -280,6 +395,33 @@ void VariableDialog::applyModel() } } + // This empties rows that might have previously had containers + if (x < ui->containersTable->rowCount()) { + ++x; + for (; x < ui->containersTable->rowCount(); ++x){ + if (ui->containersTable->item(x, 0)){ + ui->containersTable->item(x, 0)->setText(""); + } + + if (ui->containersTable->item(x, 1)){ + ui->containersTable->item(x, 1)->setText(""); + } + + if (ui->containersTable->item(x, 2)){ + ui->containersTable->item(x, 2)->setText(""); + } + } + } + + if (_currentContainer.empty() || selectedRow < 0){ + if (ui->containersTable->item(0,0) && strlen(ui->containersTable->item(0,0)->text())){ + _currentContainer = ui->containersTable->item(0,0)->text(); + } + } + + // this will update the list/map items. + updateContainerOptions(); + }; From bc059bdb865aea1382a386b6dc5cbdcbffb788f1 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 12 Apr 2024 21:47:42 -0400 Subject: [PATCH 080/466] Add Variable Editor Trigger To Loadout Dialog --- qtfred/src/ui/dialogs/LoadoutDialog.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/qtfred/src/ui/dialogs/LoadoutDialog.cpp b/qtfred/src/ui/dialogs/LoadoutDialog.cpp index 9e730551c21..b0542191826 100644 --- a/qtfred/src/ui/dialogs/LoadoutDialog.cpp +++ b/qtfred/src/ui/dialogs/LoadoutDialog.cpp @@ -517,6 +517,7 @@ void LoadoutDialog::onClearAllUsedWeaponsPressed() // TODO! Finish writing a trigger to open that dialog, once the variable editor is created void LoadoutDialog::openEditVariablePressed() { + viewport->on_actionVariables_triggered(); } void LoadoutDialog::onSelectionRequiredPressed() From c9f4982d080200acbb0252872dc133a4b12db617 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 12 Apr 2024 21:48:50 -0400 Subject: [PATCH 081/466] Add apply trigger on validated data And clean up dialog model slightly --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index e31a9f22e46..e084cdb0b30 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -20,7 +20,7 @@ void VariableDialogModel::reject() _containerItems.clear(); } -bool VariableDialogModel::checkValidModel() +void VariableDialogModel::checkValidModel() { std::unordered_set namesTaken; std::unordered_set duplicates; @@ -99,7 +99,7 @@ bool VariableDialogModel::checkValidModel() } if (messageOut1.empty()){ - return true; + apply(); } else { messageOut1 = "Please correct these variable, container and key names. The editor cannot apply your changes until they are fixed:\n\n" + messageOut1; @@ -109,11 +109,12 @@ bool VariableDialogModel::checkValidModel() msgBox.exec(); } + + } bool VariableDialogModel::apply() { - // TODO Change the connect statement to validate before coming here to apply. // what did we delete from the original list? We need to check these references and clean them. std::unordered_set deletedVariables; @@ -158,7 +159,7 @@ bool VariableDialogModel::apply() } if (!found) { - // TODO! Lookup how the old editor does this. (look for an empty slot maybe?) + // TODO! Lookup how FRED adds new variables. (look for an empty slot maybe?) } } @@ -168,7 +169,6 @@ bool VariableDialogModel::apply() // TODO! Look for referenced variables and containers. // Need a way to clean up references. I'm thinking making some pop ups to confirm replacements created in the editor. - // This means we need a way to count and replace references. return false; } @@ -198,7 +198,6 @@ void VariableDialogModel::initializeData() } catch (...) { item.numberValue = 0; - // TODO! Warning popup } item.stringValue = ""; From 6e10ee64f82d6b966e2496a14533cee48ceacacc Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 12 Apr 2024 21:56:27 -0400 Subject: [PATCH 082/466] Add TrimNumberString function And other things --- qtfred/src/ui/dialogs/LoadoutDialog.cpp | 1 - qtfred/src/ui/dialogs/VariableDialog.cpp | 120 ++++++++++++++++------- qtfred/src/ui/dialogs/VariableDialog.h | 3 + 3 files changed, 88 insertions(+), 36 deletions(-) diff --git a/qtfred/src/ui/dialogs/LoadoutDialog.cpp b/qtfred/src/ui/dialogs/LoadoutDialog.cpp index b0542191826..c115656b247 100644 --- a/qtfred/src/ui/dialogs/LoadoutDialog.cpp +++ b/qtfred/src/ui/dialogs/LoadoutDialog.cpp @@ -514,7 +514,6 @@ void LoadoutDialog::onClearAllUsedWeaponsPressed() _lastSelectionChanged = USED_WEAPONS; } -// TODO! Finish writing a trigger to open that dialog, once the variable editor is created void LoadoutDialog::openEditVariablePressed() { viewport->on_actionVariables_triggered(); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 01780f8a6aa..5868aa6d83e 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -20,7 +20,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) // Major Changes, like Applying the model, rejecting changes and updating the UI. connect(_model.get(), &AbstractDialogModel::modelChanged, this, &VariableDialog::updateUI); - connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::apply); + connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::checkValidModel); connect(this, &QDialog::rejected, _model.get(), &VariableDialogModel::reject); connect(ui->variablesTable, @@ -181,10 +181,13 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) applyModel(); } -// TODO! have applyModel set a variable that has us return early on these table updated functions. // TODO! make sure that when a variable is added that the whole model is reloaded. void VariableDialog::onVariablesTableUpdated() { + if (_applyingModel){ + return; + } + auto items = ui->variablesTable->selectedItems(); // yes, selected items returns a list, but we really should only have one item because multiselect will be off. @@ -215,47 +218,43 @@ void VariableDialog::onVariablesTableUpdated() // empty return and cell was handled earlier. - // data was altered - } else if (item->column() == 0) { + // data cell was altered + } else if (item->column() == 1) { + + // Variable is a string if (_model->getVariableType(int->row())){ - SCP_string ret = _model->setVariableStringValue(int->row(), item->text().toStdString()); + SCP_string temp = item->text()->toStdString().c_str(); + temp = temp.substr(0, NAME_LENGTH - 1); + + SCP_string ret = _model->setVariableStringValue(int->row(), temp); if (ret == ""){ applyModel(); + return; } + + item->setText(ret.c_str()); } else { SCP_string temp; SCP_string source = item->text().toStdString(); - std::copy_if(s1.begin(), s1.end(), std::back_inserter(temp), - [](char c){ - switch (c) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - case '-': - return true; - break; - default: - return false; - break; - } - } - ); - - if (temp != ) - - int ret = _model->setVariableNumberValue(int->row(), ) - } + SCP_string temp = trimNumberString(); + if (temp != source){ + item->setText(temp.c_str()); + } - // if the user somehow edited the info that comes from the model and should not be editable, reload everything. + try { + int ret = _model->setVariableNumberValue(item->row(), std::stoi(temp)); + temp = ""; + sprintf(temp, "%i", ret); + item->setText(temp); + } + catch (...) { + applyModel(); + } + } + + // if the user somehow edited the info that should only come from the model and should not be editable, reload everything. } else { applyModel(); } @@ -264,9 +263,22 @@ void VariableDialog::onVariablesTableUpdated() void VariableDialog::onVariablesSelectionChanged() {} -void VariableDialog::onContainersTableUpdated() {} // could be new name +void VariableDialog::onContainersTableUpdated() +{ + if (_applyingModel){ + return; + } + +} // could be new name void VariableDialog::onContainersSelectionChanged() {} -void VariableDialog::onContainerContentsTableUpdated() {} // could be new key or new value +void VariableDialog::onContainerContentsTableUpdated() +{ + if (_applyingModel){ + return; + } + + +} // could be new key or new value void VariableDialog::onContainerContentsSelectionChanged() {} void VariableDialog::onAddVariableButtonPressed() {} @@ -297,6 +309,8 @@ VariableDialog::~VariableDialog(){}; // NOLINT void VariableDialog::applyModel() { + _applyingModel = true; + auto variables = _model->getVariableValues(); int x, selectedRow = -1; @@ -422,8 +436,44 @@ void VariableDialog::applyModel() // this will update the list/map items. updateContainerOptions(); + _applyingModel = false; }; +SCP_string VariableDialog::trimNumberString(SCP_string source) +{ + SCP_string ret; + + // account for a lead negative sign. + if (source[0] == "-") { + ret = "-"; + } + + // filter out non-numeric digits + std::copy_if(s1.begin(), s1.end(), std::back_inserter(ret), + [](char c){ + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + return true; + break; + default: + return false; + break; + } + } + ); + + return ret; +} + } // namespace dialogs } // namespace fred diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index f40ac75f70b..22e06337e24 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -57,6 +57,9 @@ class VariableDialog : public QDialog { void onAddContainerItemButtonPressed(); void onDeleteContainerItemButtonPressed(); + SCP_string trimNumberString(SCP_string source); + + bool _applyingModel = false; SCP_string _currentVariable = ""; SCP_string _currentContainer = ""; SCP_string _currentContainerItem = ""; From 2ad0571b22e41dfae872385586325616b37c12e2 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 12 Apr 2024 23:55:22 -0400 Subject: [PATCH 083/466] Make sure that setVariableType signals failure --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index e084cdb0b30..beccd62ba8f 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -306,8 +306,10 @@ bool VariableDialogModel::setVariableType(SCP_string name, bool string) auto variable = lookupVariable(name); // nothing to change, or invalid entry + // Best way to say that it failed is to say + // that it is not switching to what the ui asked for. if (!variable || variable->string == string){ - return string; + return !string; } From 4cac920943a22b4fc975803f70f6947a16457c0a Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 12 Apr 2024 23:56:00 -0400 Subject: [PATCH 084/466] Add more dialog functions --- qtfred/src/ui/dialogs/VariableDialog.cpp | 105 +++++++++++++++++++++-- 1 file changed, 97 insertions(+), 8 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 5868aa6d83e..2609e7cc2cb 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -178,6 +178,18 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->containerContentsTable->setColumnWidth(0, 200); ui->containerContentsTable->setColumnWidth(1, 200); + // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't + ui->setVariableAsStringRadio->setAutoExclusive(false); + ui->setVariableAsNumberRadio->setAutoExclusive(false); + ui->saveContainerOnMissionCompletedRadio->setAutoExclusive(false); + ui->saveVariableOnMissionCloseRadio->setAutoExclusive(false); + ui->setContainerAsMapRadio->setAutoExclusive(false); + ui->setContainerAsListRadio->setAutoExclusive(false); + ui->setContainerAsStringRadio->setAutoExclusive(false); + ui->setContainerAsNumberRadio->setAutoExclusive(false); + ui->saveContainerOnMissionCloseRadio->setAutoExclusive(false); + ui->saveContainerOnMissionCompletedRadio->setAutoExclusive(false); + applyModel(); } @@ -262,7 +274,14 @@ void VariableDialog::onVariablesTableUpdated() } -void VariableDialog::onVariablesSelectionChanged() {} +void VariableDialog::onVariablesSelectionChanged() +{ + if (_applyingModel){ + return; + } +} + + void VariableDialog::onContainersTableUpdated() { if (_applyingModel){ @@ -270,7 +289,14 @@ void VariableDialog::onContainersTableUpdated() } } // could be new name -void VariableDialog::onContainersSelectionChanged() {} + +void VariableDialog::onContainersSelectionChanged() +{ + if (_applyingModel){ + return; + } +} + void VariableDialog::onContainerContentsTableUpdated() { if (_applyingModel){ @@ -279,13 +305,76 @@ void VariableDialog::onContainerContentsTableUpdated() } // could be new key or new value -void VariableDialog::onContainerContentsSelectionChanged() {} -void VariableDialog::onAddVariableButtonPressed() {} -void VariableDialog::onCopyVariableButtonPressed(){} -void VariableDialog::onDeleteVariableButtonPressed() {} -void VariableDialog::onSetVariableAsStringRadioSelected() {} -void VariableDialog::onSetVariableAsNumberRadioSelected() {} +void VariableDialog::onContainerContentsSelectionChanged() { + if (_applyingModel){ + return; + } + +} + +void VariableDialog::onAddVariableButtonPressed() +{ + auto ret = _model->addNewVriable(); + _currentVariable = ret; + applyModel(); +} + +void VariableDialog::onCopyVariableButtonPressed() +{ + if (_currentVarible.empty()){ + return; + } + + auto ret = _model->copyVariable(_currentVariable); + _currentVariable = ret; + applyModel(); +} + +void VariableDialog::onDeleteVariableButtonPressed() +{ + if (_currentVarible.empty()){ + return; + } + + // Because of the text update we'll need, this needs an applyModel, whether it fails or not. + _model->removeVariable(_currentVariable); + applyModel(); +} + +void VariableDialog::onSetVariableAsStringRadioSelected() +{ + if (_currentVarible.empty() || ui->setVariableAsStringRadio->isChecked()){ + return; + } + + // this doesn't return succeed or fail directly, + // but if it doesn't return true then it failed since this is the string radio + if(!_model->setVariableType(_currentVariable, true)){ + applyModel(); + } else { + ui->setVariableAsStringRadio->setChecked(true); + ui->setVariableAsNumberRadio->setChecked(false); + } +} + +void VariableDialog::onSetVariableAsNumberRadioSelected() +{ + if (_currentVarible.empty() || ui->setVariableAsNumberRadio->isChecked()){ + return; + } + + // this doesn't return succeed or fail directly, + // but if it doesn't return false then it failed since this is the number radio + if(!_model->setVariableType(_currentVariable, false)){ + applyModel(); + } else { + ui->setVariableAsStringRadio->setChecked(false); + ui->setVariableAsNumberRadio->setChecked(true); + } +} + + void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() {} void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() {} void VariableDialog::onSaveVariableAsEternalCheckboxClicked() {} From 8d40952bf5ca8e9a2f602af44724163fca0a0a17 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 13 Apr 2024 00:51:20 -0400 Subject: [PATCH 085/466] Finish adding Variable change options --- qtfred/src/ui/dialogs/VariableDialog.cpp | 129 +++++++++++++++++++++-- qtfred/src/ui/dialogs/VariableDialog.h | 1 + 2 files changed, 123 insertions(+), 7 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 2609e7cc2cb..4c25ed7cd67 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -88,6 +88,11 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onSaveVariableOnMissionCloseRadioSelected); + connect(ui->networkVariableCheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onNetworkVariableCheckboxClicked); + connect(ui->setVariableAsEternalcheckbox, &QCheckBox::clicked, this, @@ -322,7 +327,7 @@ void VariableDialog::onAddVariableButtonPressed() void VariableDialog::onCopyVariableButtonPressed() { - if (_currentVarible.empty()){ + if (_currentVariable.empty()){ return; } @@ -333,7 +338,7 @@ void VariableDialog::onCopyVariableButtonPressed() void VariableDialog::onDeleteVariableButtonPressed() { - if (_currentVarible.empty()){ + if (_currentVariable.empty()){ return; } @@ -344,7 +349,7 @@ void VariableDialog::onDeleteVariableButtonPressed() void VariableDialog::onSetVariableAsStringRadioSelected() { - if (_currentVarible.empty() || ui->setVariableAsStringRadio->isChecked()){ + if (_currentVariable.empty() || ui->setVariableAsStringRadio->isChecked()){ return; } @@ -360,7 +365,7 @@ void VariableDialog::onSetVariableAsStringRadioSelected() void VariableDialog::onSetVariableAsNumberRadioSelected() { - if (_currentVarible.empty() || ui->setVariableAsNumberRadio->isChecked()){ + if (_currentVariable.empty() || ui->setVariableAsNumberRadio->isChecked()){ return; } @@ -375,9 +380,69 @@ void VariableDialog::onSetVariableAsNumberRadioSelected() } -void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() {} -void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() {} -void VariableDialog::onSaveVariableAsEternalCheckboxClicked() {} +void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() +{ + if (_currentVariable.empty() || ui->saveContainerOnMissionCompletedRadio->isChecked()){ + return; + } + + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(_currentVariable, 1); + + if (ret != 1){ + applyModel(); + } else { + // TODO! Need "no persistence" options and functions! + ui->saveContainerOnMissionCompletedRadio->setChecked(true); + ui->saveVariableOnMissionCloseRadio->setChecked(false); + //ui->saveContainerOnMissionCompletedRadio->setChecked(true); + } +} + +void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() +{ + if (_currentVariable.empty() || ui->saveContainerOnMissionCompletedRadio->isChecked()){ + return; + } + + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(_currentVariable, 2); + + if (ret != 2){ + applyModel(); + } else { + // TODO! Need "no persistence" options. + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(true); + //ui->saveContainerOnMissionCompletedRadio->setChecked(false); + } +} + +void VariableDialog::onSaveVariableAsEternalCheckboxClicked() +{ + if (_currentVariable.empty()){ + return; + } + + // If the model returns the old status, then the change failed and we're out of sync. + if (ui->setVariableAsEternalcheckbox->isChecked() == _model->setVariableEternalFlag(_currentVariable, !ui->setVariableAsEternalcheckbox->isChecked())){ + applyModel(); + } else { + _ui->setVariableAsEternalcheckbox->setChecked(!ui->setVariableAsEternalcheckbox->isChecked()); + } +} + +void VariableDialog::onNetworkVariableCheckboxClicked() +{ + if (_currentVariable.empty()){ + return; + } + + // If the model returns the old status, then the change failed and we're out of sync. + if (ui->setVariableNetworkStatus->isChecked() == _model->setVariableNetworkStatus(_currentVariable, !ui->setVariableNetworkStatus->isChecked())){ + applyModel(); + } else { + _ui->setVariableNetworkStatus->setChecked(!ui->setVariableNetworkStatus->isChecked()); + } +} void VariableDialog::onAddContainerButtonPressed() {} void VariableDialog::onCopyContainerButtonPressed() {} @@ -528,6 +593,56 @@ void VariableDialog::applyModel() _applyingModel = false; }; +void VariableDialog::updateVariableOptions() +{ + if (_currentVariable.empty()){ + ui->copyVariableButton.setEnabled(false); + ui->deleteVariableButton.setEnabled(false); + ui->setVariableAsStringRadio.setEnabled(false); + ui->setVariableAsNumberRadio.setEnabled(false); + ui->saveContainerOnMissionCompletedRadio.setEnabled(false); + ui->saveVariableOnMissionCloseRadio.setEnabled(false); + ui->setVariableAsEternalcheckbox.setEnabled(false); + + return; + } + + ui->copyVariableButton.setEnabled(true); + ui->deleteVariableButton.setEnabled(true); + ui->setVariableAsStringRadio.setEnabled(true); + ui->setVariableAsNumberRadio.setEnabled(true); + ui->saveContainerOnMissionCompletedRadio.setEnabled(true); + ui->saveVariableOnMissionCloseRadio.setEnabled(true); + ui->setVariableAsEternalcheckbox.setEnabled(true); + + // start populating values + bool string = _model->getVariableType(_currentVariable); + ui->setVariableAsStringRadio.setChecked(string); + ui->setVariableAsNumberRadio.setChecked(!string); + ui->setVariableAsEternalcheckbox.setChecked(); + + int ret = _model->getVariableOnMissionCloseOrCompleteFlag(_currentVariable); + + if (ret == 0){ + // TODO ADD NO PERSISTENCE + } else if (ret == 1) { + ui->saveContainerOnMissionCompletedRadio.setChecked(true); + ui->saveVariableOnMissionCloseRadio.setChecked(false); + } else { + ui->saveContainerOnMissionCompletedRadio.setChecked(false); + ui->saveVariableOnMissionCloseRadio.setChecked(true); + } + + ui->networkVariableCheckbox.setChecked(_model->getVariableNetworkStatus(_currentVariable)); + ui->setVariableAsEternalcheckbox.setChecked(_model->getVariableEternalFlag(_currentVariable)); + +} + +void VariableDialog::updateContainerOptions() +{ + +} + SCP_string VariableDialog::trimNumberString(SCP_string source) { SCP_string ret; diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 22e06337e24..f4a4e70fa0a 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -42,6 +42,7 @@ class VariableDialog : public QDialog { void onSaveVariableOnMissionCompleteRadioSelected(); void onSaveVariableOnMissionCloseRadioSelected(); void onSaveVariableAsEternalCheckboxClicked(); + void onNetworkVariableCheckboxClicked(); void onAddContainerButtonPressed(); void onDeleteContainerButtonPressed(); From d4f8706208a1f253f2e14b3732e8f58c9a5663d4 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 14 Apr 2024 12:00:32 -0400 Subject: [PATCH 086/466] Add contents to UpdateContainerOptions --- qtfred/src/ui/dialogs/VariableDialog.cpp | 160 ++++++++++++++++++++--- qtfred/src/ui/dialogs/VariableDialog.h | 9 +- 2 files changed, 153 insertions(+), 16 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 4c25ed7cd67..900cc29c7c1 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -17,6 +17,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) { this->setFocus(); ui->setupUi(this); + resize(QDialog::sizeHint()); // The best I can tell without some research, when a dialog doesn't use an underling grid or layout, it needs to be resized this way before anything will show up // Major Changes, like Applying the model, rejecting changes and updating the UI. connect(_model.get(), &AbstractDialogModel::modelChanged, this, &VariableDialog::updateUI); @@ -131,7 +132,17 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) connect(ui->setContainerAsNumberRadio, &QRadioButton::toggled, this, - &VariableDialog::onSetContainerAsNumberRadio); + &VariableDialog::onSetContainerAsNumberRadioSelected); + + connect(ui->setContainerKeyAsStringRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSetContainerKeyAsStringRadioSelected); + + connect(ui->setContainerKeyAsNumberRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onSetContainerKeyAsNumberRadioSelected); connect(ui->saveContainerOnMissionCloseRadio, &QRadioButton::toggled, @@ -143,22 +154,31 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onSaveContainerOnMissionCompletedRadioSelected); + connect(ui->setContainerAsEternalCheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onSetContainerAsEternalCheckboxClicked); + + connect(ui->networkContainerCheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onNetworkContainerCheckboxClicked); + connect(ui->addContainerItemButton, &QPushButton::clicked, this, &VariableDialog::onAddContainerItemButtonPressed); + connect(ui->copyContainerItemButton, + &QPushButton::clicked, + this, + &VariableDialog::onCopyContainerItemButtonPressed); + connect(ui->deleteContainerItemButton, &QPushButton::clicked, this, &VariableDialog::onDeleteContainerItemButtonPressed); - connect(ui->setContainerAsEternalCheckbox, - &QCheckBox::clicked, - this, - &VariableDialog::onSetContainerAsEternalCheckboxClicked); - - resize(QDialog::sizeHint()); ui->variablesTable->setColumnCount(3); ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); @@ -178,8 +198,9 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->containerContentsTable->setColumnCount(2); - ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); - ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + // Default to list + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); ui->containerContentsTable->setColumnWidth(0, 200); ui->containerContentsTable->setColumnWidth(1, 200); @@ -284,6 +305,23 @@ void VariableDialog::onVariablesSelectionChanged() if (_applyingModel){ return; } + + auto items = ui->variablesTable->selectedItems(); + + SCP_string newVariableName = ""; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for(const auto& item : items) { + if (item->column() == 0){ + newVariableName = item->text().toStdString(); + break; + } + } + + if (newVariableName != _currentVariable){ + _currentVariable = newVariableName; + applyModel(); + } } @@ -297,9 +335,23 @@ void VariableDialog::onContainersTableUpdated() void VariableDialog::onContainersSelectionChanged() { - if (_applyingModel){ + if (_applyingModel){ return; } + + auto items = ui->containersTable->selectedItems(); + + SCP_string newVariableName = ""; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for(const auto& item : items) { + newVariableName = item->text().toStdString(); + } + + if (newVariableName != _currentVariable){ + _currentVariable = newVariableName; + applyModel(); + } } void VariableDialog::onContainerContentsTableUpdated() @@ -450,12 +502,15 @@ void VariableDialog::onDeleteContainerButtonPressed() {} void VariableDialog::onSetContainerAsMapRadioSelected() {} void VariableDialog::onSetContainerAsListRadioSelected() {} void VariableDialog::onSetContainerAsStringRadioSelected() {} -void VariableDialog::onSetContainerAsNumberRadio() {} +void VariableDialog::onSetContainerAsNumberRadioSelected() {} +void VariableDialog::onSetContainerKeyAsStringRadioSelected() {} +void VariableDialog::onSetContainerKeyAsNumberRadioSelected() {} void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() {} void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() {} void VariableDialog::onNetworkContainerCheckboxClicked() {} void VariableDialog::onSetContainerAsEternalCheckboxClicked() {} void VariableDialog::onAddContainerItemButtonPressed() {} +void VariableDialog::onCopyContainerItemButtonPressed() {} void VariableDialog::onDeleteContainerItemButtonPressed() {} @@ -600,7 +655,7 @@ void VariableDialog::updateVariableOptions() ui->deleteVariableButton.setEnabled(false); ui->setVariableAsStringRadio.setEnabled(false); ui->setVariableAsNumberRadio.setEnabled(false); - ui->saveContainerOnMissionCompletedRadio.setEnabled(false); + ui->saveVariableOnMissionCompletedRadio.setEnabled(false); ui->saveVariableOnMissionCloseRadio.setEnabled(false); ui->setVariableAsEternalcheckbox.setEnabled(false); @@ -611,7 +666,7 @@ void VariableDialog::updateVariableOptions() ui->deleteVariableButton.setEnabled(true); ui->setVariableAsStringRadio.setEnabled(true); ui->setVariableAsNumberRadio.setEnabled(true); - ui->saveContainerOnMissionCompletedRadio.setEnabled(true); + ui->saveVariableOnMissionCompletedRadio.setEnabled(true); ui->saveVariableOnMissionCloseRadio.setEnabled(true); ui->setVariableAsEternalcheckbox.setEnabled(true); @@ -626,10 +681,10 @@ void VariableDialog::updateVariableOptions() if (ret == 0){ // TODO ADD NO PERSISTENCE } else if (ret == 1) { - ui->saveContainerOnMissionCompletedRadio.setChecked(true); + ui->saveVariableOnMissionCompletedRadio.setChecked(true); ui->saveVariableOnMissionCloseRadio.setChecked(false); } else { - ui->saveContainerOnMissionCompletedRadio.setChecked(false); + ui->saveVariableOnMissionCompletedRadio.setChecked(false); ui->saveVariableOnMissionCloseRadio.setChecked(true); } @@ -639,6 +694,81 @@ void VariableDialog::updateVariableOptions() } void VariableDialog::updateContainerOptions() +{ + if (_currentContainer.empty()){ + ui->copyContainerButton.setEnabled(false); + ui->deleteContainerButton.setEnabled(false); + ui->setContainerAsStringRadio.setEnabled(false); + ui->setContainerAsNumberRadio.setEnabled(false); + ui->saveContainerOnMissionCompletedRadio.setEnabled(false); + ui->saveContainerOnMissionCloseRadio.setEnabled(false); + ui->setContainerAsEternalcheckbox.setEnabled(false); + ui->setContainerAsMapRadio.setEnabled(false); + ui->setContainerAsListRadio.setEnabled(false); + + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + + + } else { + ui->copyContainerButton.setEnabled(false); + ui->deleteContainerButton.setEnabled(false); + ui->setContainerAsStringRadio.setEnabled(false); + ui->setContainerAsNumberRadio.setEnabled(false); + ui->saveContainerOnMissionCompletedRadio.setEnabled(false); + ui->saveContainerOnMissionCloseRadio.setEnabled(false); + ui->setContainerAsEternalcheckbox.setEnabled(false); + ui->setContainerAsMapRadio.setEnabled(false); + ui->setContainerAsListRadio.setEnabled(false); + + if (_model->getContainerType(_currentContainer)){ + ui->setContainerAsStringRadio.setChecked(true); + ui->setContainerAsNumberRadio.setChecked(false); + } else { + ui->setContainerAsStringRadio.setChecked(false); + ui->setContainerAsNumberRadio.setChecked(true); + } + + if (_model->getConainerListOrMap(_currentContainer)){ + ui->setContainerAsListRadio.setChecked(true); + ui->setContainerAsMapRadio.setChecked(false); + + // Don't forget to change headings + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + updateContainerDataOptions(true); + + } else { + ui->setContainerAsListRadio.setChecked(false); + ui->setContainerAsMapRadio.setChecked(true); + + // Don't forget to change headings + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + updateContainerDataOptions(false); + } + + ui->setContainerAsEternalcheckbox.setChecked(_model->getContainerNetworkStatus(_currentContainer)); + ui->networkContainerCheckbox.setChecked(_model->getContainerNetworkStatus(_currentContainer)); + + // TODO! Add key data type controls + + int ret = getContainerOnMissionCloseOrCompleteFlag(_currentContainer); + + if (ret == 0){ + // TODO ADD NO PERSISTENCE + } else if (ret == 1) { + ui->saveContainerOnMissionCompletedRadio.setChecked(true); + ui->saveContainerOnMissionCloseRadio.setChecked(false); + } else { + ui->saveContainerOnMissionCompletedRadio.setChecked(false); + ui->saveContainerOnMissionCloseRadio.setChecked(true); + } + + } +} + +void VariableDialog::updateContainerDataOptions(bool list) { } diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index f4a4e70fa0a..6349adbd293 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -27,6 +27,10 @@ class VariableDialog : public QDialog { // basically UpdateUI, but called when there is an inconsistency between model and UI void applyModel(); + // Helper functions for this + void updateVariableOptions(); + void updateContainerOptions(); + void updateContainerDataOptions(bool list); void onVariablesTableUpdated(); void onVariablesSelectionChanged(); @@ -50,12 +54,15 @@ class VariableDialog : public QDialog { void onSetContainerAsMapRadioSelected(); void onSetContainerAsListRadioSelected(); void onSetContainerAsStringRadioSelected(); - void onSetContainerAsNumberRadio(); + void onSetContainerAsNumberRadioSelected(); + void onSetContainerKeyAsStringRadioSelected(); + void onSetContainerKeyAsNumberRadioSelected() void onSaveContainerOnMissionClosedRadioSelected(); void onSaveContainerOnMissionCompletedRadioSelected(); void onNetworkContainerCheckboxClicked(); void onSetContainerAsEternalCheckboxClicked(); void onAddContainerItemButtonPressed(); + void onCopyContainerItemButtonPressed(); void onDeleteContainerItemButtonPressed(); SCP_string trimNumberString(SCP_string source); From 1581d9dc1f5fa7c6bf6b6b6ad8ba58dbbd258300 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 14 Apr 2024 12:34:20 -0400 Subject: [PATCH 087/466] Save progress --- qtfred/src/ui/dialogs/VariableDialog.cpp | 25 +++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 900cc29c7c1..b5b29131bbd 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -228,7 +228,7 @@ void VariableDialog::onVariablesTableUpdated() auto items = ui->variablesTable->selectedItems(); - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + // yes, selected items returns a list, but we really should only have one row of items because multiselect will be off. for(const auto& item : items) { if (item->column() == 0){ @@ -238,7 +238,11 @@ void VariableDialog::onVariablesTableUpdated() // marking a variable as deleted failed, resync UI applyModel(); return; + } else { + updateVariableOptions(); } + } else { + } auto ret = _model->changeVariableName(item->row(), item->text().toStdString()); @@ -576,15 +580,8 @@ void VariableDialog::applyModel() } } - // TODO! Make new ui function with the following stuff. - // get type with getVariableType - // get network status with getVariableNetworkStatus - // get getVariablesOnMissionCloseOrCompleteFlag - // getVariableEternalFlag - // string or number value with getVariableStringValue or getVariableNumberValue updateVariableOptions(); - auto containers = _model->getContainerNames(); selectedRow = -1; @@ -732,7 +729,11 @@ void VariableDialog::updateContainerOptions() if (_model->getConainerListOrMap(_currentContainer)){ ui->setContainerAsListRadio.setChecked(true); ui->setContainerAsMapRadio.setChecked(false); - + + // Disable Key Controls + ui->setContainerKeyAsStringRadio.setEnabled(false); + ui->setContainerKeyAsNumberRadio.setEnabled(false); + // Don't forget to change headings ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); @@ -742,6 +743,10 @@ void VariableDialog::updateContainerOptions() ui->setContainerAsListRadio.setChecked(false); ui->setContainerAsMapRadio.setChecked(true); + // Enabled Key Controls + ui->setContainerKeyAsStringRadio.setEnabled(true); + ui->setContainerKeyAsNumberRadio.setEnabled(true); + // Don't forget to change headings ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); @@ -751,8 +756,6 @@ void VariableDialog::updateContainerOptions() ui->setContainerAsEternalcheckbox.setChecked(_model->getContainerNetworkStatus(_currentContainer)); ui->networkContainerCheckbox.setChecked(_model->getContainerNetworkStatus(_currentContainer)); - // TODO! Add key data type controls - int ret = getContainerOnMissionCloseOrCompleteFlag(_currentContainer); if (ret == 0){ From 3d5c43e45dfcddc6baf78f0cc24f996d7c3d2f1e Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 15 Apr 2024 22:43:45 -0400 Subject: [PATCH 088/466] Fix model functions by using index Instead of name. --- .../mission/dialogs/VariableDialogModel.cpp | 164 +++++++++--------- .../src/mission/dialogs/VariableDialogModel.h | 18 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 50 ++++-- 3 files changed, 123 insertions(+), 109 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index beccd62ba8f..9aaa440583b 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -245,24 +245,24 @@ void VariableDialogModel::initializeData() // true on string, false on number -bool VariableDialogModel::getVariableType(SCP_string name) +bool VariableDialogModel::getVariableType(int index) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); return (variable) ? (variable->string) : true; } -bool VariableDialogModel::getVariableNetworkStatus(SCP_string name) +bool VariableDialogModel::getVariableNetworkStatus(int index) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); return (variable) ? ((variable->flags & SEXP_VARIABLE_NETWORK) > 0) : false; } // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) -int VariableDialogModel::getVariableOnMissionCloseOrCompleteFlag(SCP_string name) +int VariableDialogModel::getVariableOnMissionCloseOrCompleteFlag(int index) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); if (!variable) { return 0; @@ -279,31 +279,31 @@ int VariableDialogModel::getVariableOnMissionCloseOrCompleteFlag(SCP_string name } -bool VariableDialogModel::getVariableEternalFlag(SCP_string name) +bool VariableDialogModel::getVariableEternalFlag(int index) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); return (variable) ? ((variable->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) > 0) : false; } -SCP_string VariableDialogModel::getVariableStringValue(SCP_string name) +SCP_string VariableDialogModel::getVariableStringValue(int index) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); return (variable && variable->string) ? (variable->stringValue) : ""; } -int VariableDialogModel::getVariableNumberValue(SCP_string name) +int VariableDialogModel::getVariableNumberValue(int index) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); return (variable && !variable->string) ? (variable->numberValue) : 0; } // true on string, false on number -bool VariableDialogModel::setVariableType(SCP_string name, bool string) +bool VariableDialogModel::setVariableType(int index, bool string) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); // nothing to change, or invalid entry // Best way to say that it failed is to say @@ -370,9 +370,9 @@ bool VariableDialogModel::setVariableType(SCP_string name, bool string) } } -bool VariableDialogModel::setVariableNetworkStatus(SCP_string name, bool network) +bool VariableDialogModel::setVariableNetworkStatus(int index, bool network) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); // nothing to change, or invalid entry if (!variable){ @@ -387,9 +387,9 @@ bool VariableDialogModel::setVariableNetworkStatus(SCP_string name, bool network return network; } -int VariableDialogModel::setVariableOnMissionCloseOrCompleteFlag(SCP_string name, int flags) +int VariableDialogModel::setVariableOnMissionCloseOrCompleteFlag(int index, int flags) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); // nothing to change, or invalid entry if (!variable || flags < 0 || flags > 2){ @@ -408,9 +408,9 @@ int VariableDialogModel::setVariableOnMissionCloseOrCompleteFlag(SCP_string name return flags; } -bool VariableDialogModel::setVariableEternalFlag(SCP_string name, bool eternal) +bool VariableDialogModel::setVariableEternalFlag(int index, bool eternal) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); // nothing to change, or invalid entry if (!variable){ @@ -426,9 +426,9 @@ bool VariableDialogModel::setVariableEternalFlag(SCP_string name, bool eternal) return eternal; } -SCP_string VariableDialogModel::setVariableStringValue(SCP_string name, SCP_string value) +SCP_string VariableDialogModel::setVariableStringValue(int index, SCP_string value) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); // nothing to change, or invalid entry if (!variable || !variable->string){ @@ -438,9 +438,9 @@ SCP_string VariableDialogModel::setVariableStringValue(SCP_string name, SCP_stri variable->stringValue = value; } -int VariableDialogModel::setVariableNumberValue(SCP_string name, int value) +int VariableDialogModel::setVariableNumberValue(int index, int value) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); // nothing to change, or invalid entry if (!variable || variable->string){ @@ -459,7 +459,7 @@ SCP_string VariableDialogModel::addNewVariable() do { name = ""; sprintf(name, "", count); - variable = lookupVariable(name); + variable = lookupVariableByName(name); ++count; } while (variable != nullptr && count < 51); @@ -473,13 +473,13 @@ SCP_string VariableDialogModel::addNewVariable() return name; } -SCP_string VariableDialogModel::changeVariableName(SCP_string oldName, SCP_string newName) +SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName) { if (newName == "") { return ""; } - auto variable = lookupVariable(oldName); + auto variable = lookupVariable(index); // nothing to change, or invalid entry if (!variable){ @@ -491,9 +491,9 @@ SCP_string VariableDialogModel::changeVariableName(SCP_string oldName, SCP_strin return newName; } -SCP_string VariableDialogModel::copyVariable(SCP_string name) +SCP_string VariableDialogModel::copyVariable(int index) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); // nothing to change, or invalid entry if (!variable){ @@ -506,7 +506,7 @@ SCP_string VariableDialogModel::copyVariable(SCP_string name) do { SCP_string newName; sprintf(newName, "%s_copy%i", name, count); - variableSearch = lookupVariable(newName); + variableSearch = lookupVariableByName(newName); // open slot found! if (!variableSearch){ @@ -533,9 +533,9 @@ SCP_string VariableDialogModel::copyVariable(SCP_string name) } // returns whether it succeeded -bool VariableDialogModel::removeVariable(SCP_string name) +bool VariableDialogModel::removeVariable(int index) { - auto variable = lookupVariable(name); + auto variable = lookupVariable(index); // nothing to change, or invalid entry if (!variable){ @@ -556,29 +556,29 @@ bool VariableDialogModel::removeVariable(SCP_string name) // Container Section // true on string, false on number -bool VariableDialogModel::getContainerValueType(SCP_string name) +bool VariableDialogModel::getContainerValueType(int index) { - auto container = lookupContainer(name); + auto container = lookupContainer(index); return (container) ? container->string : true; } // true on list, false on map -bool VariableDialogModel::getContainerListOrMap(SCP_string name) +bool VariableDialogModel::getContainerListOrMap(int index) { - auto container = lookupContainer(name); + auto container = lookupContainer(index); return (container) ? container->list : true; } -bool VariableDialogModel::getContainerNetworkStatus(SCP_string name) +bool VariableDialogModel::getContainerNetworkStatus(int index) { - auto container = lookupContainer(name); + auto container = lookupContainer(index); return (container) ? ((container->flags & SEXP_VARIABLE_NETWORK) > 0) : false; } // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) -int VariableDialogModel::getContainerOnMissionCloseOrCompleteFlag(SCP_string name) +int VariableDialogModel::getContainerOnMissionCloseOrCompleteFlag(int index) { - auto container = lookupContainer(name); + auto container = lookupContainer(index); if (!container) { return 0; @@ -592,16 +592,16 @@ int VariableDialogModel::getContainerOnMissionCloseOrCompleteFlag(SCP_string nam return 0; } -bool VariableDialogModel::getContainerEternalFlag(SCP_string name) +bool VariableDialogModel::getContainerEternalFlag(int index) { - auto container = lookupContainer(name); + auto container = lookupContainer(index); return (container) ? ((container->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) > 0) : false; } -bool VariableDialogModel::setContainerValueType(SCP_string name, bool type) +bool VariableDialogModel::setContainerValueType(int index, bool type) { - auto container = lookupContainer(name); + auto container = lookupContainer(index); if (!container){ return true; @@ -679,14 +679,14 @@ bool VariableDialogModel::setContainerValueType(SCP_string name, bool type) } // This is the most complicated function, because we need to query the user on what they want to do if the had already entered data. -bool VariableDialogModel::setContainerListOrMap(SCP_string name, bool list) +bool VariableDialogModel::setContainerListOrMap(int index, bool list) { return false; } -bool VariableDialogModel::setContainerNetworkStatus(SCP_string name, bool network) +bool VariableDialogModel::setContainerNetworkStatus(int index, bool network) { - auto container = lookupContainer(name); + auto container = lookupContainer(index); // nothing to change, or invalid entry if (!container){ @@ -702,9 +702,9 @@ bool VariableDialogModel::setContainerNetworkStatus(SCP_string name, bool networ return network; } -int VariableDialogModel::setContainerOnMissionCloseOrCompleteFlag(SCP_string name, int flags) +int VariableDialogModel::setContainerOnMissionCloseOrCompleteFlag(int index, int flags) { - auto container = lookupContainer(name); + auto container = lookupContainer(index); // nothing to change, or invalid entry if (!container || flags < 0 || flags > 2){ @@ -723,9 +723,9 @@ int VariableDialogModel::setContainerOnMissionCloseOrCompleteFlag(SCP_string nam return flags; } -bool VariableDialogModel::setContainerEternalFlag(SCP_string name, bool eternal) +bool VariableDialogModel::setContainerEternalFlag(int index, bool eternal) { - auto container = lookupContainer(name); + auto container = lookupContainer(index); // nothing to change, or invalid entry if (!container){ @@ -763,13 +763,13 @@ SCP_string VariableDialogModel::addContainer() return name; } -SCP_string VariableDialogModel::changeContainerName(SCP_string oldName, SCP_string newName) +SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newName) { if (newName == "") { return ""; } - auto container = lookupContainer(oldName); + auto container = lookupContainer(index); // nothing to change, or invalid entry if (!container){ @@ -781,9 +781,9 @@ SCP_string VariableDialogModel::changeContainerName(SCP_string oldName, SCP_stri return newName; } -bool VariableDialogModel::removeContainer(SCP_string name) +bool VariableDialogModel::removeContainer(int index) { - auto container = lookupContainer(name); + auto container = lookupContainer(index); if (!container){ return false; @@ -792,9 +792,9 @@ bool VariableDialogModel::removeContainer(SCP_string name) container->deleted = true; } -SCP_string VariableDialogModel::addListItem(SCP_string containerName) +SCP_string VariableDialogModel::addListItem(int index) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(index); if (!container){ return ""; @@ -809,14 +809,14 @@ SCP_string VariableDialogModel::addListItem(SCP_string containerName) } } -std::pair VariableDialogModel::addMapItem(SCP_string ContainerName) +std::pair VariableDialogModel::addMapItem(int index) { } -SCP_string VariableDialogModel::copyListItem(SCP_string containerName, int index) +SCP_string VariableDialogModel::copyListItem(int index, int index) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(index); if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ return ""; @@ -832,9 +832,9 @@ SCP_string VariableDialogModel::copyListItem(SCP_string containerName, int index } -bool VariableDialogModel::removeListItem(SCP_string containerName, int index) +bool VariableDialogModel::removeListItem(int containerIndex, int index) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(containerIndex); if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ return false; @@ -849,9 +849,9 @@ bool VariableDialogModel::removeListItem(SCP_string containerName, int index) } -std::pair VariableDialogModel::copyMapItem(SCP_string containerName, SCP_string keyIn) +std::pair VariableDialogModel::copyMapItem(int index, SCP_string keyIn) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(index); if (!container) { return std::make_pair("", ""); @@ -952,9 +952,9 @@ std::pair VariableDialogModel::copyMapItem(SCP_string co // both of the map's data vectors might be undesired, and not deleting takes the map immediately // out of sync. Also, just displaying both data sets would be misleading. // We just need to tell the user that the data cannot be maintained. -bool VariableDialogModel::removeMapItem(SCP_string containerName, SCP_string key) +bool VariableDialogModel::removeMapItem(int index, SCP_string key) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(index); if (!container){ return false; @@ -981,9 +981,9 @@ bool VariableDialogModel::removeMapItem(SCP_string containerName, SCP_string key return false; } -SCP_string VariableDialogModel::replaceMapItemKey(SCP_string containerName, SCP_string oldKey, SCP_string newKey) +SCP_string VariableDialogModel::replaceMapItemKey(int index, SCP_string oldKey, SCP_string newKey) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(index); if (!container){ return ""; @@ -1000,9 +1000,9 @@ SCP_string VariableDialogModel::replaceMapItemKey(SCP_string containerName, SCP_ return oldKey; } -SCP_string VariableDialogModel::changeMapItemStringValue(SCP_string containerName, SCP_string key, SCP_string newValue) +SCP_string VariableDialogModel::changeMapItemStringValue(int index, SCP_string key, SCP_string newValue) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(index); if (!container || !container->string){ return ""; @@ -1023,9 +1023,9 @@ SCP_string VariableDialogModel::changeMapItemStringValue(SCP_string containerNam return ""; } -SCP_string VariableDialogModel::changeMapItemNumberValue(SCP_string containerName, SCP_string key, int newValue) +SCP_string VariableDialogModel::changeMapItemNumberValue(int index, SCP_string key, int newValue) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(index); if (!container || !container->string){ return ""; @@ -1049,9 +1049,9 @@ SCP_string VariableDialogModel::changeMapItemNumberValue(SCP_string containerNam } // These functions should only be called when the container is guaranteed to exist! -const SCP_vector& VariableDialogModel::getMapKeys(SCP_string containerName) +const SCP_vector& VariableDialogModel::getMapKeys(int index) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(index); if (!container) { SCP_string temp; @@ -1069,19 +1069,19 @@ const SCP_vector& VariableDialogModel::getMapKeys(SCP_string contain } // Only call when the container is guaranteed to exist! -const SCP_vector& VariableDialogModel::getStringValues(SCP_string containerName) +const SCP_vector& VariableDialogModel::getStringValues(int index) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(index); if (!container) { SCP_string temp; - sprintf("getStringValues() found that container %s does not exist.", containerName.c_str()); + sprintf("getStringValues() found that container %s does not exist.", container->name.c_str()); throw std::invalid_argument(temp); } if (!container->string) { SCP_string temp; - sprintf("getStringValues() found that container %s does not store strings.", containerName.c_str()); + sprintf("getStringValues() found that container %s does not store strings.", container->name.c_str()); throw std::invalid_argument(temp); } @@ -1089,19 +1089,19 @@ const SCP_vector& VariableDialogModel::getStringValues(SCP_string co } // Only call when the container is guaranteed to exist! -const SCP_vector& VariableDialogModel::getNumberValues(SCP_string containerName) +const SCP_vector& VariableDialogModel::getNumberValues(int index) { - auto container = lookupContainer(containerName); + auto container = lookupContainer(index); if (!container) { SCP_string temp; - sprintf("getNumberValues() found that container %s does not exist.", containerName.c_str()); + sprintf("getNumberValues() found that container %s does not exist.", container->name.c_str()); throw std::invalid_argument(temp); } if (container->string) { SCP_string temp; - sprintf("getNumberValues() found that container %s does not store numbers.", containerName.c_str()); + sprintf("getNumberValues() found that container %s does not store numbers.", container->name.c_str()); throw std::invalid_argument(temp); } diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 187633e68bd..ff8b77054f0 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -118,23 +118,19 @@ class VariableDialogModel : public AbstractDialogModel { SCP_vector _variableItems; SCP_vector _containerItems; - variableInfo* lookupVariable(SCP_string name){ - for (int x = 0; x < static_cast(_variableItems.size()); ++x){ - if (_variableItems[x].name == name){ - return &_variableItems[x]; - } + variableInfo* lookupVariable(int index){ + if(index > -1 && index < static_cast(_variableItems.size()) ){ + return &_variableItems[index]; } return nullptr; } - containerInfo* lookupContainer(SCP_string name){ - for (int x = 0; x < static_cast(_containerItems.size()); ++x){ - if (_containerItems[x].name == name){ - return &_containerItems[x]; - } + containerInfo* lookupContainer(int index){ + if(index > -1 && index < static_cast(_containerItems.size()) ){ + return &_containerItems[index]; } - + return nullptr; } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index b5b29131bbd..70567b3d840 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -243,21 +243,19 @@ void VariableDialog::onVariablesTableUpdated() } } else { - } - - auto ret = _model->changeVariableName(item->row(), item->text().toStdString()); + auto ret = _model->changeVariableName(item->row(), item->text().toStdString()); - // we put something in the cell, but the model couldn't process it. - if (strlen(item->text()) && ret == ""){ - // update of variable name failed, resync UI - applyModel(); + // we put something in the cell, but the model couldn't process it. + if (strlen(item->text()) && ret == ""){ + // update of variable name failed, resync UI + applyModel(); - // we had a successful rename. So update the variable we reference. - } else if (ret != "") { - item->setText(ret.c_str()); - _currentVariable = ret; + // we had a successful rename. So update the variable we reference. + } else if (ret != "") { + item->setText(ret.c_str()); + _currentVariable = ret; + } } - // empty return and cell was handled earlier. // data cell was altered @@ -335,6 +333,7 @@ void VariableDialog::onContainersTableUpdated() return; } + } // could be new name void VariableDialog::onContainersSelectionChanged() @@ -345,15 +344,18 @@ void VariableDialog::onContainersSelectionChanged() auto items = ui->containersTable->selectedItems(); - SCP_string newVariableName = ""; + SCP_string newContainerName = ""; // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for(const auto& item : items) { - newVariableName = item->text().toStdString(); + if (item->column() == 0){ + newContainerName = item->text().toStdString(); + break; + } } - if (newVariableName != _currentVariable){ - _currentVariable = newVariableName; + if (newContainerName != _currentContainer){ + _currentContainer = newContainerName; applyModel(); } } @@ -372,6 +374,22 @@ void VariableDialog::onContainerContentsSelectionChanged() { return; } + auto items = ui->containersContentsTable->selectedItems(); + + SCP_string newContainerItemName = ""; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for(const auto& item : items) { + if (item->column() == 0){ + newContainerItemName = item->text().toStdString(); + break; + } + } + + if (newContainerItemName != _currentContainerItem){ + _currentContainerItem = newContainerItemName; + applyModel(); + } } void VariableDialog::onAddVariableButtonPressed() From 7938169730935f774630dcb72b6403e96f4a92ec Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 15 Apr 2024 22:50:05 -0400 Subject: [PATCH 089/466] Don't forget lookup by name functions This helps prevent name collisions --- .../src/mission/dialogs/VariableDialogModel.h | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index ff8b77054f0..a26646ee300 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -126,6 +126,16 @@ class VariableDialogModel : public AbstractDialogModel { return nullptr; } + variableInfo* lookupVariableByName(SCP_string name){ + for (int x = 0; x < static_cast(_variableItems.size())){ + if (_variableItems.name == name){ + return &_variableItems[x]; + } + } + + return nullptr; + } + containerInfo* lookupContainer(int index){ if(index > -1 && index < static_cast(_containerItems.size()) ){ return &_containerItems[index]; @@ -134,6 +144,16 @@ class VariableDialogModel : public AbstractDialogModel { return nullptr; } + containerInfo* lookupContainerByName(SCP_string name){ + for (int x = 0; x < static_cast(_containerItems.size())){ + if (_containerItems.name == name){ + return &_containerItems[x]; + } + } + + return nullptr; + } + // many of the controls in this editor can lead to drastic actions, so this will be very useful. const bool confirmAction(SCP_string question, SCP_string informativeText) { From e1662d8501803b81c16c2c1b670089961cab4903 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Tue, 16 Apr 2024 00:22:40 -0400 Subject: [PATCH 090/466] Fix many typos from lack of linter --- .../mission/dialogs/VariableDialogModel.cpp | 48 +-- .../src/mission/dialogs/VariableDialogModel.h | 91 ++--- qtfred/src/ui/dialogs/VariableDialog.cpp | 368 +++++++++++------- qtfred/src/ui/dialogs/VariableDialog.h | 2 +- qtfred/ui/VariableDialog.ui | 142 +++++-- 5 files changed, 392 insertions(+), 259 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 9aaa440583b..59b8b99a681 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -39,7 +39,7 @@ void VariableDialogModel::checkValidModel() if (messageOut2.empty()){ messageOut2 = "\"" + item + "\""; } else { - messageOut2 += ", "\"" + item + "\""; + messageOut2 += ", ""\"" + item + "\""; } } @@ -48,7 +48,7 @@ void VariableDialogModel::checkValidModel() } duplicates.clear(); - unordered_set namesTakenContainer; + std::unordered_set namesTakenContainer; SCP_vector duplicateKeys; for (const auto& container : _containerItems){ @@ -57,10 +57,10 @@ void VariableDialogModel::checkValidModel() } if (!container.list){ - unordered_set keysTakenContainer; + std::unordered_set keysTakenContainer; for (const auto& key : container.keys){ - if (!keysTakenContainer.insert(key)) { + if (!keysTakenContainer.insert(key).second) { SCP_string temp = key + "in map" + container.name + ", "; duplicateKeys.push_back(temp); } @@ -75,7 +75,7 @@ void VariableDialogModel::checkValidModel() if (messageOut2.empty()){ messageOut2 = "\"" + item + "\""; } else { - messageOut2 += ", "\"" + item + "\""; + messageOut2 += ", ""\"" + item + "\""; } } @@ -128,7 +128,7 @@ bool VariableDialogModel::apply() // set of instructions for updating variables if (!variable.originalName.empty()) { for (int i = 0; i < MAX_SEXP_VARIABLES; ++i) { - if (!stricmp(Sexp_variables[i].variable_name, variable.originalName)){ + if (!stricmp(Sexp_variables[i].variable_name, variable.originalName.c_str())){ if (variable.deleted) { memset(Sexp_variables[i].variable_name, 0, NAME_LENGTH); memset(Sexp_variables[i].text, 0, NAME_LENGTH); @@ -141,14 +141,14 @@ bool VariableDialogModel::apply() } strcpy_s(Sexp_variables[i].variable_name, variable.name.c_str()); - Sexp_variables[i].flags = variable.flags; + Sexp_variables[i].type = variable.flags; if (variable.flags & SEXP_VARIABLE_STRING){ - strcpy_s(Sexp_variables[i].text, variable.stringValue); - Sexp_variables[i].flags |= SEXP_VARIABLE_STRING; + strcpy_s(Sexp_variables[i].text, variable.stringValue.c_str()); + Sexp_variables[i].type |= SEXP_VARIABLE_STRING; } else { - strcpy_s(Sexp_variables[i].text, std::to_string(variable.numberValue).c_str()) - Sexp_variables[i].flags |= SEXP_VARIABLE_NUMBER; + strcpy_s(Sexp_variables[i].text, std::to_string(variable.numberValue).c_str()); + Sexp_variables[i].type |= SEXP_VARIABLE_NUMBER; } } @@ -215,27 +215,27 @@ void VariableDialogModel::initializeData() newContainer.originalName = newContainer.name; newContainer.deleted = false; - if (container.type & ContainerType::STRING_DATA) { + if (any(container.type & ContainerType::STRING_DATA)) { newContainer.string = true; - } else if (container.type & ContainerType::NUMBER_DATA) { + } else if (any(container.type & ContainerType::NUMBER_DATA)) { newContainer.string = false; } // using the SEXP variable version of these values here makes things easier - if (container.type & ContainerType::SAVE_TO_PLAYER_FILE) { + if (any(container.type & ContainerType::SAVE_TO_PLAYER_FILE)) { newContainer.flags |= SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; } - if (container.type & ContainerType::SAVE_ON_MISSION_CLOSE) { + if (any(container.type & ContainerType::SAVE_ON_MISSION_CLOSE)) { newContainer.flags |= SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE; } - if (container.type & ContainerType::SAVE_ON_MISSION_PROGRESS) { + if (any(container.type & ContainerType::SAVE_ON_MISSION_PROGRESS)) { newContainer.flags |= SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS; } - if (container.type & ContainerType::NETWORK) { - newContainer.flags =| SEXP_VARIABLE_NETWORK; + if (any(container.type & ContainerType::NETWORK)) { + newContainer.flags |= SEXP_VARIABLE_NETWORK; } newContainer.list = container.is_list(); @@ -505,7 +505,7 @@ SCP_string VariableDialogModel::copyVariable(int index) do { SCP_string newName; - sprintf(newName, "%s_copy%i", name, count); + sprintf(newName, "%s_copy%i", variable->name.c_str(), count); variableSearch = lookupVariableByName(newName); // open slot found! @@ -750,7 +750,7 @@ SCP_string VariableDialogModel::addContainer() do { name = ""; sprintf(name, "", count); - container = lookupContainer(name); + container = lookupContainerByName(name); ++count; } while (container != nullptr && count < 51); @@ -814,9 +814,9 @@ std::pair VariableDialogModel::addMapItem(int index) } -SCP_string VariableDialogModel::copyListItem(int index, int index) +SCP_string VariableDialogModel::copyListItem(int containerIndex, int index) { - auto container = lookupContainer(index); + auto container = lookupContainer(containerIndex); if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ return ""; @@ -1055,13 +1055,13 @@ const SCP_vector& VariableDialogModel::getMapKeys(int index) if (!container) { SCP_string temp; - sprintf("getMapKeys() found that container %s does not exist.", containerName.c_str()); + sprintf("getMapKeys() found that container %s does not exist.", container->name.c_str()); throw std::invalid_argument(temp.c_str()); } if (container->list) { SCP_string temp; - sprintf("getMapKeys() found that container %s is not a map.", containerName.c_str()); + sprintf("getMapKeys() found that container %s is not a map.", container->name.c_str()); throw std::invalid_argument(temp); } diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index a26646ee300..0933694a832 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -40,14 +40,14 @@ class VariableDialogModel : public AbstractDialogModel { VariableDialogModel(QObject* parent, EditorViewport* viewport); // true on string, false on number - bool getVariableType(SCP_string name); - bool getVariableNetworkStatus(SCP_string name); + bool getVariableType(int index); + bool getVariableNetworkStatus(int index); // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) - int getVariableOnMissionCloseOrCompleteFlag(SCP_string name); - bool getVariableEternalFlag(SCP_string name); + int getVariableOnMissionCloseOrCompleteFlag(int index); + bool getVariableEternalFlag(int index); - SCP_string getVariableStringValue(SCP_string name); - int getVariableNumberValue(SCP_string name); + SCP_string getVariableStringValue(int index); + int getVariableNumberValue(int index); // !! Note an innovation: when getting a request to set a value, // this model will return the value that sticks and then will overwrite @@ -55,60 +55,61 @@ class VariableDialogModel : public AbstractDialogModel { // repopulate the whole editor on each change. // true on string, false on number - bool setVariableType(SCP_string name, bool string); - bool setVariableNetworkStatus(SCP_string name, bool network); - int setVariableOnMissionCloseOrCompleteFlag(SCP_string name, int flags); - bool setVariableEternalFlag(SCP_string name, bool eternal); + bool setVariableType(int index, bool string); + bool setVariableNetworkStatus(int index, bool network); + int setVariableOnMissionCloseOrCompleteFlag(int index, int flags); + bool setVariableEternalFlag(int index, bool eternal); - SCP_string setVariableStringValue(SCP_string name, SCP_string value); - int setVariableNumberValue(SCP_string name, int value); + SCP_string setVariableStringValue(int index, SCP_string value); + int setVariableNumberValue(int index, int value); SCP_string addNewVariable(); - SCP_string changeVariableName(SCP_string oldName, SCP_string newName); - SCP_string copyVariable(SCP_string name); + SCP_string changeVariableName(int index, SCP_string newName); + SCP_string copyVariable(int index); // returns whether it succeeded - bool removeVariable(SCP_string name); + bool removeVariable(int index); // Container Section // true on string, false on number - bool getContainerValueType(SCP_string name); + bool getContainerValueType(int index); // true on list, false on map - bool getContainerListOrMap(SCP_string name); - bool getContainerNetworkStatus(SCP_string name); + bool getContainerListOrMap(int index); + bool getContainerNetworkStatus(int index); // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) - int getContainerOnMissionCloseOrCompleteFlag(SCP_string name); - bool getContainerEternalFlag(SCP_string name); + int getContainerOnMissionCloseOrCompleteFlag(int index); + bool getContainerEternalFlag(int index); - bool setContainerValueType(SCP_string name, bool type); - bool setContainerListOrMap(SCP_string name, bool list); - bool setContainerNetworkStatus(SCP_string name, bool network); - int setContainerOnMissionCloseOrCompleteFlag(SCP_string name, int flags); - bool setContainerEternalFlag(SCP_string name, bool eternal); + bool setContainerValueType(int index, bool type); + bool setContainerListOrMap(int index, bool list); + bool setContainerNetworkStatus(int index, bool network); + int setContainerOnMissionCloseOrCompleteFlag(int index, int flags); + bool setContainerEternalFlag(int index, bool eternal); SCP_string addContainer(); - SCP_string changeContainerName(SCP_string oldName, SCP_string newName); - bool removeContainer(SCP_string name); + SCP_string changeContainerName(int index, SCP_string newName); + bool removeContainer(int index); - SCP_string addListItem(SCP_string containerName); + SCP_string addListItem(int index); - SCP_string copyListItem(SCP_string containerName, int index); - bool removeListItem(SCP_string containerName, int index); + SCP_string copyListItem(int containerIndex, int index); + bool removeListItem(int containerindex, int index); - std::pair addMapItem(SCP_string ContainerName); - std::pair copyMapItem(SCP_string containerName, SCP_string key); - bool removeMapItem(SCP_string containerName, SCP_string key); + std::pair addMapItem(int index); + std::pair copyMapItem(int index, SCP_string key); + bool removeMapItem(int index, SCP_string key); - SCP_string replaceMapItemKey(SCP_string containerName, SCP_string oldKey, SCP_string newKey); - SCP_string changeMapItemStringValue(SCP_string containerName, SCP_string key, SCP_string newValue); - SCP_string changeMapItemNumberValue(SCP_string containerName, SCP_string key, int newValue); + SCP_string replaceMapItemKey(int index, SCP_string oldKey, SCP_string newKey); + SCP_string changeMapItemStringValue(int index, SCP_string key, SCP_string newValue); + SCP_string changeMapItemNumberValue(int index, SCP_string key, int newValue); - const SCP_vector& getMapKeys(SCP_string containerName); - const SCP_vector& getStringValues(SCP_string containerName); - const SCP_vector& getNumberValues(SCP_string containerName); + const SCP_vector& getMapKeys(int index); + const SCP_vector& getStringValues(int index); + const SCP_vector& getNumberValues(int index); - const SCP_vector> getVariableValues(); - const SCP_vector> getContainerNames(); + const SCP_vector> getVariableValues(); + const SCP_vector> getContainerNames(); + void VariableDialogModel::checkValidModel(); bool apply() override; void reject() override; @@ -127,8 +128,8 @@ class VariableDialogModel : public AbstractDialogModel { } variableInfo* lookupVariableByName(SCP_string name){ - for (int x = 0; x < static_cast(_variableItems.size())){ - if (_variableItems.name == name){ + for (int x = 0; x < static_cast(_variableItems.size()); ++x) { + if (_variableItems[x].name == name) { return &_variableItems[x]; } } @@ -145,8 +146,8 @@ class VariableDialogModel : public AbstractDialogModel { } containerInfo* lookupContainerByName(SCP_string name){ - for (int x = 0; x < static_cast(_containerItems.size())){ - if (_containerItems.name == name){ + for (int x = 0; x < static_cast(_containerItems.size()); ++x) { + if (_containerItems[x].name == name) { return &_containerItems[x]; } } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 70567b3d840..489527eec92 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1,6 +1,7 @@ #include "VariableDialog.h" #include "ui_VariableDialog.h" +#include #include #include @@ -20,7 +21,6 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) resize(QDialog::sizeHint()); // The best I can tell without some research, when a dialog doesn't use an underling grid or layout, it needs to be resized this way before anything will show up // Major Changes, like Applying the model, rejecting changes and updating the UI. - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &VariableDialog::updateUI); connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::checkValidModel); connect(this, &QDialog::rejected, _model.get(), &VariableDialogModel::reject); @@ -233,7 +233,7 @@ void VariableDialog::onVariablesTableUpdated() if (item->column() == 0){ // so if the user just removed the name, mark it as deleted *before changing the name* - if (_currentVariable != "" && !strlen(item->text.c_str())){ + if (_currentVariable != "" && !strlen(item->text().toStdString().c_str())) { if (!_model->removeVariable(item->row())) { // marking a variable as deleted failed, resync UI applyModel(); @@ -246,7 +246,7 @@ void VariableDialog::onVariablesTableUpdated() auto ret = _model->changeVariableName(item->row(), item->text().toStdString()); // we put something in the cell, but the model couldn't process it. - if (strlen(item->text()) && ret == ""){ + if (strlen(item->text().toStdString().c_str()) && ret == ""){ // update of variable name failed, resync UI applyModel(); @@ -262,11 +262,11 @@ void VariableDialog::onVariablesTableUpdated() } else if (item->column() == 1) { // Variable is a string - if (_model->getVariableType(int->row())){ - SCP_string temp = item->text()->toStdString().c_str(); + if (_model->getVariableType(item->row())){ + SCP_string temp = item->text().toStdString().c_str(); temp = temp.substr(0, NAME_LENGTH - 1); - SCP_string ret = _model->setVariableStringValue(int->row(), temp); + SCP_string ret = _model->setVariableStringValue(item->row(), temp); if (ret == ""){ applyModel(); return; @@ -274,10 +274,8 @@ void VariableDialog::onVariablesTableUpdated() item->setText(ret.c_str()); } else { - SCP_string temp; SCP_string source = item->text().toStdString(); - - SCP_string temp = trimNumberString(); + SCP_string temp = trimNumberString(source); if (temp != source){ item->setText(temp.c_str()); @@ -287,7 +285,7 @@ void VariableDialog::onVariablesTableUpdated() int ret = _model->setVariableNumberValue(item->row(), std::stoi(temp)); temp = ""; sprintf(temp, "%i", ret); - item->setText(temp); + item->setText(temp.c_str()); } catch (...) { applyModel(); @@ -374,7 +372,7 @@ void VariableDialog::onContainerContentsSelectionChanged() { return; } - auto items = ui->containersContentsTable->selectedItems(); + auto items = ui->containerContentsTable->selectedItems(); SCP_string newContainerItemName = ""; @@ -394,7 +392,7 @@ void VariableDialog::onContainerContentsSelectionChanged() { void VariableDialog::onAddVariableButtonPressed() { - auto ret = _model->addNewVriable(); + auto ret = _model->addNewVariable(); _currentVariable = ret; applyModel(); } @@ -405,9 +403,15 @@ void VariableDialog::onCopyVariableButtonPressed() return; } - auto ret = _model->copyVariable(_currentVariable); - _currentVariable = ret; - applyModel(); + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for(const auto& item : items) { + auto ret = _model->copyVariable(item->row()); + _currentVariable = ret; + applyModel(); + break; + } } void VariableDialog::onDeleteVariableButtonPressed() @@ -416,9 +420,15 @@ void VariableDialog::onDeleteVariableButtonPressed() return; } - // Because of the text update we'll need, this needs an applyModel, whether it fails or not. - _model->removeVariable(_currentVariable); - applyModel(); + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for(const auto& item : items) { + // Because of the text update we'll need, this needs an applyModel, whether it fails or not. + auto ret = _model->removeVariable(item->row()); + applyModel(); + break; + } } void VariableDialog::onSetVariableAsStringRadioSelected() @@ -427,14 +437,23 @@ void VariableDialog::onSetVariableAsStringRadioSelected() return; } - // this doesn't return succeed or fail directly, - // but if it doesn't return true then it failed since this is the string radio - if(!_model->setVariableType(_currentVariable, true)){ - applyModel(); - } else { - ui->setVariableAsStringRadio->setChecked(true); - ui->setVariableAsNumberRadio->setChecked(false); + + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for(const auto& item : items) { + // this doesn't return succeed or fail directly, + // but if it doesn't return true then it failed since this is the string radio + if(!_model->setVariableType(item->row(), true)){ + applyModel(); + } else { + ui->setVariableAsStringRadio->setChecked(true); + ui->setVariableAsNumberRadio->setChecked(false); + } + + break; } + } void VariableDialog::onSetVariableAsNumberRadioSelected() @@ -443,13 +462,20 @@ void VariableDialog::onSetVariableAsNumberRadioSelected() return; } - // this doesn't return succeed or fail directly, - // but if it doesn't return false then it failed since this is the number radio - if(!_model->setVariableType(_currentVariable, false)){ - applyModel(); - } else { - ui->setVariableAsStringRadio->setChecked(false); - ui->setVariableAsNumberRadio->setChecked(true); + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + + // this doesn't return succeed or fail directly, + // but if it doesn't return false then it failed since this is the number radio + if (!_model->setVariableType(item->row(), false)) { + applyModel(); + } + else { + ui->setVariableAsStringRadio->setChecked(false); + ui->setVariableAsNumberRadio->setChecked(true); + } } } @@ -460,15 +486,20 @@ void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() return; } - auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(_currentVariable, 1); + auto items = ui->variablesTable->selectedItems(); - if (ret != 1){ - applyModel(); - } else { - // TODO! Need "no persistence" options and functions! - ui->saveContainerOnMissionCompletedRadio->setChecked(true); - ui->saveVariableOnMissionCloseRadio->setChecked(false); - //ui->saveContainerOnMissionCompletedRadio->setChecked(true); + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(item->row(), 1); + + if (ret != 1){ + applyModel(); + } else { + // TODO! Need "no persistence" options and functions! + ui->saveContainerOnMissionCompletedRadio->setChecked(true); + ui->saveVariableOnMissionCloseRadio->setChecked(false); + //ui->saveContainerOnMissionCompletedRadio->setChecked(true); + } } } @@ -478,15 +509,22 @@ void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() return; } - auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(_currentVariable, 2); - if (ret != 2){ - applyModel(); - } else { - // TODO! Need "no persistence" options. - ui->saveContainerOnMissionCompletedRadio->setChecked(false); - ui->saveVariableOnMissionCloseRadio->setChecked(true); - //ui->saveContainerOnMissionCompletedRadio->setChecked(false); + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(item->row(), 2); + + if (ret != 2){ + applyModel(); + } else { + // TODO! Need "no persistence" options. + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(true); + //ui->saveContainerOnMissionCompletedRadio->setChecked(false); + } } } @@ -496,11 +534,17 @@ void VariableDialog::onSaveVariableAsEternalCheckboxClicked() return; } - // If the model returns the old status, then the change failed and we're out of sync. - if (ui->setVariableAsEternalcheckbox->isChecked() == _model->setVariableEternalFlag(_currentVariable, !ui->setVariableAsEternalcheckbox->isChecked())){ - applyModel(); - } else { - _ui->setVariableAsEternalcheckbox->setChecked(!ui->setVariableAsEternalcheckbox->isChecked()); + + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + // If the model returns the old status, then the change failed and we're out of sync. + if (ui->setVariableAsEternalcheckbox->isChecked() == _model->setVariableEternalFlag(item->row(), !ui->setVariableAsEternalcheckbox->isChecked())) { + applyModel(); + } else { + ui->setVariableAsEternalcheckbox->setChecked(!ui->setVariableAsEternalcheckbox->isChecked()); + } } } @@ -510,11 +554,17 @@ void VariableDialog::onNetworkVariableCheckboxClicked() return; } - // If the model returns the old status, then the change failed and we're out of sync. - if (ui->setVariableNetworkStatus->isChecked() == _model->setVariableNetworkStatus(_currentVariable, !ui->setVariableNetworkStatus->isChecked())){ - applyModel(); - } else { - _ui->setVariableNetworkStatus->setChecked(!ui->setVariableNetworkStatus->isChecked()); + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + + // If the model returns the old status, then the change failed and we're out of sync. + if (ui->networkVariableCheckbox->isChecked() == _model->setVariableNetworkStatus(item->row(), !ui->networkVariableCheckbox->isChecked())) { + applyModel(); + } else { + ui->networkVariableCheckbox->setChecked(!ui->networkVariableCheckbox->isChecked()); + } } } @@ -547,28 +597,28 @@ void VariableDialog::applyModel() for (x = 0; x < static_cast(variables.size()); ++x){ if (ui->variablesTable->item(x, 0)){ - ui->variablesTable->item(x, 0)->setText(variables[x]<0>.c_str()); + ui->variablesTable->item(x, 0)->setText(variables[x][0].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(variables[x]<0>.c_str()); + QTableWidgetItem* item = new QTableWidgetItem(variables[x][0].c_str()); ui->variablesTable->setItem(x, 0, item); } // check if this is the current variable. - if (!_currentVariable.empty() && variables[x]<0> == _currentVariable){ - selectedRow = x + if (!_currentVariable.empty() && variables[x][0].c_str() == _currentVariable){ + selectedRow = x; } if (ui->variablesTable->item(x, 1)){ - ui->variablesTable->item(x, 1)->setText(variables[x]<1>.c_str()); + ui->variablesTable->item(x, 1)->setText(variables[x][1].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(variables[x]<1>.c_str()); - ui->variablesTable->setItem(x, 1, nameItem); + QTableWidgetItem* item = new QTableWidgetItem(variables[x][1].c_str()); + ui->variablesTable->setItem(x, 1, item); } if (ui->variablesTable->item(x, 2)){ - ui->variablesTable->item(x, 2)->setText(variables[x]<2>.c_str()); + ui->variablesTable->item(x, 2)->setText(variables[x][2].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(variables[x]<2>.c_str()); + QTableWidgetItem* item = new QTableWidgetItem(variables[x][2].c_str()); ui->variablesTable->setItem(x, 2, item); } } @@ -593,8 +643,8 @@ void VariableDialog::applyModel() } if (_currentVariable.empty() || selectedRow < 0){ - if (ui->variablesTable->item(0,0) && strlen(ui->variablesTable->item(0,0)->text())){ - _currentVariable = ui->variablesTable->item(0,0)->text(); + if (ui->variablesTable->item(0,0) && strlen(ui->variablesTable->item(0,0)->text().toStdString().c_str())){ + _currentVariable = ui->variablesTable->item(0,0)->text().toStdString(); } } @@ -606,29 +656,29 @@ void VariableDialog::applyModel() // TODO! Change getContainerNames to a tuple with notes/maybe data key types? for (x = 0; x < static_cast(containers.size()); ++x){ if (ui->containersTable->item(x, 0)){ - ui->containersTable->item(x, 0)->setText(containers[x]<0>.c_str()); + ui->containersTable->item(x, 0)->setText(containers[x][0].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(containers[x]<0>.c_str()); + QTableWidgetItem* item = new QTableWidgetItem(containers[x][0].c_str()); ui->containersTable->setItem(x, 0, item); } // check if this is the current variable. - if (!_currentVariable.empty() && containers[x]<0> == _currentVariable){ + if (!_currentVariable.empty() && containers[x][0] == _currentVariable){ selectedRow = x; } if (ui->containersTable->item(x, 1)){ - ui->containersTable->item(x, 1)->setText(containers[x]<1>.c_str()); + ui->containersTable->item(x, 1)->setText(containers[x][1].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(containers[x]<1>.c_str()); - ui->containersTable->setItem(x, 1, nameItem); + QTableWidgetItem* item = new QTableWidgetItem(containers[x][1].c_str()); + ui->containersTable->setItem(x, 1, item); } if (ui->containersTable->item(x, 2)){ - ui->containersTable->item(x, 2)->setText(containers[x]<2>.c_str()); + ui->containersTable->item(x, 2)->setText(containers[x][2].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(containers[x]<2>.c_str()); + QTableWidgetItem* item = new QTableWidgetItem(containers[x][2].c_str()); ui->containersTable->setItem(x, 2, item); } } @@ -652,8 +702,8 @@ void VariableDialog::applyModel() } if (_currentContainer.empty() || selectedRow < 0){ - if (ui->containersTable->item(0,0) && strlen(ui->containersTable->item(0,0)->text())){ - _currentContainer = ui->containersTable->item(0,0)->text(); + if (ui->containersTable->item(0,0) && strlen(ui->containersTable->item(0,0)->text().toStdString().c_str())){ + _currentContainer = ui->containersTable->item(0,0)->text().toStdString(); } } @@ -666,91 +716,108 @@ void VariableDialog::applyModel() void VariableDialog::updateVariableOptions() { if (_currentVariable.empty()){ - ui->copyVariableButton.setEnabled(false); - ui->deleteVariableButton.setEnabled(false); - ui->setVariableAsStringRadio.setEnabled(false); - ui->setVariableAsNumberRadio.setEnabled(false); - ui->saveVariableOnMissionCompletedRadio.setEnabled(false); - ui->saveVariableOnMissionCloseRadio.setEnabled(false); - ui->setVariableAsEternalcheckbox.setEnabled(false); + ui->copyVariableButton->setEnabled(false); + ui->deleteVariableButton->setEnabled(false); + ui->setVariableAsStringRadio->setEnabled(false); + ui->setVariableAsNumberRadio->setEnabled(false); + ui->saveVariableOnMissionCompletedRadio->setEnabled(false); + ui->saveVariableOnMissionCloseRadio->setEnabled(false); + ui->setVariableAsEternalcheckbox->setEnabled(false); return; } - ui->copyVariableButton.setEnabled(true); - ui->deleteVariableButton.setEnabled(true); - ui->setVariableAsStringRadio.setEnabled(true); - ui->setVariableAsNumberRadio.setEnabled(true); - ui->saveVariableOnMissionCompletedRadio.setEnabled(true); - ui->saveVariableOnMissionCloseRadio.setEnabled(true); - ui->setVariableAsEternalcheckbox.setEnabled(true); + ui->copyVariableButton->setEnabled(true); + ui->deleteVariableButton->setEnabled(true); + ui->setVariableAsStringRadio->setEnabled(true); + ui->setVariableAsNumberRadio->setEnabled(true); + ui->saveVariableOnMissionCompletedRadio->setEnabled(true); + ui->saveVariableOnMissionCloseRadio->setEnabled(true); + ui->setVariableAsEternalcheckbox->setEnabled(true); + + auto items = ui->variablesTable->selectedItems(); + int row = -1; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + row = item->row(); + } // start populating values - bool string = _model->getVariableType(_currentVariable); - ui->setVariableAsStringRadio.setChecked(string); - ui->setVariableAsNumberRadio.setChecked(!string); - ui->setVariableAsEternalcheckbox.setChecked(); + bool string = _model->getVariableType(row); + ui->setVariableAsStringRadio->setChecked(string); + ui->setVariableAsNumberRadio->setChecked(!string); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); - int ret = _model->getVariableOnMissionCloseOrCompleteFlag(_currentVariable); + int ret = _model->getVariableOnMissionCloseOrCompleteFlag(row); if (ret == 0){ // TODO ADD NO PERSISTENCE } else if (ret == 1) { - ui->saveVariableOnMissionCompletedRadio.setChecked(true); - ui->saveVariableOnMissionCloseRadio.setChecked(false); + ui->saveVariableOnMissionCompletedRadio->setChecked(true); + ui->saveVariableOnMissionCloseRadio->setChecked(false); } else { - ui->saveVariableOnMissionCompletedRadio.setChecked(false); - ui->saveVariableOnMissionCloseRadio.setChecked(true); + ui->saveVariableOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(true); } - ui->networkVariableCheckbox.setChecked(_model->getVariableNetworkStatus(_currentVariable)); - ui->setVariableAsEternalcheckbox.setChecked(_model->getVariableEternalFlag(_currentVariable)); + ui->networkVariableCheckbox->setChecked(_model->getVariableNetworkStatus(row)); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); } void VariableDialog::updateContainerOptions() { if (_currentContainer.empty()){ - ui->copyContainerButton.setEnabled(false); - ui->deleteContainerButton.setEnabled(false); - ui->setContainerAsStringRadio.setEnabled(false); - ui->setContainerAsNumberRadio.setEnabled(false); - ui->saveContainerOnMissionCompletedRadio.setEnabled(false); - ui->saveContainerOnMissionCloseRadio.setEnabled(false); - ui->setContainerAsEternalcheckbox.setEnabled(false); - ui->setContainerAsMapRadio.setEnabled(false); - ui->setContainerAsListRadio.setEnabled(false); + ui->copyContainerButton->setEnabled(false); + ui->deleteContainerButton->setEnabled(false); + ui->setContainerAsStringRadio->setEnabled(false); + ui->setContainerAsNumberRadio->setEnabled(false); + ui->saveContainerOnMissionCompletedRadio->setEnabled(false); + ui->saveContainerOnMissionCloseRadio->setEnabled(false); + ui->setContainerAsEternalCheckbox->setEnabled(false); + ui->setContainerAsMapRadio->setEnabled(false); + ui->setContainerAsListRadio->setEnabled(false); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); } else { - ui->copyContainerButton.setEnabled(false); - ui->deleteContainerButton.setEnabled(false); - ui->setContainerAsStringRadio.setEnabled(false); - ui->setContainerAsNumberRadio.setEnabled(false); - ui->saveContainerOnMissionCompletedRadio.setEnabled(false); - ui->saveContainerOnMissionCloseRadio.setEnabled(false); - ui->setContainerAsEternalcheckbox.setEnabled(false); - ui->setContainerAsMapRadio.setEnabled(false); - ui->setContainerAsListRadio.setEnabled(false); - - if (_model->getContainerType(_currentContainer)){ - ui->setContainerAsStringRadio.setChecked(true); - ui->setContainerAsNumberRadio.setChecked(false); + auto items = ui->containersTable->selectedItems(); + int row = -1; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + row = item->row(); + } + + + ui->copyContainerButton->setEnabled(false); + ui->deleteContainerButton->setEnabled(false); + ui->setContainerAsStringRadio->setEnabled(false); + ui->setContainerAsNumberRadio->setEnabled(false); + ui->saveContainerOnMissionCompletedRadio->setEnabled(false); + ui->saveContainerOnMissionCloseRadio->setEnabled(false); + ui->setContainerAsEternalCheckbox->setEnabled(false); + ui->setContainerAsMapRadio->setEnabled(false); + ui->setContainerAsListRadio->setEnabled(false); + + if (_model->getContainerValueType(row)){ + ui->setContainerAsStringRadio->setChecked(true); + ui->setContainerAsNumberRadio->setChecked(false); } else { - ui->setContainerAsStringRadio.setChecked(false); - ui->setContainerAsNumberRadio.setChecked(true); + ui->setContainerAsStringRadio->setChecked(false); + ui->setContainerAsNumberRadio->setChecked(true); } - if (_model->getConainerListOrMap(_currentContainer)){ - ui->setContainerAsListRadio.setChecked(true); - ui->setContainerAsMapRadio.setChecked(false); + if (_model->getContainerListOrMap(row)){ + ui->setContainerAsListRadio->setChecked(true); + ui->setContainerAsMapRadio->setChecked(false); // Disable Key Controls - ui->setContainerKeyAsStringRadio.setEnabled(false); - ui->setContainerKeyAsNumberRadio.setEnabled(false); + ui->setContainerKeyAsStringRadio->setEnabled(false); + ui->setContainerKeyAsNumberRadio->setEnabled(false); // Don't forget to change headings ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); @@ -758,12 +825,12 @@ void VariableDialog::updateContainerOptions() updateContainerDataOptions(true); } else { - ui->setContainerAsListRadio.setChecked(false); - ui->setContainerAsMapRadio.setChecked(true); + ui->setContainerAsListRadio->setChecked(false); + ui->setContainerAsMapRadio->setChecked(true); // Enabled Key Controls - ui->setContainerKeyAsStringRadio.setEnabled(true); - ui->setContainerKeyAsNumberRadio.setEnabled(true); + ui->setContainerKeyAsStringRadio->setEnabled(true); + ui->setContainerKeyAsNumberRadio->setEnabled(true); // Don't forget to change headings ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); @@ -771,19 +838,19 @@ void VariableDialog::updateContainerOptions() updateContainerDataOptions(false); } - ui->setContainerAsEternalcheckbox.setChecked(_model->getContainerNetworkStatus(_currentContainer)); - ui->networkContainerCheckbox.setChecked(_model->getContainerNetworkStatus(_currentContainer)); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerNetworkStatus(row)); + ui->networkContainerCheckbox->setChecked(_model->getContainerNetworkStatus(row)); - int ret = getContainerOnMissionCloseOrCompleteFlag(_currentContainer); + int ret = _model->getContainerOnMissionCloseOrCompleteFlag(row); if (ret == 0){ // TODO ADD NO PERSISTENCE } else if (ret == 1) { - ui->saveContainerOnMissionCompletedRadio.setChecked(true); - ui->saveContainerOnMissionCloseRadio.setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(true); + ui->saveContainerOnMissionCloseRadio->setChecked(false); } else { - ui->saveContainerOnMissionCompletedRadio.setChecked(false); - ui->saveContainerOnMissionCloseRadio.setChecked(true); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(true); } } @@ -799,13 +866,15 @@ SCP_string VariableDialog::trimNumberString(SCP_string source) SCP_string ret; // account for a lead negative sign. - if (source[0] == "-") { + if (source[0] == '-') { ret = "-"; } // filter out non-numeric digits - std::copy_if(s1.begin(), s1.end(), std::back_inserter(ret), - [](char c){ + std::copy_if(source.begin(), source.end(), std::back_inserter(ret), + [](char c) -> bool { + bool result = false; + switch (c) { case '0': case '1': @@ -817,12 +886,13 @@ SCP_string VariableDialog::trimNumberString(SCP_string source) case '7': case '8': case '9': - return true; + result = true; break; default: - return false; break; } + + return result; } ); diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 6349adbd293..f803dbdf295 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -56,7 +56,7 @@ class VariableDialog : public QDialog { void onSetContainerAsStringRadioSelected(); void onSetContainerAsNumberRadioSelected(); void onSetContainerKeyAsStringRadioSelected(); - void onSetContainerKeyAsNumberRadioSelected() + void onSetContainerKeyAsNumberRadioSelected(); void onSaveContainerOnMissionClosedRadioSelected(); void onSaveContainerOnMissionCompletedRadioSelected(); void onNetworkContainerCheckboxClicked(); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index b85a2fc68a9..69299605fb9 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -6,8 +6,8 @@ 0 0 - 600 - 649 + 601 + 701 @@ -84,21 +84,30 @@ 10 30 231 - 131 + 151 + + QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + 10 - 170 + 190 341 191 - Contents + Container Contents @@ -109,6 +118,15 @@ 151 + + QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + @@ -193,7 +211,7 @@ 360 20 211 - 141 + 161 @@ -205,10 +223,17 @@ 10 20 201 - 121 + 136 + + + + No Persistence + + + @@ -244,7 +269,7 @@ 360 - 170 + 190 211 191 @@ -357,13 +382,13 @@ 260 - 100 - 311 - 121 + 90 + 131 + 131 - Options + Type @@ -400,31 +425,6 @@ - - - - - - Save on Mission Close - - - - - - - Save on Mission Completed - - - - - - - Eternal - - - - - @@ -437,14 +437,23 @@ 191 + + QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + 270 - 38 - 301 - 51 + 50 + 291 + 31 @@ -471,6 +480,59 @@ + + + + 400 + 90 + 171 + 131 + + + + Persistence + + + + + 13 + 18 + 174 + 108 + + + + + + + No Persistence + + + + + + + Save on Mission Close + + + + + + + Save on Mission Completed + + + + + + + Eternal + + + + + + From 364e39cedfecadde3853787b7e0f5a0e08709129 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Tue, 16 Apr 2024 23:24:26 -0400 Subject: [PATCH 091/466] more fixes --- .../src/mission/dialogs/VariableDialogModel.cpp | 16 ++++++++++------ qtfred/src/ui/dialogs/LoadoutDialog.cpp | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 59b8b99a681..2d3b3b74001 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -436,6 +436,7 @@ SCP_string VariableDialogModel::setVariableStringValue(int index, SCP_string val } variable->stringValue = value; + return value; } int VariableDialogModel::setVariableNumberValue(int index, int value) @@ -448,6 +449,8 @@ int VariableDialogModel::setVariableNumberValue(int index, int value) } variable->numberValue = value; + + return value; } SCP_string VariableDialogModel::addNewVariable() @@ -1108,9 +1111,9 @@ const SCP_vector& VariableDialogModel::getNumberValues(int index) return container->numberValues; } -const SCP_vector> VariableDialogModel::getVariableValues() +const SCP_vector> VariableDialogModel::getVariableValues() { - SCP_vector> outStrings; + SCP_vector> outStrings; for (const auto& item : _variableItems) { SCP_string notes = ""; @@ -1127,16 +1130,16 @@ const SCP_vector> VariableDialogM SCP_string temp; sprintf(temp, "%i", item.numberValue); - outStrings.emplace_back(item.name, (item.string) ? item.stringValue : temp, notes); + outStrings.push_back(std::array{item.name, (item.string) ? item.stringValue : temp, notes}); } return outStrings; } -const SCP_vector> VariableDialogModel::getContainerNames() +const SCP_vector> VariableDialogModel::getContainerNames() { - SCP_vector> outStrings; + SCP_vector> outStrings; for (const auto& item : _containerItems) { SCP_string notes = ""; @@ -1149,7 +1152,8 @@ const SCP_vector> VariableDialogModel::getCont notes = "Renamed"; } - outStrings.emplace_back(item.name, notes); + //TODO! FIX ME + outStrings.push_back(std::array{item.name, item.name, notes}); } return outStrings; diff --git a/qtfred/src/ui/dialogs/LoadoutDialog.cpp b/qtfred/src/ui/dialogs/LoadoutDialog.cpp index c115656b247..83678409d45 100644 --- a/qtfred/src/ui/dialogs/LoadoutDialog.cpp +++ b/qtfred/src/ui/dialogs/LoadoutDialog.cpp @@ -516,7 +516,8 @@ void LoadoutDialog::onClearAllUsedWeaponsPressed() void LoadoutDialog::openEditVariablePressed() { - viewport->on_actionVariables_triggered(); + //TODO! FIX ME! + //viewport->on_actionVariables_triggered(); } void LoadoutDialog::onSelectionRequiredPressed() From bd75eff4e447e3b3246e0d9cda5d8b982f928d14 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Wed, 17 Apr 2024 00:05:36 -0400 Subject: [PATCH 092/466] Make sure that tables have contents And fixes and progress --- qtfred/src/ui/dialogs/VariableDialog.cpp | 30 ++-- qtfred/ui/VariableDialog.ui | 192 +++++++++++------------ 2 files changed, 114 insertions(+), 108 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 489527eec92..01561500e87 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -184,25 +184,25 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->variablesTable->setColumnWidth(0, 200); - ui->variablesTable->setColumnWidth(1, 200); - ui->variablesTable->setColumnWidth(2, 200); + ui->variablesTable->setColumnWidth(0, 70); + ui->variablesTable->setColumnWidth(1, 70); + ui->variablesTable->setColumnWidth(2, 70); ui->containersTable->setColumnCount(3); ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->containersTable->setColumnWidth(0, 200); - ui->containersTable->setColumnWidth(1, 200); - ui->containersTable->setColumnWidth(2, 200); + ui->containersTable->setColumnWidth(0, 70); + ui->containersTable->setColumnWidth(1, 70); + ui->containersTable->setColumnWidth(2, 70); ui->containerContentsTable->setColumnCount(2); // Default to list ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setColumnWidth(0, 200); - ui->containerContentsTable->setColumnWidth(1, 200); + ui->containerContentsTable->setColumnWidth(0, 105); + ui->containerContentsTable->setColumnWidth(1, 105); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't ui->setVariableAsStringRadio->setAutoExclusive(false); @@ -595,6 +595,8 @@ void VariableDialog::applyModel() auto variables = _model->getVariableValues(); int x, selectedRow = -1; + ui->variablesTable->setRowCount(static_cast(variables.size())); + for (x = 0; x < static_cast(variables.size()); ++x){ if (ui->variablesTable->item(x, 0)){ ui->variablesTable->item(x, 0)->setText(variables[x][0].c_str()); @@ -604,7 +606,7 @@ void VariableDialog::applyModel() } // check if this is the current variable. - if (!_currentVariable.empty() && variables[x][0].c_str() == _currentVariable){ + if (!_currentVariable.empty() && variables[x][0] == _currentVariable){ selectedRow = x; } @@ -623,7 +625,7 @@ void VariableDialog::applyModel() } } - // TODO, try setting row count? + /* // This empties rows that might have previously had variables if (x < ui->variablesTable->rowCount()) { ++x; @@ -641,6 +643,7 @@ void VariableDialog::applyModel() } } } + */ if (_currentVariable.empty() || selectedRow < 0){ if (ui->variablesTable->item(0,0) && strlen(ui->variablesTable->item(0,0)->text().toStdString().c_str())){ @@ -651,6 +654,7 @@ void VariableDialog::applyModel() updateVariableOptions(); auto containers = _model->getContainerNames(); + ui->containersTable->setRowCount(static_cast(containers.size())); selectedRow = -1; // TODO! Change getContainerNames to a tuple with notes/maybe data key types? @@ -743,6 +747,12 @@ void VariableDialog::updateVariableOptions() row = item->row(); } + if (row == -1 && ui->variablesTable->rowCount() > 0) { + row = 0; + _currentVariable = ui->variablesTable->item(row, 0)->text().toStdString(); + } + + // start populating values bool string = _model->getVariableType(row); ui->setVariableAsStringRadio->setChecked(string); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 69299605fb9..28c134d1aa1 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -6,14 +6,14 @@ 0 0 - 601 + 608 701 - 600 - 640 + 608 + 701 @@ -83,7 +83,7 @@ 10 30 - 231 + 251 151 @@ -114,7 +114,7 @@ 10 30 - 221 + 241 151 @@ -131,9 +131,9 @@ - 240 + 260 30 - 91 + 71 91 @@ -141,14 +141,14 @@ - Add Data + Add - Copy Data + Copy @@ -160,7 +160,7 @@ - Delete Data + Delete @@ -170,9 +170,9 @@ - 250 - 40 - 97 + 270 + 30 + 71 91 @@ -180,14 +180,14 @@ - Add Container + Add - Copy Container + Copy @@ -199,7 +199,7 @@ - Delete Container + Delete @@ -235,16 +235,16 @@ - + - Save on Mission Close + Save on Mission Completed - + - Save on Mission Completed + Save on Mission Close @@ -258,7 +258,7 @@ - Network-Variable + Network Variable @@ -382,48 +382,37 @@ 260 - 90 - 131 - 131 + 120 + 81 + 101 Type - + 10 20 - 300 - 101 + 71 + 80 - + - - - - - String - - - - - - - Number - - - - - - - Network-Variable - - - - + + + String + + + + + + + Number + + @@ -447,58 +436,25 @@ QAbstractItemView::SelectRows - - - - 270 - 50 - 291 - 31 - - - - - - - Add Variable - - - - - - - Copy Variable - - - - - - - Delete Variable - - - - - - 400 - 90 - 171 - 131 + 350 + 20 + 231 + 201 - Persistence + Persistence Options - + - 13 - 18 - 174 - 108 + 10 + 20 + 221 + 181 @@ -510,16 +466,16 @@ - + - Save on Mission Close + Save on Mission Completed - + - Save on Mission Completed + Save on Mission Close @@ -530,9 +486,49 @@ + + + + Network Variable + + + + + + + 270 + 30 + 71 + 86 + + + + + + + Add + + + + + + + Copy + + + + + + + Delete + + + + + From 24310690f6dd4b212c5420a868dd98b611c9c6b7 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Wed, 17 Apr 2024 00:26:42 -0400 Subject: [PATCH 093/466] more progress --- qtfred/ui/VariableDialog.ui | 390 +++++++++++++++++------------------- 1 file changed, 183 insertions(+), 207 deletions(-) diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 28c134d1aa1..6cd2503ec8a 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -7,67 +7,195 @@ 0 0 608 - 701 + 636 608 - 701 + 560 - Fiction Viewer Editor + Variables Editor - - - QLayout::SetMaximumSize + + + + 0 + 0 + - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 12 - - - - Variables and Containers Editor + + Variables + + + + + 270 + 120 + 71 + 101 + + + + Type Options + + + + + 0 + 20 + 116 + 80 + + + + + + String + + + + + + + Number + + + + - - - - - Qt::Horizontal - - - - 40 - 20 - + + + + + 10 + 30 + 251 + 191 + + + + QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + Qt::DotLine + + + false + + + false + + + + + + 360 + 20 + 221 + 201 + + + + Persistence Options + + + + + 10 + 20 + 221 + 181 + - - - + + + + + No Persistence + + + + + + + Save on Mission Completed + + + + + + + Save on Mission Close + + + + + + + Eternal + + + + + + + Network Variable + + + + + + + + + + 270 + 30 + 71 + 86 + + + + + + + Add + + + + + + + Copy + + + + + + + Delete + + + + + + - + @@ -96,6 +224,12 @@ QAbstractItemView::SelectRows + + Qt::DotLine + + + false + @@ -127,6 +261,12 @@ QAbstractItemView::SelectRows + + Qt::DotLine + + + false + @@ -367,170 +507,6 @@ - - - - - 0 - 230 - - - - Variables - - - - - 260 - 120 - 81 - 101 - - - - Type - - - - - 10 - 20 - 71 - 80 - - - - - - - String - - - - - - - Number - - - - - - - - - - 10 - 30 - 241 - 191 - - - - QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - - - - 350 - 20 - 231 - 201 - - - - Persistence Options - - - - - 10 - 20 - 221 - 181 - - - - - - - No Persistence - - - - - - - Save on Mission Completed - - - - - - - Save on Mission Close - - - - - - - Eternal - - - - - - - Network Variable - - - - - - - - - - 270 - 30 - 71 - 86 - - - - - - - Add - - - - - - - Copy - - - - - - - Delete - - - - - - - From a57949c74788cd9209081322ff13ac8ce1f71f41 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Wed, 17 Apr 2024 00:27:22 -0400 Subject: [PATCH 094/466] getting closer to correct width --- qtfred/src/ui/dialogs/VariableDialog.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 01561500e87..8a8f1f21808 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -192,9 +192,9 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->containersTable->setColumnWidth(0, 70); - ui->containersTable->setColumnWidth(1, 70); - ui->containersTable->setColumnWidth(2, 70); + ui->containersTable->setColumnWidth(0, 80); + ui->containersTable->setColumnWidth(1, 80); + ui->containersTable->setColumnWidth(2, 75); ui->containerContentsTable->setColumnCount(2); From 6fe5ab7dc7505ab1422a9e988913da9082e25b92 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 17 Apr 2024 16:29:11 -0400 Subject: [PATCH 095/466] Fix No Persistence not updating And other fixes --- qtfred/src/ui/dialogs/VariableDialog.cpp | 71 +++++++++++++++++------- qtfred/src/ui/dialogs/VariableDialog.h | 2 + qtfred/ui/VariableDialog.ui | 2 +- 3 files changed, 53 insertions(+), 22 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 01561500e87..c34df97f64c 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -18,7 +18,8 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) { this->setFocus(); ui->setupUi(this); - resize(QDialog::sizeHint()); // The best I can tell without some research, when a dialog doesn't use an underling grid or layout, it needs to be resized this way before anything will show up + resize(QDialog::sizeHint()); // The best I can tell without some research, when a dialog doesn't use an underlying grid or layout, it needs to be resized this way before anything will show up + // Major Changes, like Applying the model, rejecting changes and updating the UI. connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::checkValidModel); @@ -79,6 +80,11 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onSetVariableAsNumberRadioSelected); + connect(ui->doNotSaveVariableRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onDoNotSaveVariableRadioSelected) + connect(ui->saveContainerOnMissionCompletedRadio, &QRadioButton::toggled, this, @@ -144,6 +150,11 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onSetContainerKeyAsNumberRadioSelected); + connect(ui->doNotSaveContainerRadio, + &QRadioButton::toggled, + this, + &VariableDialog::onDoNotSaveContainerRadioSelected) + connect(ui->saveContainerOnMissionCloseRadio, &QRadioButton::toggled, this, @@ -184,25 +195,27 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->variablesTable->setColumnWidth(0, 70); - ui->variablesTable->setColumnWidth(1, 70); + ui->variablesTable->setColumnWidth(0, 90); + ui->variablesTable->setColumnWidth(1, 90); ui->variablesTable->setColumnWidth(2, 70); + // TODO! Make sure row 3 is not editable. ui->containersTable->setColumnCount(3); ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->containersTable->setColumnWidth(0, 70); - ui->containersTable->setColumnWidth(1, 70); + ui->containersTable->setColumnWidth(0, 90); + ui->containersTable->setColumnWidth(1, 90); ui->containersTable->setColumnWidth(2, 70); - + // TODO! Make sure row 3 is not editable. + ui->containerContentsTable->setColumnCount(2); // Default to list ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setColumnWidth(0, 105); - ui->containerContentsTable->setColumnWidth(1, 105); + ui->containerContentsTable->setColumnWidth(0, 125); + ui->containerContentsTable->setColumnWidth(1, 125); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't ui->setVariableAsStringRadio->setAutoExclusive(false); @@ -437,7 +450,6 @@ void VariableDialog::onSetVariableAsStringRadioSelected() return; } - auto items = ui->variablesTable->selectedItems(); // yes, selected items returns a list, but we really should only have one item because multiselect will be off. @@ -453,7 +465,6 @@ void VariableDialog::onSetVariableAsStringRadioSelected() break; } - } void VariableDialog::onSetVariableAsNumberRadioSelected() @@ -479,6 +490,11 @@ void VariableDialog::onSetVariableAsNumberRadioSelected() } } +void VariableDialog::onDoNotSaveVariableRadioSelected() +{ + +} + void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() { @@ -577,6 +593,7 @@ void VariableDialog::onSetContainerAsStringRadioSelected() {} void VariableDialog::onSetContainerAsNumberRadioSelected() {} void VariableDialog::onSetContainerKeyAsStringRadioSelected() {} void VariableDialog::onSetContainerKeyAsNumberRadioSelected() {} +void VariableDialog::onDoNotSaveContainerRadioSelected(){} void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() {} void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() {} void VariableDialog::onNetworkContainerCheckboxClicked() {} @@ -724,6 +741,7 @@ void VariableDialog::updateVariableOptions() ui->deleteVariableButton->setEnabled(false); ui->setVariableAsStringRadio->setEnabled(false); ui->setVariableAsNumberRadio->setEnabled(false); + ui->doNotSaveVariableRadio->setEnabled(false); ui->saveVariableOnMissionCompletedRadio->setEnabled(false); ui->saveVariableOnMissionCloseRadio->setEnabled(false); ui->setVariableAsEternalcheckbox->setEnabled(false); @@ -735,6 +753,7 @@ void VariableDialog::updateVariableOptions() ui->deleteVariableButton->setEnabled(true); ui->setVariableAsStringRadio->setEnabled(true); ui->setVariableAsNumberRadio->setEnabled(true); + ui->doNotSaveVariableRadio->setEnabled(true); ui->saveVariableOnMissionCompletedRadio->setEnabled(true); ui->saveVariableOnMissionCloseRadio->setEnabled(true); ui->setVariableAsEternalcheckbox->setEnabled(true); @@ -762,11 +781,15 @@ void VariableDialog::updateVariableOptions() int ret = _model->getVariableOnMissionCloseOrCompleteFlag(row); if (ret == 0){ - // TODO ADD NO PERSISTENCE + ui->doNotSaveVariableRadio->setChecked(true); + ui->saveVariableOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(false); } else if (ret == 1) { + ui->doNotSaveVariableRadio->setChecked(false); ui->saveVariableOnMissionCompletedRadio->setChecked(true); ui->saveVariableOnMissionCloseRadio->setChecked(false); } else { + ui->doNotSaveVariableRadio->setChecked(false); ui->saveVariableOnMissionCompletedRadio->setChecked(false); ui->saveVariableOnMissionCloseRadio->setChecked(true); } @@ -783,6 +806,7 @@ void VariableDialog::updateContainerOptions() ui->deleteContainerButton->setEnabled(false); ui->setContainerAsStringRadio->setEnabled(false); ui->setContainerAsNumberRadio->setEnabled(false); + ui->doNotSaveContainerRadio->setEnabled(false); ui->saveContainerOnMissionCompletedRadio->setEnabled(false); ui->saveContainerOnMissionCloseRadio->setEnabled(false); ui->setContainerAsEternalCheckbox->setEnabled(false); @@ -803,15 +827,16 @@ void VariableDialog::updateContainerOptions() } - ui->copyContainerButton->setEnabled(false); - ui->deleteContainerButton->setEnabled(false); - ui->setContainerAsStringRadio->setEnabled(false); - ui->setContainerAsNumberRadio->setEnabled(false); - ui->saveContainerOnMissionCompletedRadio->setEnabled(false); - ui->saveContainerOnMissionCloseRadio->setEnabled(false); - ui->setContainerAsEternalCheckbox->setEnabled(false); - ui->setContainerAsMapRadio->setEnabled(false); - ui->setContainerAsListRadio->setEnabled(false); + ui->copyContainerButton->setEnabled(true); + ui->deleteContainerButton->setEnabled(true); + ui->setContainerAsStringRadio->setEnabled(true); + ui->setContainerAsNumberRadio->setEnabled(true); + ui->doNotSaveContainerRadio->setEnabled(true); + ui->saveContainerOnMissionCompletedRadio->setEnabled(true); + ui->saveContainerOnMissionCloseRadio->setEnabled(true); + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsMapRadio->setEnabled(true); + ui->setContainerAsListRadio->setEnabled(true); if (_model->getContainerValueType(row)){ ui->setContainerAsStringRadio->setChecked(true); @@ -854,11 +879,15 @@ void VariableDialog::updateContainerOptions() int ret = _model->getContainerOnMissionCloseOrCompleteFlag(row); if (ret == 0){ - // TODO ADD NO PERSISTENCE + ui->doNotSaveContainerRadio->setChecked(true); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(false); } else if (ret == 1) { + ui->doNotSaveContainerRadio->setChecked(false); ui->saveContainerOnMissionCompletedRadio->setChecked(true); ui->saveContainerOnMissionCloseRadio->setChecked(false); } else { + ui->doNotSaveContainerRadio->setChecked(false); ui->saveContainerOnMissionCompletedRadio->setChecked(false); ui->saveContainerOnMissionCloseRadio->setChecked(true); } diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index f803dbdf295..d85c4aa0de7 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -43,6 +43,7 @@ class VariableDialog : public QDialog { void onCopyVariableButtonPressed(); void onSetVariableAsStringRadioSelected(); void onSetVariableAsNumberRadioSelected(); + void onDoNotSaveVariableRadioSelected(); void onSaveVariableOnMissionCompleteRadioSelected(); void onSaveVariableOnMissionCloseRadioSelected(); void onSaveVariableAsEternalCheckboxClicked(); @@ -57,6 +58,7 @@ class VariableDialog : public QDialog { void onSetContainerAsNumberRadioSelected(); void onSetContainerKeyAsStringRadioSelected(); void onSetContainerKeyAsNumberRadioSelected(); + void onDoNotSaveContainerRadioSelected(); void onSaveContainerOnMissionClosedRadioSelected(); void onSaveContainerOnMissionCompletedRadioSelected(); void onNetworkContainerCheckboxClicked(); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 6cd2503ec8a..a63a7c24a9d 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -123,7 +123,7 @@ - + No Persistence From 94057c5ef29835ab74c0df3c160376579fbbb667 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 18 Apr 2024 14:24:01 -0400 Subject: [PATCH 096/466] Clean up container data updates And move the trimString method. --- .../mission/dialogs/VariableDialogModel.cpp | 37 ++++++ .../src/mission/dialogs/VariableDialogModel.h | 4 + qtfred/src/ui/dialogs/VariableDialog.cpp | 122 +++++++----------- qtfred/src/ui/dialogs/VariableDialog.h | 1 - 4 files changed, 86 insertions(+), 78 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 2d3b3b74001..d289aa99ebd 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1159,6 +1159,43 @@ const SCP_vector> VariableDialogModel::getContainerNam return outStrings; } +SCP_string VariableDialogModel::trimNumberString(SCP_string source) +{ + SCP_string ret; + + // account for a lead negative sign. + if (source[0] == '-') { + ret = "-"; + } + + // filter out non-numeric digits + std::copy_if(source.begin(), source.end(), std::back_inserter(ret), + [](char c) -> bool { + bool result = false; + + switch (c) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + result = true; + break; + default: + break; + } + + return result; + } + ); + + return ret; +} } // dialogs } // fred diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 0933694a832..19b4b3cc4a6 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -30,6 +30,8 @@ struct containerInfo { // this will allow us to look up the original values used in the mission previously. SCP_string originalName = ""; + // I found out that keys could be strictly typed as numbers *after* finishing the majority of the model.... + // So I am just going to store numerical keys as strings and use a bool to differentiate. SCP_vector keys; SCP_vector numberValues; SCP_vector stringValues; @@ -177,6 +179,8 @@ class VariableDialogModel : public AbstractDialogModel { break; } } + + static SCP_string trimNumberString(SCP_string source); }; } // namespace dialogs diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index c34df97f64c..94c90b7bff0 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -288,7 +288,7 @@ void VariableDialog::onVariablesTableUpdated() item->setText(ret.c_str()); } else { SCP_string source = item->text().toStdString(); - SCP_string temp = trimNumberString(source); + SCP_string temp = _model->trimNumberString(source); if (temp != source){ item->setText(temp.c_str()); @@ -492,10 +492,28 @@ void VariableDialog::onSetVariableAsNumberRadioSelected() void VariableDialog::onDoNotSaveVariableRadioSelected() { + if (_currentVariable.empty() || ui->doNotSaveVariableRadio->isChecked()){ + return; + } + + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(item->row(), 1); + if (ret != 1){ + applyModel(); + } else { + ui->doNotSaveVariableRadio->setChecked(true); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(false); + } + } } + void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() { if (_currentVariable.empty() || ui->saveContainerOnMissionCompletedRadio->isChecked()){ @@ -511,10 +529,9 @@ void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() if (ret != 1){ applyModel(); } else { - // TODO! Need "no persistence" options and functions! + ui->doNotSaveVariableRadio->setChecked(false); ui->saveContainerOnMissionCompletedRadio->setChecked(true); ui->saveVariableOnMissionCloseRadio->setChecked(false); - //ui->saveContainerOnMissionCompletedRadio->setChecked(true); } } } @@ -536,10 +553,9 @@ void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() if (ret != 2){ applyModel(); } else { - // TODO! Need "no persistence" options. + ui->doNotSaveVariableRadio->setChecked(false); ui->saveContainerOnMissionCompletedRadio->setChecked(false); ui->saveVariableOnMissionCloseRadio->setChecked(true); - //ui->saveContainerOnMissionCompletedRadio->setChecked(false); } } } @@ -612,7 +628,7 @@ void VariableDialog::applyModel() auto variables = _model->getVariableValues(); int x, selectedRow = -1; - ui->variablesTable->setRowCount(static_cast(variables.size())); + ui->variablesTable->setRowCount(static_cast(variables.size() + 1)); for (x = 0; x < static_cast(variables.size()); ++x){ if (ui->variablesTable->item(x, 0)){ @@ -622,7 +638,8 @@ void VariableDialog::applyModel() ui->variablesTable->setItem(x, 0, item); } - // check if this is the current variable. + // check if this is the current variable. This keeps us selecting the correct variable even when + // there's a deletion. if (!_currentVariable.empty() && variables[x][0] == _currentVariable){ selectedRow = x; } @@ -642,25 +659,14 @@ void VariableDialog::applyModel() } } - /* - // This empties rows that might have previously had variables - if (x < ui->variablesTable->rowCount()) { - ++x; - for (; x < ui->variablesTable->rowCount(); ++x){ - if (ui->variablesTable->item(x, 0)){ - ui->variablesTable->item(x, 0)->setText(""); - } - - if (ui->variablesTable->item(x, 1)){ - ui->variablesTable->item(x, 1)->setText(""); - } - - if (ui->variablesTable->item(x, 2)){ - ui->variablesTable->item(x, 2)->setText(""); - } - } + // set the Add varaible row + ++x; + if (ui->variablesTable->item(x, 0)){ + ui->variablesTable->item(x, 0)->setText("Add Variable ..."); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add Variable ..."); + ui->variablesTable->setItem(x, 0, item); } - */ if (_currentVariable.empty() || selectedRow < 0){ if (ui->variablesTable->item(0,0) && strlen(ui->variablesTable->item(0,0)->text().toStdString().c_str())){ @@ -674,7 +680,6 @@ void VariableDialog::applyModel() ui->containersTable->setRowCount(static_cast(containers.size())); selectedRow = -1; - // TODO! Change getContainerNames to a tuple with notes/maybe data key types? for (x = 0; x < static_cast(containers.size()); ++x){ if (ui->containersTable->item(x, 0)){ ui->containersTable->item(x, 0)->setText(containers[x][0].c_str()); @@ -704,22 +709,13 @@ void VariableDialog::applyModel() } } - // This empties rows that might have previously had containers - if (x < ui->containersTable->rowCount()) { - ++x; - for (; x < ui->containersTable->rowCount(); ++x){ - if (ui->containersTable->item(x, 0)){ - ui->containersTable->item(x, 0)->setText(""); - } - - if (ui->containersTable->item(x, 1)){ - ui->containersTable->item(x, 1)->setText(""); - } - - if (ui->containersTable->item(x, 2)){ - ui->containersTable->item(x, 2)->setText(""); - } - } + // set the Add container row + ++x; + if (ui->variablesTable->item(x, 0)){ + ui->variablesTable->item(x, 0)->setText("Add Container ..."); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add Container ..."); + ui->variablesTable->setItem(x, 0, item); } if (_currentContainer.empty() || selectedRow < 0){ @@ -897,45 +893,17 @@ void VariableDialog::updateContainerOptions() void VariableDialog::updateContainerDataOptions(bool list) { + if (_currentContainer.empty()){ + ui->copyContainerItemButton->setEnabled(false); + ui->deleteContainerItemButton->setEnabled(false); + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + } else if (list) { -} + } -SCP_string VariableDialog::trimNumberString(SCP_string source) -{ - SCP_string ret; - - // account for a lead negative sign. - if (source[0] == '-') { - ret = "-"; - } - - // filter out non-numeric digits - std::copy_if(source.begin(), source.end(), std::back_inserter(ret), - [](char c) -> bool { - bool result = false; - - switch (c) { - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - result = true; - break; - default: - break; - } - return result; - } - ); - return ret; } diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index d85c4aa0de7..acdc69d425d 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -67,7 +67,6 @@ class VariableDialog : public QDialog { void onCopyContainerItemButtonPressed(); void onDeleteContainerItemButtonPressed(); - SCP_string trimNumberString(SCP_string source); bool _applyingModel = false; SCP_string _currentVariable = ""; From 7d7b6eeeb83a7047e124f3312e936525c31192bd Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 18 Apr 2024 14:25:18 -0400 Subject: [PATCH 097/466] Prevent crash from empty source string --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index d289aa99ebd..f16cb193ec2 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1161,6 +1161,10 @@ const SCP_vector> VariableDialogModel::getContainerNam SCP_string VariableDialogModel::trimNumberString(SCP_string source) { + if (source.empty()){ + return ""; + } + SCP_string ret; // account for a lead negative sign. From 83fde5acfc902f61df7ce8ff02baf9f96633d64a Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 18 Apr 2024 14:45:20 -0400 Subject: [PATCH 098/466] Actually, upgrade the lambda This handles the leading negative no matter where it is. --- .../mission/dialogs/VariableDialogModel.cpp | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index f16cb193ec2..8712127878d 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1161,24 +1161,19 @@ const SCP_vector> VariableDialogModel::getContainerNam SCP_string VariableDialogModel::trimNumberString(SCP_string source) { - if (source.empty()){ - return ""; - } - SCP_string ret; - - // account for a lead negative sign. - if (source[0] == '-') { - ret = "-"; - } + bool foundNonZero = false; // filter out non-numeric digits std::copy_if(source.begin(), source.end(), std::back_inserter(ret), - [](char c) -> bool { - bool result = false; - + [&foundNonZero, &ret](char c) -> bool { switch (c) { + // ignore leading zeros case '0': + if (foundNonZero) + return true; + else + return false; case '1': case '2': case '3': @@ -1188,16 +1183,28 @@ SCP_string VariableDialogModel::trimNumberString(SCP_string source) case '7': case '8': case '9': - result = true; + foundNonZero = true; + return true; break; + // only copy the '-' char if it is the first thing to be copied. + case '-': + if (ret.empty()){ + return true; + } else { + return false; + } default: + return false; break; } - - return result; } ); + // if all that made it out was a dash, then return nothing. + if (ret == "-"){ + return ""; + } + return ret; } From f61649e911ac26a777a85ecf4a276892303a9f18 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 18 Apr 2024 15:14:25 -0400 Subject: [PATCH 099/466] Add type text to container Table --- .../mission/dialogs/VariableDialogModel.cpp | 28 +++++++++++++++++-- .../src/mission/dialogs/VariableDialogModel.h | 1 + 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 8712127878d..db620668ea9 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1142,10 +1142,34 @@ const SCP_vector> VariableDialogModel::getContainerNam SCP_vector> outStrings; for (const auto& item : _containerItems) { + SCP_string type = ""; SCP_string notes = ""; + if (item.list){ + type = "List of "; + + if (item.string) { + type += "Strings"; + } else { + type += "Numbers"; + } + } else { + if (item.integerKeys){ + type = "Number-Keyed Map of " + } else { + type = "String-Keyed Map of " + } + + if (item.string){ + type += "Strings" + } else { + type += "Numbers" + } + } + + if (item.deleted) { - notes = "Marked for Deletion"; + notes = "Flaged for Deletion"; } else if (item.originalName == "") { notes = "New"; } else if (item.name != item.originalName){ @@ -1153,7 +1177,7 @@ const SCP_vector> VariableDialogModel::getContainerNam } //TODO! FIX ME - outStrings.push_back(std::array{item.name, item.name, notes}); + outStrings.push_back(std::array{item.name, type, notes}); } return outStrings; diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 19b4b3cc4a6..2d0bbbaad3e 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -25,6 +25,7 @@ struct containerInfo { bool deleted = false; bool list = true; bool string = true; + bool integerKeys = false; int flags = 0; // this will allow us to look up the original values used in the mission previously. From e4254636dd70290db515f8b4e61def97ef12d57e Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 18 Apr 2024 16:52:45 -0400 Subject: [PATCH 100/466] Finish addMapItem and upgrade getcontainernames This has an upgrade for displaying List and Map info in a configurable manner. --- .../mission/dialogs/VariableDialogModel.cpp | 171 ++++++++++++++++-- .../src/mission/dialogs/VariableDialogModel.h | 5 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 4 +- 3 files changed, 165 insertions(+), 15 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index db620668ea9..5ac1ecf6300 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -814,7 +814,50 @@ SCP_string VariableDialogModel::addListItem(int index) std::pair VariableDialogModel::addMapItem(int index) { - + auto container = lookupContainer(index); + + std::pair ret = {"", ""}; + + // no container available + if (!container){ + return ret; + } + + bool conflict; + int count = 0; + SCP_string newKey; + + do { + conflict = false; + + if (container->integerKeys){ + sprintf(newKey, "%i", count); + } else { + sprintf(newKey, "key%i", count); + } + + for (int x = 0; x < static_cast(container->keys.size()); ++x) { + if (container->keys[x] == newKey){ + conflict = true; + break; + } + } + + ++count; + } while (conflict && count < 101); + + if (conflict) { + return ret; + } + + ret.first = newKey; + + if (container.string) + ret.second = ""; + else + ret.second = "0"; + + return ret; } SCP_string VariableDialogModel::copyListItem(int containerIndex, int index) @@ -1136,35 +1179,138 @@ const SCP_vector> VariableDialogModel::getVariableValu return outStrings; } - const SCP_vector> VariableDialogModel::getContainerNames() { + // This logic makes the mode which we use to display, easily configureable. + SCP_string listPrefix; + SCP_string listPostscript; + + SCP_string mapPrefix; + SCP_string mapMidScript; + SCP_string mapPostscript + + switch (_listTextMode) { + case 1: + listPrefix = ""; + listPostscript = " List"; + break; + + case 2: + listPrefix = "List ("; + listPostscript = ")"; + break; + + case 3: + listPrefix = "List <"; + listPostscript = ">"; + break; + + case 4: + listPrefix = "("; + listPostscript = ")"; + break; + + case 5: + listPrefix = "<" + listPostscript = ">" + break; + + case 6: + listPrefix = "" + listPostscript = "" + break; + + + default: + // this takes care of weird cases. The logic should be simple enough to not have bugs, but just in case, switch back to default. + _listTextMode = 0; + listPrefix = "List of"; + listPostscript = "s"; + break; + } + + switch (_mapTextMode) { + case 1: + mapPrefix = ""; + mapMidScript = "-keyed Map of "; + mapPostscript = " Values"; + + break; + case 2: + mapPrefix = "Map ("; + mapMidScript = ", "; + mapPostscript = ")"; + + break; + case 3: + mapPrefix = "Map <"; + mapMidScript = ", "; + mapPostscript = ">"; + + break; + case 4: + mapPrefix = "("; + mapMidScript = ", "; + mapPostscript = ")"; + + break; + case 5: + mapPrefix = "<"; + mapMidScript = ", "; + mapPostscript = ">"; + + break; + case 6: + mapPrefix = ""; + mapMidScript = ", "; + mapPostscript = ""; + + break; + + case default: + _mapTextMode = 0; + mapPrefix = "Map with "; + mapMidScript = " Keys and "; + mapPostscript = " Values"; + + break; + } + + SCP_vector> outStrings; for (const auto& item : _containerItems) { SCP_string type = ""; SCP_string notes = ""; + if (item.string) { + type = "String"; + } else { + type += "Number"; + } + if (item.list){ - type = "List of "; + type = listPrefix + type + listPostscript; - if (item.string) { - type += "Strings"; - } else { - type += "Numbers"; - } } else { + + type = mapPrefix; + if (item.integerKeys){ - type = "Number-Keyed Map of " + type += "Number"; } else { - type = "String-Keyed Map of " + type += "String"; } + type += mapMidScript; + if (item.string){ - type += "Strings" + type += "String" } else { - type += "Numbers" + type += "Number" } + + type += mapPostscript; } @@ -1176,7 +1322,6 @@ const SCP_vector> VariableDialogModel::getContainerNam notes = "Renamed"; } - //TODO! FIX ME outStrings.push_back(std::array{item.name, type, notes}); } diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 2d0bbbaad3e..e5cead090fd 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -121,6 +121,8 @@ class VariableDialogModel : public AbstractDialogModel { private: SCP_vector _variableItems; SCP_vector _containerItems; + int _listTextMode = 0; + int _mapTextMode = 0; variableInfo* lookupVariable(int index){ if(index > -1 && index < static_cast(_variableItems.size()) ){ @@ -158,6 +160,8 @@ class VariableDialogModel : public AbstractDialogModel { return nullptr; } + static SCP_string trimNumberString(SCP_string source); + // many of the controls in this editor can lead to drastic actions, so this will be very useful. const bool confirmAction(SCP_string question, SCP_string informativeText) { @@ -181,7 +185,6 @@ class VariableDialogModel : public AbstractDialogModel { } } - static SCP_string trimNumberString(SCP_string source); }; } // namespace dialogs diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 94c90b7bff0..c307a1826f9 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -380,7 +380,8 @@ void VariableDialog::onContainerContentsTableUpdated() } // could be new key or new value -void VariableDialog::onContainerContentsSelectionChanged() { +void VariableDialog::onContainerContentsSelectionChanged() +{ if (_applyingModel){ return; } @@ -600,6 +601,7 @@ void VariableDialog::onNetworkVariableCheckboxClicked() } } +// TODO! 17 more functions to write void VariableDialog::onAddContainerButtonPressed() {} void VariableDialog::onCopyContainerButtonPressed() {} void VariableDialog::onDeleteContainerButtonPressed() {} From a06cb0c68842f4634fcbe83e6bb1c4b36ece7325 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 18 Apr 2024 18:03:37 -0400 Subject: [PATCH 101/466] More progress on updateContainerDataOptions --- qtfred/src/ui/dialogs/VariableDialog.cpp | 41 +++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index c307a1826f9..dfd6fb875e6 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -895,15 +895,54 @@ void VariableDialog::updateContainerOptions() void VariableDialog::updateContainerDataOptions(bool list) { + if (_currentContainer.empty()){ ui->copyContainerItemButton->setEnabled(false); ui->deleteContainerItemButton->setEnabled(false); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + + return; } else if (list) { + ui->copyContainerItemButton->setEnabled(true); + ui->deleteContainerItemButton->setEnabled(true); + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - } + = _model->getMapKeys(); + + auto items = ui->containersTable->selectedItems(); + int row = -1; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + row = item->row(); + } + const SCP_vector& getMapKeys(int index); + const SCP_vector& getStringValues(int index); + const SCP_vector& getNumberValues(int index); + + + if (row < 0){ + return; + } + + + + ui->containerContentstable->setRowCount( ); + + + } else { + ui->copyContainerItemButton->setEnabled(true); + ui->deleteContainerItemButton->setEnabled(true); + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + + + ui->continerContentsTable->setRowCount(); + + } } From 26335ffadfd89c0ec8a92ec01d82d32e74ee334c Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Fri, 19 Apr 2024 23:56:47 -0400 Subject: [PATCH 102/466] use fast reload for XWI import When importing missions, FRED should use fast reload. It already does it for importing FS1 missions, so this adds it for XWI missions as well. --- fred2/freddoc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fred2/freddoc.cpp b/fred2/freddoc.cpp index 245ae4031f8..1462ab08f4c 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -640,7 +640,7 @@ void CFREDDoc::OnFileImportXWI() strcpy_s(xwi_path, xwi_path_mfc); // load mission into memory - if (!load_mission(xwi_path, MPF_IMPORT_XWI)) + if (!load_mission(xwi_path, MPF_IMPORT_XWI | MPF_FAST_RELOAD)) continue; // get filename From 2a337133cb5cd3dbd72614202acb63c750a91e83 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 20 Apr 2024 18:36:30 -0400 Subject: [PATCH 103/466] Fixes and add a couple more UI functions --- .../mission/dialogs/VariableDialogModel.cpp | 18 ++-- .../src/mission/dialogs/VariableDialogModel.h | 1 + qtfred/src/ui/dialogs/VariableDialog.cpp | 91 +++++++++++++------ 3 files changed, 74 insertions(+), 36 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 5ac1ecf6300..a08fbc9df01 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -477,11 +477,7 @@ SCP_string VariableDialogModel::addNewVariable() } SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName) -{ - if (newName == "") { - return ""; - } - +{ auto variable = lookupVariable(index); // nothing to change, or invalid entry @@ -489,6 +485,16 @@ SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName return ""; } + // no name means no variable + if (newName == "") { + variable->deleted = true; + } + + // Truncate name if needed + if (newName.len() >= TOKEN_LENGTH){ + newName = newName.substr(0, TAKEN_LENGTH - 1); + } + // We cannot have two variables with the same name, but we need to check this somewhere else (like on accept attempt). variable->name = newName; return newName; @@ -508,7 +514,7 @@ SCP_string VariableDialogModel::copyVariable(int index) do { SCP_string newName; - sprintf(newName, "%s_copy%i", variable->name.c_str(), count); + sprintf(newName, "%s_copy%i", variable->name.substr(0, TOKEN_LENGTH - 6).c_str(), count); variableSearch = lookupVariableByName(newName); // open slot found! diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index e5cead090fd..6fb18c1f0d4 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -90,6 +90,7 @@ class VariableDialogModel : public AbstractDialogModel { bool setContainerEternalFlag(int index, bool eternal); SCP_string addContainer(); + SCP_string copyContainer(int index); SCP_string changeContainerName(int index, SCP_string newName); bool removeContainer(int index); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index dfd6fb875e6..b854b917ca9 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -198,7 +198,6 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setColumnWidth(0, 90); ui->variablesTable->setColumnWidth(1, 90); ui->variablesTable->setColumnWidth(2, 70); - // TODO! Make sure row 3 is not editable. ui->containersTable->setColumnCount(3); ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); @@ -207,7 +206,6 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->containersTable->setColumnWidth(0, 90); ui->containersTable->setColumnWidth(1, 90); ui->containersTable->setColumnWidth(2, 70); - // TODO! Make sure row 3 is not editable. ui->containerContentsTable->setColumnCount(2); @@ -601,10 +599,45 @@ void VariableDialog::onNetworkVariableCheckboxClicked() } } -// TODO! 17 more functions to write -void VariableDialog::onAddContainerButtonPressed() {} +void VariableDialog::onAddContainerButtonPressed() +{ + auto result = (_model->addContainer()); + + if (result.empty()) { + QMessageBox msgBox; + msgBox.setText("Adding a container failed because the code is out of automatic names. Try adding a container directly in the table."); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.setDefaultButton(QMessageBox::Ok); + msgBox.exec(); + } + + applyModel(); + +} + +// TODO! 16 more functions to write void VariableDialog::onCopyContainerButtonPressed() {} -void VariableDialog::onDeleteContainerButtonPressed() {} + +void VariableDialog::onDeleteContainerButtonPressed() +{ + auto items = ui->containersTable->selectedItems(); + int row = -1; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + row = item->row(); + } + + if (row == -1){ + return; + } + + // UI is somehow out of sync with the model, so update UI. + if (!_model->removeContainer(row)) { + applyModel(); + } + +} void VariableDialog::onSetContainerAsMapRadioSelected() {} void VariableDialog::onSetContainerAsListRadioSelected() {} void VariableDialog::onSetContainerAsStringRadioSelected() {} @@ -623,8 +656,10 @@ void VariableDialog::onDeleteContainerItemButtonPressed() {} VariableDialog::~VariableDialog(){}; // NOLINT + void VariableDialog::applyModel() { + // TODO! We need an undelete action. Best way is to change the text on the button if the notes say "Deleted" _applyingModel = true; auto variables = _model->getVariableValues(); @@ -648,9 +683,11 @@ void VariableDialog::applyModel() if (ui->variablesTable->item(x, 1)){ ui->variablesTable->item(x, 1)->setText(variables[x][1].c_str()); + ui->varaiblesTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(variables[x][1].c_str()); ui->variablesTable->setItem(x, 1, item); + ui->variablesTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); } if (ui->variablesTable->item(x, 2)){ @@ -658,6 +695,7 @@ void VariableDialog::applyModel() } else { QTableWidgetItem* item = new QTableWidgetItem(variables[x][2].c_str()); ui->variablesTable->setItem(x, 2, item); + ui->variablesTable->item(x, 2)->setFlags(item->flags() & ~Qt::ItemIsEditable); } } @@ -670,6 +708,24 @@ void VariableDialog::applyModel() ui->variablesTable->setItem(x, 0, item); } + if (ui->variablesTable->item(x, 1)){ + ui->varaiblesTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->item(x, 1)->setText(""); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->setItem(x, 1, item); + } + + if (ui->variablesTable->item(x, 2)){ + ui->varaiblesTable->item(x, 2)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->item(x, 2)->setText(""); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->setItem(x, 2, item); + } + if (_currentVariable.empty() || selectedRow < 0){ if (ui->variablesTable->item(0,0) && strlen(ui->variablesTable->item(0,0)->text().toStdString().c_str())){ _currentVariable = ui->variablesTable->item(0,0)->text().toStdString(); @@ -695,7 +751,6 @@ void VariableDialog::applyModel() selectedRow = x; } - if (ui->containersTable->item(x, 1)){ ui->containersTable->item(x, 1)->setText(containers[x][1].c_str()); } else { @@ -909,30 +964,6 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - = _model->getMapKeys(); - - auto items = ui->containersTable->selectedItems(); - int row = -1; - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - row = item->row(); - } - - const SCP_vector& getMapKeys(int index); - const SCP_vector& getStringValues(int index); - const SCP_vector& getNumberValues(int index); - - - if (row < 0){ - return; - } - - - - ui->containerContentstable->setRowCount( ); - - } else { ui->copyContainerItemButton->setEnabled(true); ui->deleteContainerItemButton->setEnabled(true); From 7c37628e1ae82d3b345a54da1ded3a91da33ab58 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 20 Apr 2024 18:52:36 -0400 Subject: [PATCH 104/466] Add get item rows to reduce duplicate code --- .../src/mission/dialogs/VariableDialogModel.h | 1 + qtfred/src/ui/dialogs/VariableDialog.cpp | 140 +++++++++++++++++- qtfred/src/ui/dialogs/VariableDialog.h | 3 + 3 files changed, 138 insertions(+), 6 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 6fb18c1f0d4..62819e6df4f 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -84,6 +84,7 @@ class VariableDialogModel : public AbstractDialogModel { bool getContainerEternalFlag(int index); bool setContainerValueType(int index, bool type); + bool setContainerKeyType(int index, bool string); bool setContainerListOrMap(int index, bool list); bool setContainerNetworkStatus(int index, bool network); int setContainerOnMissionCloseOrCompleteFlag(int index, int flags); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index b854b917ca9..d9e65af207e 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -615,7 +615,7 @@ void VariableDialog::onAddContainerButtonPressed() } -// TODO! 16 more functions to write +// TODO! 15 more functions to write void VariableDialog::onCopyContainerButtonPressed() {} void VariableDialog::onDeleteContainerButtonPressed() @@ -638,11 +638,101 @@ void VariableDialog::onDeleteContainerButtonPressed() } } -void VariableDialog::onSetContainerAsMapRadioSelected() {} -void VariableDialog::onSetContainerAsListRadioSelected() {} -void VariableDialog::onSetContainerAsStringRadioSelected() {} -void VariableDialog::onSetContainerAsNumberRadioSelected() {} -void VariableDialog::onSetContainerKeyAsStringRadioSelected() {} + +void VariableDialog::onSetContainerAsMapRadioSelected() +{ + auto items = ui->containersTable->selectedItems(); + int row = -1; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + row = item->row(); + } + + if (row == -1){ + return; + } + + setContainerListOrMap(row, false); + applyModel(); +} + +void VariableDialog::onSetContainerAsListRadioSelected() +{ + auto items = ui->containersTable->selectedItems(); + int row = -1; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + row = item->row(); + } + + if (row == -1){ + return; + } + + setContainerListOrMap(row, true); + applyModel(); +} + + +void VariableDialog::onSetContainerAsStringRadioSelected() +{ + auto items = ui->containersTable->selectedItems(); + int row = -1; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + row = item->row(); + } + + if (row == -1){ + return; + } + + setContainerValueType(row, true); + applyModel(); +} + +void VariableDialog::onSetContainerAsNumberRadioSelected() +{ + auto items = ui->containersTable->selectedItems(); + int row = -1; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + row = item->row(); + } + + if (row == -1){ + return; + } + + setContainerValueType(row, false); + applyModel(); +} + +void VariableDialog::onSetContainerKeyAsStringRadioSelected() +{ + auto items = ui->containersTable->selectedItems(); + int row = -1; + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + row = item->row(); + } + + if (row == -1){ + return; + } + + setContainerKeyType(row, true); + applyModel(); + +} + + + void VariableDialog::onSetContainerKeyAsNumberRadioSelected() {} void VariableDialog::onDoNotSaveContainerRadioSelected(){} void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() {} @@ -979,6 +1069,44 @@ void VariableDialog::updateContainerDataOptions(bool list) } +int VariableDialog::getCurrentVariableRow() +{ + auto items = ui->variablesTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + if (item){ + return item->row(); + } + } + + return -1; +} + +int VariableDialog::getCurrentContainerRow(){ + auto items = ui->containersTable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + if (item) { + return item->row(); + } + } + + return -1; +} + +int variableDialog::getCurrentContainerItemRow(){ + auto items = ui->containerItemsTeable->selectedItems(); + + // yes, selected items returns a list, but we really should only have one item because multiselect will be off. + for (const auto& item : items) { + return item->row(); + } + + return -1; +} + } // namespace dialogs } // namespace fred } // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index acdc69d425d..7c6552ffa8b 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -67,6 +67,9 @@ class VariableDialog : public QDialog { void onCopyContainerItemButtonPressed(); void onDeleteContainerItemButtonPressed(); + int getCurrentVariableRow(); + int getCurrentContainerRow(); + int getCurrentContainerItemRow(); bool _applyingModel = false; SCP_string _currentVariable = ""; From e7b0f61870ba1f6d5afaa29a8bf7a58b2c58e8cb Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 20 Apr 2024 19:04:14 -0400 Subject: [PATCH 105/466] Start applying currentVariableRow --- qtfred/src/ui/dialogs/VariableDialog.cpp | 113 ++++++++++++----------- 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index d9e65af207e..73df4b5580c 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -317,8 +317,6 @@ void VariableDialog::onVariablesSelectionChanged() return; } - auto items = ui->variablesTable->selectedItems(); - SCP_string newVariableName = ""; // yes, selected items returns a list, but we really should only have one item because multiselect will be off. @@ -415,15 +413,15 @@ void VariableDialog::onCopyVariableButtonPressed() return; } - auto items = ui->variablesTable->selectedItems(); + int currentRow = getCurrentVariableRow(); - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for(const auto& item : items) { - auto ret = _model->copyVariable(item->row()); - _currentVariable = ret; - applyModel(); - break; - } + if (currentRow < 0){ + return; + } + + auto ret = _model->copyVariable(currentRow); + _currentVariable = ret; + applyModel(); } void VariableDialog::onDeleteVariableButtonPressed() @@ -432,82 +430,85 @@ void VariableDialog::onDeleteVariableButtonPressed() return; } - auto items = ui->variablesTable->selectedItems(); + int currentRow = getCurrentVariableRow(); - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for(const auto& item : items) { - // Because of the text update we'll need, this needs an applyModel, whether it fails or not. - auto ret = _model->removeVariable(item->row()); - applyModel(); - break; - } + if (currentRow < 0){ + return; + } + + // Because of the text update we'll need, this needs an applyModel, whether it fails or not. + _model->removeVariable(currentRow); + applyModel(); } void VariableDialog::onSetVariableAsStringRadioSelected() { - if (_currentVariable.empty() || ui->setVariableAsStringRadio->isChecked()){ + if (ui->setVariableAsStringRadio->isChecked()){ return; } - auto items = ui->variablesTable->selectedItems(); + int currentRow = getCurrentVariableRow(); - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for(const auto& item : items) { - // this doesn't return succeed or fail directly, - // but if it doesn't return true then it failed since this is the string radio - if(!_model->setVariableType(item->row(), true)){ - applyModel(); - } else { - ui->setVariableAsStringRadio->setChecked(true); - ui->setVariableAsNumberRadio->setChecked(false); - } + if (currentRow < 0){ + return; + } - break; + // this doesn't return succeed or fail directly, + // but if it doesn't return true then it failed since this is the string radio + if(!_model->setVariableType(currentRow, true)){ + applyModel(); + } else { + ui->setVariableAsStringRadio->setChecked(true); + ui->setVariableAsNumberRadio->setChecked(false); } + + break; } void VariableDialog::onSetVariableAsNumberRadioSelected() { - if (_currentVariable.empty() || ui->setVariableAsNumberRadio->isChecked()){ + if (ui->setVariableAsNumberRadio->isChecked()){ return; } - auto items = ui->variablesTable->selectedItems(); + int currentRow = getCurrentVariableRow(); - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { + if (currentRow < 0){ + return; + } - // this doesn't return succeed or fail directly, - // but if it doesn't return false then it failed since this is the number radio - if (!_model->setVariableType(item->row(), false)) { - applyModel(); - } - else { - ui->setVariableAsStringRadio->setChecked(false); - ui->setVariableAsNumberRadio->setChecked(true); - } + // this doesn't return succeed or fail directly, + // but if it doesn't return false then it failed since this is the number radio + if (!_model->setVariableType(currentRow, false)) { + applyModel(); } + else { + ui->setVariableAsStringRadio->setChecked(false); + ui->setVariableAsNumberRadio->setChecked(true); + } + break; } void VariableDialog::onDoNotSaveVariableRadioSelected() { - if (_currentVariable.empty() || ui->doNotSaveVariableRadio->isChecked()){ + if (ui->doNotSaveVariableRadio->isChecked()){ return; } - auto items = ui->variablesTable->selectedItems(); + int currentRow = getCurrentVariableRow(); - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(item->row(), 1); + if (currentRow < 0){ + return; + } - if (ret != 1){ - applyModel(); - } else { - ui->doNotSaveVariableRadio->setChecked(true); - ui->saveContainerOnMissionCompletedRadio->setChecked(false); - ui->saveVariableOnMissionCloseRadio->setChecked(false); - } + int ret = _model->setVariableOnMissionCloseOrCompleteFlag(currentRow, 1); + + if (ret != 1){ + applyModel(); + } else { + ui->doNotSaveVariableRadio->setChecked(true); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(false); } } From ad74c21b8273c6ee89734c6b1ed43c3ca0ca80b5 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 20 Apr 2024 23:31:17 -0400 Subject: [PATCH 106/466] Edit onVariablesTableUpdated And Apply GetCurrentContainerRow --- .../src/mission/dialogs/VariableDialogModel.h | 1 + qtfred/src/ui/dialogs/VariableDialog.cpp | 204 ++++++++++-------- 2 files changed, 110 insertions(+), 95 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 62819e6df4f..d15bcb8509b 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -91,6 +91,7 @@ class VariableDialogModel : public AbstractDialogModel { bool setContainerEternalFlag(int index, bool eternal); SCP_string addContainer(); + SCP_string addContainer(SCP_string nameIn); SCP_string copyContainer(int index); SCP_string changeContainerName(int index, SCP_string newName); bool removeContainer(int index); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 73df4b5580c..55f7912cce0 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -237,76 +237,77 @@ void VariableDialog::onVariablesTableUpdated() return; } - auto items = ui->variablesTable->selectedItems(); + int currentRow = getCurrentVariableRow(); - // yes, selected items returns a list, but we really should only have one row of items because multiselect will be off. - for(const auto& item : items) { - if (item->column() == 0){ + if (currentRow < 0){ + return; + } + + auto item = - // so if the user just removed the name, mark it as deleted *before changing the name* - if (_currentVariable != "" && !strlen(item->text().toStdString().c_str())) { - if (!_model->removeVariable(item->row())) { - // marking a variable as deleted failed, resync UI - applyModel(); - return; - } else { - updateVariableOptions(); - } - } else { - - auto ret = _model->changeVariableName(item->row(), item->text().toStdString()); - - // we put something in the cell, but the model couldn't process it. - if (strlen(item->text().toStdString().c_str()) && ret == ""){ - // update of variable name failed, resync UI - applyModel(); - - // we had a successful rename. So update the variable we reference. - } else if (ret != "") { - item->setText(ret.c_str()); - _currentVariable = ret; - } - } - // empty return and cell was handled earlier. + // so if the user just removed the name, mark it as deleted *before changing the name* + if (_currentVariable != "" && !strlen(item->text().toStdString().c_str())) { + if (!_model->removeVariable(item->row())) { + // marking a variable as deleted failed, resync UI + applyModel(); + return; + } else { + updateVariableOptions(); + } + } else { - // data cell was altered - } else if (item->column() == 1) { - - // Variable is a string - if (_model->getVariableType(item->row())){ - SCP_string temp = item->text().toStdString().c_str(); - temp = temp.substr(0, NAME_LENGTH - 1); - - SCP_string ret = _model->setVariableStringValue(item->row(), temp); - if (ret == ""){ - applyModel(); - return; - } - - item->setText(ret.c_str()); - } else { - SCP_string source = item->text().toStdString(); - SCP_string temp = _model->trimNumberString(source); - - if (temp != source){ - item->setText(temp.c_str()); - } - - try { - int ret = _model->setVariableNumberValue(item->row(), std::stoi(temp)); - temp = ""; - sprintf(temp, "%i", ret); - item->setText(temp.c_str()); - } - catch (...) { - applyModel(); - } - } + auto ret = _model->changeVariableName(item->row(), item->text().toStdString()); - // if the user somehow edited the info that should only come from the model and should not be editable, reload everything. - } else { + // we put something in the cell, but the model couldn't process it. + if (strlen(item->text().toStdString().c_str()) && ret == ""){ + // update of variable name failed, resync UI applyModel(); + + // we had a successful rename. So update the variable we reference. + } else if (ret != "") { + item->setText(ret.c_str()); + _currentVariable = ret; + } + } + // empty return and cell was handled earlier. + + // data cell was altered + } else if (item->column() == 1) { + + // Variable is a string + if (_model->getVariableType(item->row())){ + SCP_string temp = item->text().toStdString().c_str(); + temp = temp.substr(0, NAME_LENGTH - 1); + + SCP_string ret = _model->setVariableStringValue(item->row(), temp); + if (ret == ""){ + applyModel(); + return; + } + + item->setText(ret.c_str()); + } else { + SCP_string source = item->text().toStdString(); + SCP_string temp = _model->trimNumberString(source); + + if (temp != source){ + item->setText(temp.c_str()); + } + + try { + int ret = _model->setVariableNumberValue(item->row(), std::stoi(temp)); + temp = ""; + sprintf(temp, "%i", ret); + item->setText(temp.c_str()); + } + catch (...) { + applyModel(); + } } + + // if the user somehow edited the info that should only come from the model and should not be editable, reload everything. + } else { + applyModel(); } } @@ -340,8 +341,33 @@ void VariableDialog::onContainersTableUpdated() return; } + int row = getCurrentContainerRow(); + + // just in case something is goofy, return + if (row < 0){ + return; + } + + // Are they adding a new container? + if (row == ui->containersTable->rowCount - 1){ + if (ui->containersTable->item(row, 0)) { + SCP_string newString = ui->containersTable->item(row, 0).text().toStdString(); + if (!newString.empty() && newString != "Add Container ..."){ + _model->addContainer(newSTring); + _currentContainer = newString(); + applyModel(); + } + } -} // could be new name + // are they editing an existing container name? + } else if (ui->containersTable->item(row, 0)){ + _currentContainer = ui->containersTable->item(row,0).toStdString(); + + if (_currentContainer != _model->changeContainerName(row, ui->containersTable->item(row,0).toStdString())){ + applyModel(); + } + } +} void VariableDialog::onContainersSelectionChanged() { @@ -645,10 +671,7 @@ void VariableDialog::onSetContainerAsMapRadioSelected() auto items = ui->containersTable->selectedItems(); int row = -1; - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - row = item->row(); - } + int row = getCurrentContainerRow(); if (row == -1){ return; @@ -661,12 +684,7 @@ void VariableDialog::onSetContainerAsMapRadioSelected() void VariableDialog::onSetContainerAsListRadioSelected() { auto items = ui->containersTable->selectedItems(); - int row = -1; - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - row = item->row(); - } + int row = getCurrentContainerRow(); if (row == -1){ return; @@ -680,12 +698,7 @@ void VariableDialog::onSetContainerAsListRadioSelected() void VariableDialog::onSetContainerAsStringRadioSelected() { auto items = ui->containersTable->selectedItems(); - int row = -1; - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - row = item->row(); - } + int row = getCurrentContainerRow(); if (row == -1){ return; @@ -698,12 +711,7 @@ void VariableDialog::onSetContainerAsStringRadioSelected() void VariableDialog::onSetContainerAsNumberRadioSelected() { auto items = ui->containersTable->selectedItems(); - int row = -1; - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - row = item->row(); - } + int row = getCurrentContainerRow(); if (row == -1){ return; @@ -716,17 +724,11 @@ void VariableDialog::onSetContainerAsNumberRadioSelected() void VariableDialog::onSetContainerKeyAsStringRadioSelected() { auto items = ui->containersTable->selectedItems(); - int row = -1; - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - row = item->row(); - } + int row = getCurrentContainerRow(); if (row == -1){ return; } - setContainerKeyType(row, true); applyModel(); @@ -734,7 +736,19 @@ void VariableDialog::onSetContainerKeyAsStringRadioSelected() -void VariableDialog::onSetContainerKeyAsNumberRadioSelected() {} +void VariableDialog::onSetContainerKeyAsNumberRadioSelected() +{ + auto items = ui->containersTable->selectedItems(); + int row = getCurrentContainerRow(); + + if (row == -1){ + return; + } + + setContainerKeyType(row, false); + applyModel(); +} + void VariableDialog::onDoNotSaveContainerRadioSelected(){} void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() {} void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() {} From bb838ea4ddc62e8949453f3b738649cc497a0e73 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 20 Apr 2024 23:38:32 -0400 Subject: [PATCH 107/466] Place getCurrentContainerRow in more places --- qtfred/src/ui/dialogs/VariableDialog.cpp | 122 ++++++++++------------- 1 file changed, 51 insertions(+), 71 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 55f7912cce0..7ee65550a8a 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -542,88 +542,82 @@ void VariableDialog::onDoNotSaveVariableRadioSelected() void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() { - if (_currentVariable.empty() || ui->saveContainerOnMissionCompletedRadio->isChecked()){ + if (ui->saveContainerOnMissionCompletedRadio->isChecked()){ return; } - auto items = ui->variablesTable->selectedItems(); + int row = getCurrentVariableRow(); - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(item->row(), 1); + if (row < 0){ + return; + } + + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(row(), 1); - if (ret != 1){ - applyModel(); - } else { - ui->doNotSaveVariableRadio->setChecked(false); - ui->saveContainerOnMissionCompletedRadio->setChecked(true); - ui->saveVariableOnMissionCloseRadio->setChecked(false); - } + if (ret != 1){ + applyModel(); + } else { + ui->doNotSaveVariableRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(true); + ui->saveVariableOnMissionCloseRadio->setChecked(false); } } void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() { - if (_currentVariable.empty() || ui->saveContainerOnMissionCompletedRadio->isChecked()){ + if (ui->saveContainerOnMissionCompletedRadio->isChecked()){ return; } + int row = getCurrentVariableRow(); - auto items = ui->variablesTable->selectedItems(); - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { + if (row < 0){ + return; + } - auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(item->row(), 2); + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(row, 2); - if (ret != 2){ - applyModel(); - } else { - ui->doNotSaveVariableRadio->setChecked(false); - ui->saveContainerOnMissionCompletedRadio->setChecked(false); - ui->saveVariableOnMissionCloseRadio->setChecked(true); - } + // out of sync because we did not get the expected return value. + if (ret != 2){ + applyModel(); + } else { + ui->doNotSaveVariableRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(true); } } void VariableDialog::onSaveVariableAsEternalCheckboxClicked() { - if (_currentVariable.empty()){ + int row = getCurrentVariableRow(); + + if (row < 0){ return; } - - auto items = ui->variablesTable->selectedItems(); - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - // If the model returns the old status, then the change failed and we're out of sync. - if (ui->setVariableAsEternalcheckbox->isChecked() == _model->setVariableEternalFlag(item->row(), !ui->setVariableAsEternalcheckbox->isChecked())) { - applyModel(); - } else { - ui->setVariableAsEternalcheckbox->setChecked(!ui->setVariableAsEternalcheckbox->isChecked()); - } + // If the model returns the old status, then the change failed and we're out of sync. + if (ui->setVariableAsEternalcheckbox->isChecked() == _model->setVariableEternalFlag(row, !ui->setVariableAsEternalcheckbox->isChecked())) { + applyModel(); + } else { + ui->setVariableAsEternalcheckbox->setChecked(!ui->setVariableAsEternalcheckbox->isChecked()); } } void VariableDialog::onNetworkVariableCheckboxClicked() { - if (_currentVariable.empty()){ + int row = getCurrentVariableRow(); + + if (row < 0){ return; } - auto items = ui->variablesTable->selectedItems(); - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - - // If the model returns the old status, then the change failed and we're out of sync. - if (ui->networkVariableCheckbox->isChecked() == _model->setVariableNetworkStatus(item->row(), !ui->networkVariableCheckbox->isChecked())) { - applyModel(); - } else { - ui->networkVariableCheckbox->setChecked(!ui->networkVariableCheckbox->isChecked()); - } + // If the model returns the old status, then the change failed and we're out of sync. + if (ui->networkVariableCheckbox->isChecked() == _model->setVariableNetworkStatus(row, !ui->networkVariableCheckbox->isChecked())) { + applyModel(); + } else { + ui->networkVariableCheckbox->setChecked(!ui->networkVariableCheckbox->isChecked()); } + } void VariableDialog::onAddContainerButtonPressed() @@ -647,15 +641,9 @@ void VariableDialog::onCopyContainerButtonPressed() {} void VariableDialog::onDeleteContainerButtonPressed() { - auto items = ui->containersTable->selectedItems(); - int row = -1; - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - row = item->row(); - } + int row = getCurrentContainerRow(); - if (row == -1){ + if (row < 0){ return; } @@ -668,12 +656,9 @@ void VariableDialog::onDeleteContainerButtonPressed() void VariableDialog::onSetContainerAsMapRadioSelected() { - auto items = ui->containersTable->selectedItems(); - int row = -1; - int row = getCurrentContainerRow(); - if (row == -1){ + if (row < 0){ return; } @@ -683,10 +668,9 @@ void VariableDialog::onSetContainerAsMapRadioSelected() void VariableDialog::onSetContainerAsListRadioSelected() { - auto items = ui->containersTable->selectedItems(); int row = getCurrentContainerRow(); - if (row == -1){ + if (row < 0){ return; } @@ -697,10 +681,9 @@ void VariableDialog::onSetContainerAsListRadioSelected() void VariableDialog::onSetContainerAsStringRadioSelected() { - auto items = ui->containersTable->selectedItems(); int row = getCurrentContainerRow(); - if (row == -1){ + if (row < 0){ return; } @@ -710,10 +693,9 @@ void VariableDialog::onSetContainerAsStringRadioSelected() void VariableDialog::onSetContainerAsNumberRadioSelected() { - auto items = ui->containersTable->selectedItems(); int row = getCurrentContainerRow(); - if (row == -1){ + if (row < 0){ return; } @@ -723,10 +705,9 @@ void VariableDialog::onSetContainerAsNumberRadioSelected() void VariableDialog::onSetContainerKeyAsStringRadioSelected() { - auto items = ui->containersTable->selectedItems(); int row = getCurrentContainerRow(); - if (row == -1){ + if (row < 0){ return; } setContainerKeyType(row, true); @@ -738,10 +719,9 @@ void VariableDialog::onSetContainerKeyAsStringRadioSelected() void VariableDialog::onSetContainerKeyAsNumberRadioSelected() { - auto items = ui->containersTable->selectedItems(); int row = getCurrentContainerRow(); - if (row == -1){ + if (row < 0){ return; } From 72271be1f9b4888d8a205aeef090bfb7402ace6f Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 20 Apr 2024 23:50:54 -0400 Subject: [PATCH 108/466] Getting closer to completion of basics --- qtfred/src/ui/dialogs/VariableDialog.cpp | 135 ++++++++++++++++++++--- 1 file changed, 121 insertions(+), 14 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 7ee65550a8a..36fd06035ea 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -636,7 +636,7 @@ void VariableDialog::onAddContainerButtonPressed() } -// TODO! 15 more functions to write +// TODO! 4 more functions to write void VariableDialog::onCopyContainerButtonPressed() {} void VariableDialog::onDeleteContainerButtonPressed() @@ -656,84 +656,191 @@ void VariableDialog::onDeleteContainerButtonPressed() void VariableDialog::onSetContainerAsMapRadioSelected() { + if (ui->setContainerAsMapRadio->isChecked()){ + return; + } + int row = getCurrentContainerRow(); if (row < 0){ return; } - setContainerListOrMap(row, false); + model->setContainerListOrMap(row, false); applyModel(); } void VariableDialog::onSetContainerAsListRadioSelected() { + if (ui->setContainerAsListRadio->isChecked()){ + return; + } + int row = getCurrentContainerRow(); if (row < 0){ return; } - setContainerListOrMap(row, true); + model->setContainerListOrMap(row, true); applyModel(); } void VariableDialog::onSetContainerAsStringRadioSelected() { + if (ui->setContainerAsStringRadio->isChecked()){ + return; + } + int row = getCurrentContainerRow(); if (row < 0){ return; } - setContainerValueType(row, true); + model->setContainerValueType(row, true); applyModel(); } void VariableDialog::onSetContainerAsNumberRadioSelected() { + if (ui->setContainerAsNumberRadio->isChecked()){ + return; + } + + int row = getCurrentContainerRow(); if (row < 0){ return; } - setContainerValueType(row, false); + model->setContainerValueType(row, false); applyModel(); } void VariableDialog::onSetContainerKeyAsStringRadioSelected() { + if (ui->setContainerKeyAsStringRadio->isChecked()){ + return; + } + int row = getCurrentContainerRow(); if (row < 0){ return; } - setContainerKeyType(row, true); - applyModel(); + model->setContainerKeyType(row, true); + applyModel(); } - void VariableDialog::onSetContainerKeyAsNumberRadioSelected() { + if (ui->setContainerKeyAsNumberRadio->isChecked()){ + return; + } + int row = getCurrentContainerRow(); if (row < 0){ return; } - setContainerKeyType(row, false); + model->setContainerKeyType(row, false); applyModel(); } -void VariableDialog::onDoNotSaveContainerRadioSelected(){} -void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() {} -void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() {} -void VariableDialog::onNetworkContainerCheckboxClicked() {} -void VariableDialog::onSetContainerAsEternalCheckboxClicked() {} +void VariableDialog::onDoNotSaveContainerRadioSelected() +{ + if (ui->doNotSaveContainerRadio->isChecked()){ + return; + } + + int row = getCurrentContainerRow(); + + if (row < 0){ + return; + } + + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 0) != 0){ + applyModel(); + } else { + ui->doNotSaveContainerRadio.setChecked(true); + ui->saveContainerOnMissionClosedRadio.setChecked(false); + ui->saveContainerOnMissionCompletedRadio.setChecked(false); + } +} +void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() +{ + if (ui->saveContainerOnMissionClosedRadio->isChecked()){ + return; + } + + int row = getCurrentContainerRow(); + + if (row < 0){ + return; + } + + if (model->setContainerOnMissionCloseOrCompleteFlag(row, 2) != 2) + applyModel(); + else { + ui->doNotSaveContainerRadio.setChecked(false); + ui->saveContainerOnMissionClosedRadio.setChecked(true); + ui->saveContainerOnMissionCompletedRadio.setChecked(false); + } +} + +void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() +{ + if (ui->saveContainerOnMissionCompletedRadio->isChecked()){ + return; + } + + int row = getCurrentContainerRow(); + + if (row < 0){ + return; + } + + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 1) != 1) + applyModel(); + else { + ui->doNotSaveContainerRadio.setChecked(false); + ui->saveContainerOnMissionClosedRadio.setChecked(false); + ui->saveContainerOnMissionCompletedRadio.setChecked(true); + } +} + +void VariableDialog::onNetworkContainerCheckboxClicked() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + return; + } + + if (ui->networkContainerCheckbox->ischecked() != _model->setContainerNetworkStatus(row, ui->networkContainerCheckbox->ischecked())){ + applyModel(); + } +} + +void VariableDialog::onSetContainerAsEternalCheckboxClicked() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + return; + } + + if (ui->setContainerAsEternalCheckbox->ischecked() != _model->setContainerNetworkStatus(row, ui->setContainerAsEternalCheckbox->ischecked())){ + applyModel(); + } +} + void VariableDialog::onAddContainerItemButtonPressed() {} void VariableDialog::onCopyContainerItemButtonPressed() {} void VariableDialog::onDeleteContainerItemButtonPressed() {} From 3bec6d8cfc93866e1329193cce14912c5c1ea89d Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 00:14:19 -0400 Subject: [PATCH 109/466] No const here --- qtfred/src/mission/dialogs/VariableDialogModel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index d15bcb8509b..245f79821a0 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -166,7 +166,7 @@ class VariableDialogModel : public AbstractDialogModel { static SCP_string trimNumberString(SCP_string source); // many of the controls in this editor can lead to drastic actions, so this will be very useful. - const bool confirmAction(SCP_string question, SCP_string informativeText) + bool confirmAction(SCP_string question, SCP_string informativeText) { QMessageBox msgBox; msgBox.setText(question.c_str()); From 6bd3b43cd893d3493523bbc4fddcb697dadb0a12 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 00:17:01 -0400 Subject: [PATCH 110/466] Don't forget extra line left by accident --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index a08fbc9df01..a0038453d02 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -192,7 +192,6 @@ void VariableDialogModel::initializeData() } else { item.string = false; - Sexp_variables[i].text; try { item.numberValue = std::stoi(Sexp_variables[i].text); } From 6264656097ea2fe08add5512ab823d25534cd198 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 00:37:46 -0400 Subject: [PATCH 111/466] As far as I can tell populating items is done --- qtfred/src/ui/dialogs/VariableDialog.cpp | 167 ++++++++++++++++++++++- 1 file changed, 164 insertions(+), 3 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 36fd06035ea..109746082c5 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1142,32 +1142,193 @@ void VariableDialog::updateContainerOptions() void VariableDialog::updateContainerDataOptions(bool list) { + int row = getCurrentContainerRow(); - if (_currentContainer.empty()){ + // No overarching container, no container contents + if (row < 0){ + ui->addContainerItemButton->setEnabled(false); ui->copyContainerItemButton->setEnabled(false); ui->deleteContainerItemButton->setEnabled(false); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + ui->containerContentsTable->setRowCount(0); return; + + // list type container } else if (list) { + ui->addContainerItemButton->setEnabled(true); ui->copyContainerItemButton->setEnabled(true); ui->deleteContainerItemButton->setEnabled(true); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + // with string contents + if (_model->getContainerValueType(row)){ + auto strings = _model->getStringValues(); + ui->continerContentsTable->setRowCount(static_cast(strings.size()) + 1); + + int x; + for (x = 0; x < static_cast(strings.size()); ++x){ + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText(strings[x].c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(strings[x].c_str()); + ui->containerContentsTable->setItem(x, 0, item); + } + + // empty out the second column as it's not needed in list mode + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(""); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + ui->containerContentsTable->setItem(x, 1, item); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + } + } + + ++x; + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText("Add item ..."); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + ui->containerContentsTable->setItem(x, 0, item); + } + + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(""); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + ui->containerContentsTable->setItem(x, 1, item); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + } + + // list with number contents + } else { + auto numbers = _model->getNumberValues(); + ui->continerContentsTable->setRowCount(static_cast(numbers.size()) + 1); + + int x; + for (x = 0; x < static_cast(numbers.size()); ++x){ + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText(std::to_string(numbers[x]).c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); + ui->containerContentsTable->setItem(x, 0, item); + } + + // empty out the second column as it's not needed in list mode + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(""); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + ui->containerContentsTable->setItem(x, 1, item); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + } + } + + ++x; + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText("Add item ..."); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + ui->containerContentsTable->setItem(x, 0, item); + } + + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(""); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + ui->containerContentsTable->setItem(x, 1, item); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + } + + } + + // or it could be a map container } else { + ui->addContainerItemButton->setEnabled(true); ui->copyContainerItemButton->setEnabled(true); ui->deleteContainerItemButton->setEnabled(true); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + // keys I didn't bother to make separate. Should have done the same with values. + auto keys = _model->getMapKeys(row); - ui->continerContentsTable->setRowCount(); + // string valued map. + if (_model->getContainerValueType(row)){ + auto strings = _model->getStringValues(); + + // use the map as the size because map containers are only as good as their keys anyway. + ui->continerContentsTable->setRowCount(static_cast(keys.size()) + 1); + + int x; + for (x = 0; x < static_cast(keys.size()); ++x){ + if (ui->contiainerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText(keys[x].c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(keys[x].c_str()); + ui->containerContentsTable->setItem(x, 0, item); + } + + + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(strings[x].c_str()); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(strings[x].c_str()); + ui->containerContentsTable->setItem(x, 1, item); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + } + } - } + // number valued map + } else { + auto numbers = _model->getNumberValues(); + ui->continerContentsTable->setRowCount(static_cast(keys.size()) + 1); + + int x; + for (x = 0; x < static_cast(keys.size()); ++x){ + if (ui->contiainerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText(keys[x].c_str()); + } else { + QTableWidgetItem* item = new QTableWidgetItem(keys[x].c_str()); + ui->containerContentsTable->setItem(x, 0, item); + } + + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(std::to_string(numbers[x]).c_str()); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); + ui->containerContentsTable->setItem(x, 1, item); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + } + } + ++x; + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText("Add key ..."); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add key ..."); + ui->containerContentsTable->setItem(x, 0, item); + } + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText("Add Value ..."); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add Value ..."); + ui->containerContentsTable->setItem(x, 1, item); + ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + } + } + } } From 314ba92782c19fe17b2c3eafa29c48d0947dc713 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 00:41:38 -0400 Subject: [PATCH 112/466] Fix Compiler Errors --- .../mission/dialogs/VariableDialogModel.cpp | 18 +++++++++--------- .../src/mission/dialogs/VariableDialogModel.h | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index a0038453d02..68c7046307a 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -490,8 +490,8 @@ SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName } // Truncate name if needed - if (newName.len() >= TOKEN_LENGTH){ - newName = newName.substr(0, TAKEN_LENGTH - 1); + if (newName.length() >= TOKEN_LENGTH){ + newName = newName.substr(0, TOKEN_LENGTH - 1); } // We cannot have two variables with the same name, but we need to check this somewhere else (like on accept attempt). @@ -857,7 +857,7 @@ std::pair VariableDialogModel::addMapItem(int index) ret.first = newKey; - if (container.string) + if (container->string) ret.second = ""; else ret.second = "0"; @@ -1106,13 +1106,13 @@ const SCP_vector& VariableDialogModel::getMapKeys(int index) if (!container) { SCP_string temp; - sprintf("getMapKeys() found that container %s does not exist.", container->name.c_str()); + sprintf(temp, "getMapKeys() found that container %s does not exist.", container->name.c_str()); throw std::invalid_argument(temp.c_str()); } if (container->list) { SCP_string temp; - sprintf("getMapKeys() found that container %s is not a map.", container->name.c_str()); + sprintf(temp, "getMapKeys() found that container %s is not a map.", container->name.c_str()); throw std::invalid_argument(temp); } @@ -1126,13 +1126,13 @@ const SCP_vector& VariableDialogModel::getStringValues(int index) if (!container) { SCP_string temp; - sprintf("getStringValues() found that container %s does not exist.", container->name.c_str()); + sprintf(temp, "getStringValues() found that container %s does not exist.", container->name.c_str()); throw std::invalid_argument(temp); } if (!container->string) { SCP_string temp; - sprintf("getStringValues() found that container %s does not store strings.", container->name.c_str()); + sprintf(temp, "getStringValues() found that container %s does not store strings.", container->name.c_str()); throw std::invalid_argument(temp); } @@ -1146,13 +1146,13 @@ const SCP_vector& VariableDialogModel::getNumberValues(int index) if (!container) { SCP_string temp; - sprintf("getNumberValues() found that container %s does not exist.", container->name.c_str()); + sprintf(temp, "getNumberValues() found that container %s does not exist.", container->name.c_str()); throw std::invalid_argument(temp); } if (container->string) { SCP_string temp; - sprintf("getNumberValues() found that container %s does not store numbers.", container->name.c_str()); + sprintf(temp, "getNumberValues() found that container %s does not store numbers.", container->name.c_str()); throw std::invalid_argument(temp); } diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 245f79821a0..ea201446b48 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -115,7 +115,7 @@ class VariableDialogModel : public AbstractDialogModel { const SCP_vector> getVariableValues(); const SCP_vector> getContainerNames(); - void VariableDialogModel::checkValidModel(); + void checkValidModel(); bool apply() override; void reject() override; From 792cf0a0ed8d9221d664504b9701018b2f5fc054 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 01:11:57 -0400 Subject: [PATCH 113/466] More fixes --- .../mission/dialogs/VariableDialogModel.cpp | 16 +++++++------- qtfred/src/ui/dialogs/VariableDialog.cpp | 22 ++++++++++--------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 68c7046307a..c122133afd9 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1192,7 +1192,7 @@ const SCP_vector> VariableDialogModel::getContainerNam SCP_string mapPrefix; SCP_string mapMidScript; - SCP_string mapPostscript + SCP_string mapPostscript; switch (_listTextMode) { case 1: @@ -1216,13 +1216,13 @@ const SCP_vector> VariableDialogModel::getContainerNam break; case 5: - listPrefix = "<" - listPostscript = ">" + listPrefix = "<"; + listPostscript = ">"; break; case 6: - listPrefix = "" - listPostscript = "" + listPrefix = ""; + listPostscript = ""; break; @@ -1272,7 +1272,7 @@ const SCP_vector> VariableDialogModel::getContainerNam break; - case default: + default: _mapTextMode = 0; mapPrefix = "Map with "; mapMidScript = " Keys and "; @@ -1310,9 +1310,9 @@ const SCP_vector> VariableDialogModel::getContainerNam type += mapMidScript; if (item.string){ - type += "String" + type += "String"; } else { - type += "Number" + type += "Number"; } type += mapPostscript; diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 109746082c5..b4bb238a38a 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -83,7 +83,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) connect(ui->doNotSaveVariableRadio, &QRadioButton::toggled, this, - &VariableDialog::onDoNotSaveVariableRadioSelected) + &VariableDialog::onDoNotSaveVariableRadioSelected); connect(ui->saveContainerOnMissionCompletedRadio, &QRadioButton::toggled, @@ -243,7 +243,7 @@ void VariableDialog::onVariablesTableUpdated() return; } - auto item = + auto item = ui->variablesTable->item(currentRow); // so if the user just removed the name, mark it as deleted *before changing the name* if (_currentVariable != "" && !strlen(item->text().toStdString().c_str())) { @@ -272,7 +272,7 @@ void VariableDialog::onVariablesTableUpdated() // empty return and cell was handled earlier. // data cell was altered - } else if (item->column() == 1) { + if (item->column() == 1) { // Variable is a string if (_model->getVariableType(item->row())){ @@ -318,16 +318,18 @@ void VariableDialog::onVariablesSelectionChanged() return; } - SCP_string newVariableName = ""; + int row = getCurrentVariableRow(); - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for(const auto& item : items) { - if (item->column() == 0){ - newVariableName = item->text().toStdString(); - break; - } + if (row < 0){ + return; } + + SCP_string newVariableName = ""; + if (item == 0){ + newVariableName = item->text().toStdString(); + } + if (newVariableName != _currentVariable){ _currentVariable = newVariableName; applyModel(); From ce32ef30226fef25ce6555069f739b6facd3d675 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 13:41:23 -0400 Subject: [PATCH 114/466] Linter Fixes --- .../mission/dialogs/VariableDialogModel.cpp | 5 +- .../src/mission/dialogs/VariableDialogModel.h | 4 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 85 ++++++++++--------- 3 files changed, 53 insertions(+), 41 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index c122133afd9..d56b6611552 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -798,6 +798,7 @@ bool VariableDialogModel::removeContainer(int index) } container->deleted = true; + return container->deleted; } SCP_string VariableDialogModel::addListItem(int index) @@ -996,6 +997,8 @@ std::pair VariableDialogModel::copyMapItem(int index, SC } } } + + return std::make_pair("", ""); } // it's really because of this feature that we need data to only be in one or the other vector for maps. @@ -1184,7 +1187,7 @@ const SCP_vector> VariableDialogModel::getVariableValu return outStrings; } -const SCP_vector> VariableDialogModel::getContainerNames() +SCP_vector> VariableDialogModel::getContainerNames() { // This logic makes the mode which we use to display, easily configureable. SCP_string listPrefix; diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index ea201446b48..2f8c7b48aff 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -121,6 +121,9 @@ class VariableDialogModel : public AbstractDialogModel { void reject() override; void initializeData(); + + static SCP_string trimNumberString(SCP_string source); + private: SCP_vector _variableItems; SCP_vector _containerItems; @@ -163,7 +166,6 @@ class VariableDialogModel : public AbstractDialogModel { return nullptr; } - static SCP_string trimNumberString(SCP_string source); // many of the controls in this editor can lead to drastic actions, so this will be very useful. bool confirmAction(SCP_string question, SCP_string informativeText) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index b4bb238a38a..cff5802a88d 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -153,7 +153,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) connect(ui->doNotSaveContainerRadio, &QRadioButton::toggled, this, - &VariableDialog::onDoNotSaveContainerRadioSelected) + &VariableDialog::onDoNotSaveContainerRadioSelected); connect(ui->saveContainerOnMissionCloseRadio, &QRadioButton::toggled, @@ -231,6 +231,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) } // TODO! make sure that when a variable is added that the whole model is reloaded. +// TODO! Fix me. This function does not work as intended because it must process both, not just one. void VariableDialog::onVariablesTableUpdated() { if (_applyingModel){ @@ -243,14 +244,14 @@ void VariableDialog::onVariablesTableUpdated() return; } - auto item = ui->variablesTable->item(currentRow); + auto item = ui->variablesTable->item(currentRow, 0); + bool apply = false; // so if the user just removed the name, mark it as deleted *before changing the name* if (_currentVariable != "" && !strlen(item->text().toStdString().c_str())) { if (!_model->removeVariable(item->row())) { // marking a variable as deleted failed, resync UI - applyModel(); - return; + apply = true; } else { updateVariableOptions(); } @@ -261,7 +262,7 @@ void VariableDialog::onVariablesTableUpdated() // we put something in the cell, but the model couldn't process it. if (strlen(item->text().toStdString().c_str()) && ret == ""){ // update of variable name failed, resync UI - applyModel(); + apply = true; // we had a successful rename. So update the variable we reference. } else if (ret != "") { @@ -271,7 +272,11 @@ void VariableDialog::onVariablesTableUpdated() } // empty return and cell was handled earlier. - // data cell was altered + + item = ui->variablesTable->item(currentRow, 1); + + // check if data column was altered + // TODO! Set up comparison between last and current value if (item->column() == 1) { // Variable is a string @@ -281,11 +286,10 @@ void VariableDialog::onVariablesTableUpdated() SCP_string ret = _model->setVariableStringValue(item->row(), temp); if (ret == ""){ - applyModel(); - return; - } - - item->setText(ret.c_str()); + apply = true; + } else { + item->setText(ret.c_str()); + } } else { SCP_string source = item->text().toStdString(); SCP_string temp = _model->trimNumberString(source); @@ -323,10 +327,12 @@ void VariableDialog::onVariablesSelectionChanged() if (row < 0){ return; } - + SCP_string newVariableName = ""; - if (item == 0){ + auto item = ui->variablesTable->item(x, 0); + + if (item){ newVariableName = item->text().toStdString(); } @@ -351,19 +357,19 @@ void VariableDialog::onContainersTableUpdated() } // Are they adding a new container? - if (row == ui->containersTable->rowCount - 1){ + if (row == ui->containersTable->rowCount() - 1){ if (ui->containersTable->item(row, 0)) { - SCP_string newString = ui->containersTable->item(row, 0).text().toStdString(); + SCP_string newString = ui->containersTable->item(row, 0)->text().toStdString(); if (!newString.empty() && newString != "Add Container ..."){ - _model->addContainer(newSTring); - _currentContainer = newString(); + _model->addContainer(newString); + _currentContainer = newString; applyModel(); } } // are they editing an existing container name? } else if (ui->containersTable->item(row, 0)){ - _currentContainer = ui->containersTable->item(row,0).toStdString(); + _currentContainer = ui->containersTable->item(row,0)->text().toStdString(); if (_currentContainer != _model->changeContainerName(row, ui->containersTable->item(row,0).toStdString())){ applyModel(); @@ -489,8 +495,6 @@ void VariableDialog::onSetVariableAsStringRadioSelected() ui->setVariableAsStringRadio->setChecked(true); ui->setVariableAsNumberRadio->setChecked(false); } - - break; } void VariableDialog::onSetVariableAsNumberRadioSelected() @@ -514,7 +518,6 @@ void VariableDialog::onSetVariableAsNumberRadioSelected() ui->setVariableAsStringRadio->setChecked(false); ui->setVariableAsNumberRadio->setChecked(true); } - break; } void VariableDialog::onDoNotSaveVariableRadioSelected() @@ -554,7 +557,7 @@ void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() return; } - auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(row(), 1); + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(row, 1); if (ret != 1){ applyModel(); @@ -668,7 +671,7 @@ void VariableDialog::onSetContainerAsMapRadioSelected() return; } - model->setContainerListOrMap(row, false); + _model->setContainerListOrMap(row, false); applyModel(); } @@ -684,7 +687,7 @@ void VariableDialog::onSetContainerAsListRadioSelected() return; } - model->setContainerListOrMap(row, true); + _model->setContainerListOrMap(row, true); applyModel(); } @@ -701,7 +704,7 @@ void VariableDialog::onSetContainerAsStringRadioSelected() return; } - model->setContainerValueType(row, true); + _model->setContainerValueType(row, true); applyModel(); } @@ -718,7 +721,7 @@ void VariableDialog::onSetContainerAsNumberRadioSelected() return; } - model->setContainerValueType(row, false); + _model->setContainerValueType(row, false); applyModel(); } @@ -734,7 +737,7 @@ void VariableDialog::onSetContainerKeyAsStringRadioSelected() return; } - model->setContainerKeyType(row, true); + _model->setContainerKeyType(row, true); applyModel(); } @@ -751,7 +754,7 @@ void VariableDialog::onSetContainerKeyAsNumberRadioSelected() return; } - model->setContainerKeyType(row, false); + _model->setContainerKeyType(row, false); applyModel(); } @@ -770,9 +773,9 @@ void VariableDialog::onDoNotSaveContainerRadioSelected() if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 0) != 0){ applyModel(); } else { - ui->doNotSaveContainerRadio.setChecked(true); - ui->saveContainerOnMissionClosedRadio.setChecked(false); - ui->saveContainerOnMissionCompletedRadio.setChecked(false); + ui->doNotSaveContainerRadio->setChecked(true); + ui->saveContainerOnMissionCloseRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); } } void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() @@ -790,9 +793,9 @@ void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() if (model->setContainerOnMissionCloseOrCompleteFlag(row, 2) != 2) applyModel(); else { - ui->doNotSaveContainerRadio.setChecked(false); - ui->saveContainerOnMissionClosedRadio.setChecked(true); - ui->saveContainerOnMissionCompletedRadio.setChecked(false); + ui->doNotSaveContainerRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(true); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); } } @@ -811,9 +814,9 @@ void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 1) != 1) applyModel(); else { - ui->doNotSaveContainerRadio.setChecked(false); - ui->saveContainerOnMissionClosedRadio.setChecked(false); - ui->saveContainerOnMissionCompletedRadio.setChecked(true); + ui->doNotSaveContainerRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(true); } } @@ -825,7 +828,7 @@ void VariableDialog::onNetworkContainerCheckboxClicked() return; } - if (ui->networkContainerCheckbox->ischecked() != _model->setContainerNetworkStatus(row, ui->networkContainerCheckbox->ischecked())){ + if (ui->networkContainerCheckbox->isChecked() != _model->setContainerNetworkStatus(row, ui->networkContainerCheckbox->ischecked())){ applyModel(); } } @@ -843,7 +846,11 @@ void VariableDialog::onSetContainerAsEternalCheckboxClicked() } } -void VariableDialog::onAddContainerItemButtonPressed() {} +void VariableDialog::onAddContainerItemButtonPressed() +{ + +} + void VariableDialog::onCopyContainerItemButtonPressed() {} void VariableDialog::onDeleteContainerItemButtonPressed() {} From 474527ef8381ec85d27b8d2a66c725ef7db1dd86 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 14:15:35 -0400 Subject: [PATCH 115/466] More Linter Fixes --- .../mission/dialogs/VariableDialogModel.cpp | 3 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 41 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index d56b6611552..b3c71bc16aa 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -899,6 +899,7 @@ bool VariableDialogModel::removeListItem(int containerIndex, int index) container->numberValues.erase(container->numberValues.begin() + index); } + return true; } std::pair VariableDialogModel::copyMapItem(int index, SCP_string keyIn) @@ -1187,7 +1188,7 @@ const SCP_vector> VariableDialogModel::getVariableValu return outStrings; } -SCP_vector> VariableDialogModel::getContainerNames() +const SCP_vector> VariableDialogModel::getContainerNames() { // This logic makes the mode which we use to display, easily configureable. SCP_string listPrefix; diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index cff5802a88d..c0a5ee4370a 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1174,8 +1174,8 @@ void VariableDialog::updateContainerDataOptions(bool list) // with string contents if (_model->getContainerValueType(row)){ - auto strings = _model->getStringValues(); - ui->continerContentsTable->setRowCount(static_cast(strings.size()) + 1); + auto strings = _model->getStringValues(row); + ui->containerContentsTable->setRowCount(static_cast(strings.size()) + 1); int x; for (x = 0; x < static_cast(strings.size()); ++x){ @@ -1189,11 +1189,11 @@ void VariableDialog::updateContainerDataOptions(bool list) // empty out the second column as it's not needed in list mode if (ui->containerContentsTable->item(x, 1)){ ui->containerContentsTable->item(x, 1)->setText(""); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(""); - ui->containerContentsTable->setItem(x, 1, item); ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); } } @@ -1207,17 +1207,17 @@ void VariableDialog::updateContainerDataOptions(bool list) if (ui->containerContentsTable->item(x, 1)){ ui->containerContentsTable->item(x, 1)->setText(""); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(""); - ui->containerContentsTable->setItem(x, 1, item); ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); } // list with number contents } else { auto numbers = _model->getNumberValues(); - ui->continerContentsTable->setRowCount(static_cast(numbers.size()) + 1); + ui->containerContentsTable->setRowCount(static_cast(numbers.size()) + 1); int x; for (x = 0; x < static_cast(numbers.size()); ++x){ @@ -1231,7 +1231,7 @@ void VariableDialog::updateContainerDataOptions(bool list) // empty out the second column as it's not needed in list mode if (ui->containerContentsTable->item(x, 1)){ ui->containerContentsTable->item(x, 1)->setText(""); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(""); ui->containerContentsTable->setItem(x, 1, item); @@ -1249,11 +1249,11 @@ void VariableDialog::updateContainerDataOptions(bool list) if (ui->containerContentsTable->item(x, 1)){ ui->containerContentsTable->item(x, 1)->setText(""); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(""); - ui->containerContentsTable->setItem(x, 1, item); ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); } } @@ -1274,7 +1274,7 @@ void VariableDialog::updateContainerDataOptions(bool list) auto strings = _model->getStringValues(); // use the map as the size because map containers are only as good as their keys anyway. - ui->continerContentsTable->setRowCount(static_cast(keys.size()) + 1); + ui->containerContentsTable->setRowCount(static_cast(keys.size()) + 1); int x; for (x = 0; x < static_cast(keys.size()); ++x){ @@ -1285,21 +1285,20 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setItem(x, 0, item); } - if (ui->containerContentsTable->item(x, 1)){ ui->containerContentsTable->item(x, 1)->setText(strings[x].c_str()); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(strings[x].c_str()); - ui->containerContentsTable->setItem(x, 1, item); ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); } } // number valued map } else { - auto numbers = _model->getNumberValues(); - ui->continerContentsTable->setRowCount(static_cast(keys.size()) + 1); + auto numbers = _model->getNumberValues(row); + ui->containerContentsTable->setRowCount(static_cast(keys.size()) + 1); int x; for (x = 0; x < static_cast(keys.size()); ++x){ @@ -1312,11 +1311,11 @@ void VariableDialog::updateContainerDataOptions(bool list) if (ui->containerContentsTable->item(x, 1)){ ui->containerContentsTable->item(x, 1)->setText(std::to_string(numbers[x]).c_str()); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); - ui->containerContentsTable->setItem(x, 1, item); ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); } } @@ -1330,11 +1329,11 @@ void VariableDialog::updateContainerDataOptions(bool list) if (ui->containerContentsTable->item(x, 1)){ ui->containerContentsTable->item(x, 1)->setText("Add Value ..."); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem("Add Value ..."); - ui->containerContentsTable->setItem(x, 1, item); ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); } } } @@ -1368,7 +1367,7 @@ int VariableDialog::getCurrentContainerRow(){ return -1; } -int variableDialog::getCurrentContainerItemRow(){ +int VariableDialog::getCurrentContainerItemRow(){ auto items = ui->containerItemsTeable->selectedItems(); // yes, selected items returns a list, but we really should only have one item because multiselect will be off. From 29754cafc285f5201bf3cd8eda92f4926f499906 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 14:39:45 -0400 Subject: [PATCH 116/466] More Linter Fixes --- qtfred/src/ui/dialogs/VariableDialog.cpp | 20 ++++++++++---------- qtfred/src/ui/dialogs/VariableDialog.h | 2 ++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index c0a5ee4370a..bf97488fa9a 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -330,7 +330,7 @@ void VariableDialog::onVariablesSelectionChanged() SCP_string newVariableName = ""; - auto item = ui->variablesTable->item(x, 0); + auto item = ui->variablesTable->item(row, 0); if (item){ newVariableName = item->text().toStdString(); @@ -790,7 +790,7 @@ void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() return; } - if (model->setContainerOnMissionCloseOrCompleteFlag(row, 2) != 2) + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 2) != 2) applyModel(); else { ui->doNotSaveContainerRadio->setChecked(false); @@ -841,7 +841,7 @@ void VariableDialog::onSetContainerAsEternalCheckboxClicked() return; } - if (ui->setContainerAsEternalCheckbox->ischecked() != _model->setContainerNetworkStatus(row, ui->setContainerAsEternalCheckbox->ischecked())){ + if (ui->setContainerAsEternalCheckbox->isChecked() != _model->setContainerNetworkStatus(row, ui->setContainerAsEternalCheckbox->ischecked())){ applyModel(); } } @@ -884,7 +884,7 @@ void VariableDialog::applyModel() if (ui->variablesTable->item(x, 1)){ ui->variablesTable->item(x, 1)->setText(variables[x][1].c_str()); - ui->varaiblesTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(variables[x][1].c_str()); ui->variablesTable->setItem(x, 1, item); @@ -910,7 +910,7 @@ void VariableDialog::applyModel() } if (ui->variablesTable->item(x, 1)){ - ui->varaiblesTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->item(x, 1)->setFlags(ui->variablesTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); ui->variablesTable->item(x, 1)->setText(""); } else { QTableWidgetItem* item = new QTableWidgetItem(""); @@ -919,7 +919,7 @@ void VariableDialog::applyModel() } if (ui->variablesTable->item(x, 2)){ - ui->varaiblesTable->item(x, 2)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->item(x, 2)->setFlags(ui->variablesTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); ui->variablesTable->item(x, 2)->setText(""); } else { QTableWidgetItem* item = new QTableWidgetItem(""); @@ -1216,7 +1216,7 @@ void VariableDialog::updateContainerDataOptions(bool list) // list with number contents } else { - auto numbers = _model->getNumberValues(); + auto numbers = _model->getNumberValues(row); ui->containerContentsTable->setRowCount(static_cast(numbers.size()) + 1); int x; @@ -1278,7 +1278,7 @@ void VariableDialog::updateContainerDataOptions(bool list) int x; for (x = 0; x < static_cast(keys.size()); ++x){ - if (ui->contiainerContentsTable->item(x, 0)){ + if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText(keys[x].c_str()); } else { QTableWidgetItem* item = new QTableWidgetItem(keys[x].c_str()); @@ -1302,7 +1302,7 @@ void VariableDialog::updateContainerDataOptions(bool list) int x; for (x = 0; x < static_cast(keys.size()); ++x){ - if (ui->contiainerContentsTable->item(x, 0)){ + if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText(keys[x].c_str()); } else { QTableWidgetItem* item = new QTableWidgetItem(keys[x].c_str()); @@ -1368,7 +1368,7 @@ int VariableDialog::getCurrentContainerRow(){ } int VariableDialog::getCurrentContainerItemRow(){ - auto items = ui->containerItemsTeable->selectedItems(); + auto items = ui->containerItemsTable->selectedItems(); // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 7c6552ffa8b..b41817fb2e7 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -73,8 +73,10 @@ class VariableDialog : public QDialog { bool _applyingModel = false; SCP_string _currentVariable = ""; + SCP_string _currentVariableData = ""; SCP_string _currentContainer = ""; SCP_string _currentContainerItem = ""; + SCP_string _currentContainerItemData = ""; }; From 3698cbe09ff856abb914fdc1ace8df58d87c935d Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 15:00:44 -0400 Subject: [PATCH 117/466] Hopefully Final Linter mistakes --- qtfred/src/ui/dialogs/VariableDialog.cpp | 26 ++++++++++++++---------- qtfred/src/ui/dialogs/VariableDialog.h | 2 +- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index bf97488fa9a..c5778d90694 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -158,7 +158,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) connect(ui->saveContainerOnMissionCloseRadio, &QRadioButton::toggled, this, - &VariableDialog::onSaveContainerOnMissionClosedRadioSelected); + &VariableDialog::onSaveContainerOnMissionCloseRadioSelected); connect(ui->saveContainerOnMissionCompletedRadio, &QRadioButton::toggled, @@ -371,7 +371,7 @@ void VariableDialog::onContainersTableUpdated() } else if (ui->containersTable->item(row, 0)){ _currentContainer = ui->containersTable->item(row,0)->text().toStdString(); - if (_currentContainer != _model->changeContainerName(row, ui->containersTable->item(row,0).toStdString())){ + if (_currentContainer != _model->changeContainerName(row, ui->containersTable->item(row,0)->text().toStdString())){ applyModel(); } } @@ -641,8 +641,11 @@ void VariableDialog::onAddContainerButtonPressed() } -// TODO! 4 more functions to write -void VariableDialog::onCopyContainerButtonPressed() {} +// TODO! 3 more functions to write +void VariableDialog::onCopyContainerButtonPressed() +{ + +} void VariableDialog::onDeleteContainerButtonPressed() { @@ -778,9 +781,9 @@ void VariableDialog::onDoNotSaveContainerRadioSelected() ui->saveContainerOnMissionCompletedRadio->setChecked(false); } } -void VariableDialog::onSaveContainerOnMissionClosedRadioSelected() +void VariableDialog::onSaveContainerOnMissionCloseRadioSelected() { - if (ui->saveContainerOnMissionClosedRadio->isChecked()){ + if (ui->saveContainerOnMissionCloseRadio->isChecked()){ return; } @@ -828,7 +831,7 @@ void VariableDialog::onNetworkContainerCheckboxClicked() return; } - if (ui->networkContainerCheckbox->isChecked() != _model->setContainerNetworkStatus(row, ui->networkContainerCheckbox->ischecked())){ + if (ui->networkContainerCheckbox->isChecked() != _model->setContainerNetworkStatus(row, ui->networkContainerCheckbox->isChecked())){ applyModel(); } } @@ -841,7 +844,7 @@ void VariableDialog::onSetContainerAsEternalCheckboxClicked() return; } - if (ui->setContainerAsEternalCheckbox->isChecked() != _model->setContainerNetworkStatus(row, ui->setContainerAsEternalCheckbox->ischecked())){ + if (ui->setContainerAsEternalCheckbox->isChecked() != _model->setContainerNetworkStatus(row, ui->setContainerAsEternalCheckbox->isChecked())){ applyModel(); } } @@ -884,7 +887,7 @@ void VariableDialog::applyModel() if (ui->variablesTable->item(x, 1)){ ui->variablesTable->item(x, 1)->setText(variables[x][1].c_str()); - ui->variablesTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->item(x, 1)->setFlags(ui->variablesTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(variables[x][1].c_str()); ui->variablesTable->setItem(x, 1, item); @@ -893,6 +896,7 @@ void VariableDialog::applyModel() if (ui->variablesTable->item(x, 2)){ ui->variablesTable->item(x, 2)->setText(variables[x][2].c_str()); + ui->variablesTable->item(x, 2)->setFlags(ui->variablesTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(variables[x][2].c_str()); ui->variablesTable->setItem(x, 2, item); @@ -1271,7 +1275,7 @@ void VariableDialog::updateContainerDataOptions(bool list) // string valued map. if (_model->getContainerValueType(row)){ - auto strings = _model->getStringValues(); + auto strings = _model->getStringValues(row); // use the map as the size because map containers are only as good as their keys anyway. ui->containerContentsTable->setRowCount(static_cast(keys.size()) + 1); @@ -1368,7 +1372,7 @@ int VariableDialog::getCurrentContainerRow(){ } int VariableDialog::getCurrentContainerItemRow(){ - auto items = ui->containerItemsTable->selectedItems(); + auto items = ui->containerContentsTable->selectedItems(); // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index b41817fb2e7..2d4bf1425fa 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -59,7 +59,7 @@ class VariableDialog : public QDialog { void onSetContainerKeyAsStringRadioSelected(); void onSetContainerKeyAsNumberRadioSelected(); void onDoNotSaveContainerRadioSelected(); - void onSaveContainerOnMissionClosedRadioSelected(); + void onSaveContainerOnMissionCloseRadioSelected(); void onSaveContainerOnMissionCompletedRadioSelected(); void onNetworkContainerCheckboxClicked(); void onSetContainerAsEternalCheckboxClicked(); From 22663cb2972e5779ae85ab4f6dc652f18aa2ecbf Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 15:54:11 -0400 Subject: [PATCH 118/466] Finish Dialog Code Need to make more changes to the model --- .../mission/dialogs/VariableDialogModel.cpp | 4 +- .../src/mission/dialogs/VariableDialogModel.h | 3 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 103 ++++++++++++++++-- 3 files changed, 95 insertions(+), 15 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index b3c71bc16aa..c4c6267bc6d 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1007,7 +1007,7 @@ std::pair VariableDialogModel::copyMapItem(int index, SC // both of the map's data vectors might be undesired, and not deleting takes the map immediately // out of sync. Also, just displaying both data sets would be misleading. // We just need to tell the user that the data cannot be maintained. -bool VariableDialogModel::removeMapItem(int index, SCP_string key) +bool VariableDialogModel::removeMapItem(int index, int itemIndex) { auto container = lookupContainer(index); @@ -1015,6 +1015,8 @@ bool VariableDialogModel::removeMapItem(int index, SCP_string key) return false; } + auto item = lookupContainerItem(itemIndex); + for (int x = 0; x < static_cast(container->keys.size()); ++x) { if (container->keys[x] == key) { if (container->string && x < static_cast(container->stringValues.size())) { diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 2f8c7b48aff..d84c74eefed 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -97,13 +97,12 @@ class VariableDialogModel : public AbstractDialogModel { bool removeContainer(int index); SCP_string addListItem(int index); - SCP_string copyListItem(int containerIndex, int index); bool removeListItem(int containerindex, int index); std::pair addMapItem(int index); std::pair copyMapItem(int index, SCP_string key); - bool removeMapItem(int index, SCP_string key); + bool removeMapItem(int index, int rowIndex); SCP_string replaceMapItemKey(int index, SCP_string oldKey, SCP_string newKey); SCP_string changeMapItemStringValue(int index, SCP_string key, SCP_string newValue); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index c5778d90694..f70ffacf953 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -335,7 +335,13 @@ void VariableDialog::onVariablesSelectionChanged() if (item){ newVariableName = item->text().toStdString(); } - + + item = ui->variablesTable->item(row, 1); + + if (item){ + _currentVariableData = item->text().toStdString(); + } + if (newVariableName != _currentVariable){ _currentVariable = newVariableName; applyModel(); @@ -401,6 +407,7 @@ void VariableDialog::onContainersSelectionChanged() } } +// TODO, finish this function void VariableDialog::onContainerContentsTableUpdated() { if (_applyingModel){ @@ -416,16 +423,26 @@ void VariableDialog::onContainerContentsSelectionChanged() return; } - auto items = ui->containerContentsTable->selectedItems(); + int row = getCurrentContainerItemRow(); - SCP_string newContainerItemName = ""; + if (row < 0){ + return; + } - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for(const auto& item : items) { - if (item->column() == 0){ - newContainerItemName = item->text().toStdString(); - break; - } + auto item = ui->containerContentsTable->item(row, 0); + + if (item){ + SCP_string newContainerItemName = item->text().toStdString(); + } else { + return; + } + + item = ui->containerContentsTable->item(row, 1); + + if (item){ + _currentContainerItemData = item->text().toStdString(); + } else { + _currentContainerItemData = ""; } if (newContainerItemName != _currentContainerItem){ @@ -641,10 +658,17 @@ void VariableDialog::onAddContainerButtonPressed() } -// TODO! 3 more functions to write void VariableDialog::onCopyContainerButtonPressed() { + int row = getCurrentContainerRow(); + + if (row < 0 ){ + return; + } + // This will always need an apply model update, whether it succeeds or fails. + copyContainer(row); + applyModel(); } void VariableDialog::onDeleteContainerButtonPressed() @@ -851,11 +875,66 @@ void VariableDialog::onSetContainerAsEternalCheckboxClicked() void VariableDialog::onAddContainerItemButtonPressed() { + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + return; + } + + if (_model->getContainerListOrMap(containerRow)) { + _model->addListItem(containerRow); + } else { + _model->addMapItem(containerRow); + } + + applyModel(); +} + +void VariableDialog::onCopyContainerItemButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + return; + } + + int itemRow = getCurrentContainerItemRow(); + + if (itemRow < 0){ + return; + } + + if (_model->getContainerListOrMap(containerRow)) { + _model->copyListItem(containerRow, itemRow); + } else { + _model->copyMapItem(containerRow, itemRow); + } + applyModel(); } -void VariableDialog::onCopyContainerItemButtonPressed() {} -void VariableDialog::onDeleteContainerItemButtonPressed() {} +void VariableDialog::onDeleteContainerItemButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + return; + } + + int itemRow = getCurrentContainerItemRow(); + + if (itemRow < 0){ + return; + } + + if (_model->getContainerListOrMap(containerRow)) { + _model->removeListItem(containerRow, itemRow); + } else { + _model->removeMapItem(containerRow, itemRow); + } + + applyModel(); +} VariableDialog::~VariableDialog(){}; // NOLINT From a82db02b41ecb935fbed9956fa352ec3a955185e Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 15:56:47 -0400 Subject: [PATCH 119/466] Fix missing function --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index c4c6267bc6d..1556d94905f 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -768,7 +768,14 @@ SCP_string VariableDialogModel::addContainer() _containerItems.emplace_back(); _containerItems.back().name = name; - return name; + return _containerItems.back().name; +} + +SCP_string VariableDialogModel::addContainer(SCP_string nameIn) +{ + _containerItems.emplace_back(); + _containerItems.back().name = nameIn; + return _containerItems.back().name; } SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newName) From 209e905a0565433c7f6f0f6847c10e83a2811302 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 16:00:43 -0400 Subject: [PATCH 120/466] Add copy Container Function --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 1556d94905f..94a3ef348f0 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -778,6 +778,19 @@ SCP_string VariableDialogModel::addContainer(SCP_string nameIn) return _containerItems.back().name; } +SCP_string VariableDialogModel::copyContainer(int index) +{ + auto container = lookupContainer(index); + + // nothing to copy, invalid entry + if (!container){ + return ""; + } + + // K.I.S.S. + _containerItems.push_back(*container); +} + SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newName) { if (newName == "") { From 4e669f9ce4bad3f8a0d547a9bdd3640af27cee0a Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 16:02:00 -0400 Subject: [PATCH 121/466] don't forget to change name. --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 94a3ef348f0..c9d146c0a54 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -789,6 +789,9 @@ SCP_string VariableDialogModel::copyContainer(int index) // K.I.S.S. _containerItems.push_back(*container); + _containerItems.back().name += "_copy"; + _containerItems.back().name = _containerItems.back().name.substr(0, TOKEN_LENGTH); + return } SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newName) From 91723be9c50c8f72365066f3202a76e583738b84 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 16:05:12 -0400 Subject: [PATCH 122/466] And stub another function for now There are at least two stubbed functions left on the model --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index c9d146c0a54..8073992e676 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -686,6 +686,11 @@ bool VariableDialogModel::setContainerValueType(int index, bool type) return container->string; } +// TODO finish these two functions. +bool VariableDialogModel::setContainerKeyType(int index, bool string) { + return false; +} + // This is the most complicated function, because we need to query the user on what they want to do if the had already entered data. bool VariableDialogModel::setContainerListOrMap(int index, bool list) { From 7f0666cc334de2427dbe75060e0672a194d6d1e1 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 16:46:57 -0400 Subject: [PATCH 123/466] More Fixes, but Requires Linter For more progress --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 2 +- qtfred/src/mission/dialogs/VariableDialogModel.h | 2 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 11 ++++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 8073992e676..68dbdd2e138 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -930,7 +930,7 @@ bool VariableDialogModel::removeListItem(int containerIndex, int index) return true; } -std::pair VariableDialogModel::copyMapItem(int index, SCP_string keyIn) +std::pair VariableDialogModel::copyMapItem(int index, int mapIndex) { auto container = lookupContainer(index); diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index d84c74eefed..e3e64421cdb 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -101,7 +101,7 @@ class VariableDialogModel : public AbstractDialogModel { bool removeListItem(int containerindex, int index); std::pair addMapItem(int index); - std::pair copyMapItem(int index, SCP_string key); + std::pair copyMapItem(int index, int itemIndex); bool removeMapItem(int index, int rowIndex); SCP_string replaceMapItemKey(int index, SCP_string oldKey, SCP_string newKey); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index f70ffacf953..70195806797 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -430,13 +430,14 @@ void VariableDialog::onContainerContentsSelectionChanged() } auto item = ui->containerContentsTable->item(row, 0); - - if (item){ - SCP_string newContainerItemName = item->text().toStdString(); - } else { + SCP_string newContainerItemName; + + if (!item){ return; } + newContainerItemName = item->text().toStdString(); + item = ui->containerContentsTable->item(row, 1); if (item){ @@ -667,7 +668,7 @@ void VariableDialog::onCopyContainerButtonPressed() } // This will always need an apply model update, whether it succeeds or fails. - copyContainer(row); + _model->copyContainer(row); applyModel(); } From f38dbae310f714c1cc89a967731400706944a7a4 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sun, 21 Apr 2024 22:50:51 -0400 Subject: [PATCH 124/466] Save changes to transfer over --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 68dbdd2e138..7d8df7e6a92 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -796,7 +796,7 @@ SCP_string VariableDialogModel::copyContainer(int index) _containerItems.push_back(*container); _containerItems.back().name += "_copy"; _containerItems.back().name = _containerItems.back().name.substr(0, TOKEN_LENGTH); - return + return _containerItems.back().name; } SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newName) @@ -934,7 +934,10 @@ std::pair VariableDialogModel::copyMapItem(int index, in { auto container = lookupContainer(index); - if (!container) { + // any invalid case, early return + if (!container || mapIndex < 0 || mapIndex >= static_cast(container->keys.size()) + || (mapIndex >= static_cast(container->stringValues.size()) && container->string) + || (mapIndex >= static_cast(container->numberValues.size()) && !container->string)){ return std::make_pair("", ""); } From d147330739246294ff590a81d27000ce718a38cd Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 21 Apr 2024 23:38:14 -0400 Subject: [PATCH 125/466] Fix null item crash --- .../src/mission/dialogs/VariableDialogModel.cpp | 8 ++++---- qtfred/src/ui/dialogs/VariableDialog.cpp | 17 ++++++++++------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 7d8df7e6a92..33d54a3a2ee 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -932,7 +932,7 @@ bool VariableDialogModel::removeListItem(int containerIndex, int index) std::pair VariableDialogModel::copyMapItem(int index, int mapIndex) { - auto container = lookupContainer(index); + /*auto container = lookupContainer(index); // any invalid case, early return if (!container || mapIndex < 0 || mapIndex >= static_cast(container->keys.size()) @@ -1029,7 +1029,7 @@ std::pair VariableDialogModel::copyMapItem(int index, in } } } - + */ return std::make_pair("", ""); } @@ -1040,7 +1040,7 @@ std::pair VariableDialogModel::copyMapItem(int index, in // We just need to tell the user that the data cannot be maintained. bool VariableDialogModel::removeMapItem(int index, int itemIndex) { - auto container = lookupContainer(index); +/* auto container = lookupContainer(index); if (!container){ return false; @@ -1065,7 +1065,7 @@ bool VariableDialogModel::removeMapItem(int index, int itemIndex) } } - // NO SPRINGS!!! HEHEHEHEHE + // NO SPRINGS!!! HEHEHEHEHE*/ return false; } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 70195806797..73c39436b09 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -277,6 +277,9 @@ void VariableDialog::onVariablesTableUpdated() // check if data column was altered // TODO! Set up comparison between last and current value + // TODO! Also this crashes because item->(x, 1) is null + // TODO! Variable is not editable + // TODO! Network container does not turn off if (item->column() == 1) { // Variable is a string @@ -1276,7 +1279,7 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(""); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } } @@ -1294,7 +1297,7 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(""); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } @@ -1318,8 +1321,8 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); } } @@ -1336,7 +1339,7 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(""); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } @@ -1374,7 +1377,7 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(strings[x].c_str()); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } } @@ -1398,7 +1401,7 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } } @@ -1416,7 +1419,7 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem("Add Value ..."); - ui->containerContentsTable->item(x, 1)->setFlags(item->flags() | Qt::ItemIsEditable); + item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } } From 7f1e72ecca7e3be6651aef2b57f1b4b49f04c152 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Mon, 22 Apr 2024 00:32:51 -0400 Subject: [PATCH 126/466] Some final ui adjustments --- qtfred/src/ui/dialogs/VariableDialog.cpp | 8 ++--- qtfred/ui/VariableDialog.ui | 44 +++++++++++++++--------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 73c39436b09..aeab2a3a494 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -197,7 +197,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); ui->variablesTable->setColumnWidth(0, 90); ui->variablesTable->setColumnWidth(1, 90); - ui->variablesTable->setColumnWidth(2, 70); + ui->variablesTable->setColumnWidth(2, 65); ui->containersTable->setColumnCount(3); ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); @@ -205,7 +205,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); ui->containersTable->setColumnWidth(0, 90); ui->containersTable->setColumnWidth(1, 90); - ui->containersTable->setColumnWidth(2, 70); + ui->containersTable->setColumnWidth(2, 65); ui->containerContentsTable->setColumnCount(2); @@ -213,7 +213,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); ui->containerContentsTable->setColumnWidth(0, 125); - ui->containerContentsTable->setColumnWidth(1, 125); + ui->containerContentsTable->setColumnWidth(1, 120); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't ui->setVariableAsStringRadio->setAutoExclusive(false); @@ -1279,7 +1279,7 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(""); - item->->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } } diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index a63a7c24a9d..7417a5a0661 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -7,13 +7,13 @@ 0 0 608 - 636 + 665 608 - 560 + 665 @@ -38,19 +38,19 @@ 270 120 - 71 + 81 101 Type Options - + - 0 + 10 20 - 116 + 71 80 @@ -103,9 +103,9 @@ - 360 + 370 20 - 221 + 211 201 @@ -165,7 +165,7 @@ 270 30 - 71 + 82 86 @@ -236,7 +236,7 @@ 10 190 - 341 + 351 191 @@ -273,7 +273,7 @@ 260 30 - 71 + 82 91 @@ -312,7 +312,7 @@ 270 30 - 71 + 82 91 @@ -348,9 +348,9 @@ - 360 + 370 20 - 211 + 201 161 @@ -408,9 +408,9 @@ - 360 + 370 190 - 211 + 191 191 @@ -422,7 +422,7 @@ 10 20 - 201 + 181 152 @@ -509,6 +509,16 @@ + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + From 3c7639a9e313015f074b938decd0d120f1f24c8f Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Mon, 22 Apr 2024 00:33:16 -0400 Subject: [PATCH 127/466] And one more apparently --- qtfred/src/ui/dialogs/VariableDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index aeab2a3a494..69fa2f7a385 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -212,7 +212,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) // Default to list ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setColumnWidth(0, 125); + ui->containerContentsTable->setColumnWidth(0, 120); ui->containerContentsTable->setColumnWidth(1, 120); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't From 9dfc6d7c686914fbd8f4e5af3af1f4e4193759b7 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 22 Apr 2024 01:02:17 -0400 Subject: [PATCH 128/466] Fixes for UI bugs --- qtfred/src/ui/dialogs/VariableDialog.cpp | 49 +++++++++++++----------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 69fa2f7a385..28107a2fa95 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -71,27 +71,27 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) &VariableDialog::onDeleteVariableButtonPressed); connect(ui->setVariableAsStringRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSetVariableAsStringRadioSelected); connect(ui->setVariableAsNumberRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSetVariableAsNumberRadioSelected); connect(ui->doNotSaveVariableRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onDoNotSaveVariableRadioSelected); connect(ui->saveContainerOnMissionCompletedRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSaveVariableOnMissionCompleteRadioSelected); connect(ui->saveVariableOnMissionCloseRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSaveVariableOnMissionCloseRadioSelected); @@ -121,47 +121,47 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) &VariableDialog::onDeleteContainerButtonPressed); connect(ui->setContainerAsMapRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSetContainerAsMapRadioSelected); connect(ui->setContainerAsListRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSetContainerAsListRadioSelected); connect(ui->setContainerAsStringRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSetContainerAsStringRadioSelected); connect(ui->setContainerAsNumberRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSetContainerAsNumberRadioSelected); connect(ui->setContainerKeyAsStringRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSetContainerKeyAsStringRadioSelected); connect(ui->setContainerKeyAsNumberRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSetContainerKeyAsNumberRadioSelected); connect(ui->doNotSaveContainerRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onDoNotSaveContainerRadioSelected); connect(ui->saveContainerOnMissionCloseRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSaveContainerOnMissionCloseRadioSelected); connect(ui->saveContainerOnMissionCompletedRadio, - &QRadioButton::toggled, + &QRadioButton::clicked, this, &VariableDialog::onSaveContainerOnMissionCompletedRadioSelected); @@ -950,9 +950,9 @@ void VariableDialog::applyModel() _applyingModel = true; auto variables = _model->getVariableValues(); - int x, selectedRow = -1; + int x = 0, selectedRow = -1; - ui->variablesTable->setRowCount(static_cast(variables.size() + 1)); + ui->variablesTable->setRowCount(static_cast(variables.size()) + 1); for (x = 0; x < static_cast(variables.size()); ++x){ if (ui->variablesTable->item(x, 0)){ @@ -970,11 +970,9 @@ void VariableDialog::applyModel() if (ui->variablesTable->item(x, 1)){ ui->variablesTable->item(x, 1)->setText(variables[x][1].c_str()); - ui->variablesTable->item(x, 1)->setFlags(ui->variablesTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(variables[x][1].c_str()); ui->variablesTable->setItem(x, 1, item); - ui->variablesTable->item(x, 1)->setFlags(item->flags() & ~Qt::ItemIsEditable); } if (ui->variablesTable->item(x, 2)){ @@ -987,8 +985,8 @@ void VariableDialog::applyModel() } } - // set the Add varaible row - ++x; + // set the Add variable row + // TODO, fix this not appearing if (ui->variablesTable->item(x, 0)){ ui->variablesTable->item(x, 0)->setText("Add Variable ..."); } else { @@ -1086,6 +1084,7 @@ void VariableDialog::updateVariableOptions() ui->saveVariableOnMissionCompletedRadio->setEnabled(false); ui->saveVariableOnMissionCloseRadio->setEnabled(false); ui->setVariableAsEternalcheckbox->setEnabled(false); + ui->networkVariableCheckbox->setEnabled(false); return; } @@ -1098,6 +1097,7 @@ void VariableDialog::updateVariableOptions() ui->saveVariableOnMissionCompletedRadio->setEnabled(true); ui->saveVariableOnMissionCloseRadio->setEnabled(true); ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->networkVariableCheckbox->setEnabled(true); auto items = ui->variablesTable->selectedItems(); int row = -1; @@ -1107,8 +1107,10 @@ void VariableDialog::updateVariableOptions() row = item->row(); } + // if nothing is selected, but something could be selected, make it so. if (row == -1 && ui->variablesTable->rowCount() > 0) { row = 0; + ui->variablesTable->item(row, 0).setSelected(true); _currentVariable = ui->variablesTable->item(row, 0)->text().toStdString(); } @@ -1117,8 +1119,7 @@ void VariableDialog::updateVariableOptions() bool string = _model->getVariableType(row); ui->setVariableAsStringRadio->setChecked(string); ui->setVariableAsNumberRadio->setChecked(!string); - ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); - + int ret = _model->getVariableOnMissionCloseOrCompleteFlag(row); if (ret == 0){ @@ -1153,6 +1154,7 @@ void VariableDialog::updateContainerOptions() ui->setContainerAsEternalCheckbox->setEnabled(false); ui->setContainerAsMapRadio->setEnabled(false); ui->setContainerAsListRadio->setEnabled(false); + ui->networkContainerCheckbox->setEnabled(false); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); @@ -1178,6 +1180,7 @@ void VariableDialog::updateContainerOptions() ui->setContainerAsEternalCheckbox->setEnabled(true); ui->setContainerAsMapRadio->setEnabled(true); ui->setContainerAsListRadio->setEnabled(true); + ui->networkContainerCheckbox->setEnabled(true); if (_model->getContainerValueType(row)){ ui->setContainerAsStringRadio->setChecked(true); @@ -1214,7 +1217,7 @@ void VariableDialog::updateContainerOptions() updateContainerDataOptions(false); } - ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerNetworkStatus(row)); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); ui->networkContainerCheckbox->setChecked(_model->getContainerNetworkStatus(row)); int ret = _model->getContainerOnMissionCloseOrCompleteFlag(row); From 84cad8c18841ed846c75fc71e3450fa5403e34f8 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 22 Apr 2024 01:09:44 -0400 Subject: [PATCH 129/466] More Fixes and polish --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 8 ++++---- qtfred/src/ui/dialogs/VariableDialog.cpp | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 33d54a3a2ee..73483db4189 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -460,7 +460,7 @@ SCP_string VariableDialogModel::addNewVariable() do { name = ""; - sprintf(name, "", count); + sprintf(name, "newVar%i", count); variable = lookupVariableByName(name); ++count; } while (variable != nullptr && count < 51); @@ -762,7 +762,7 @@ SCP_string VariableDialogModel::addContainer() do { name = ""; - sprintf(name, "", count); + sprintf(name, "newCont%i", count); container = lookupContainerByName(name); ++count; } while (container != nullptr && count < 51); @@ -794,8 +794,8 @@ SCP_string VariableDialogModel::copyContainer(int index) // K.I.S.S. _containerItems.push_back(*container); - _containerItems.back().name += "_copy"; - _containerItems.back().name = _containerItems.back().name.substr(0, TOKEN_LENGTH); + _containerItems.back().name = "copy_" + _containerItems.back().name; + _containerItems.back().name = _containerItems.back().name.substr(0, TOKEN_LENGTH - 1); return _containerItems.back().name; } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 28107a2fa95..7a8e7d72c26 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1119,7 +1119,7 @@ void VariableDialog::updateVariableOptions() bool string = _model->getVariableType(row); ui->setVariableAsStringRadio->setChecked(string); ui->setVariableAsNumberRadio->setChecked(!string); - + int ret = _model->getVariableOnMissionCloseOrCompleteFlag(row); if (ret == 0){ @@ -1158,7 +1158,7 @@ void VariableDialog::updateContainerOptions() ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - + ui->containerContentsTable->setRowCount(0); } else { auto items = ui->containersTable->selectedItems(); From dd44d7d13a13b2be8574bcc0538caaa48afaaa9c Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 22 Apr 2024 01:40:46 -0400 Subject: [PATCH 130/466] Fixing more issues --- .../mission/dialogs/VariableDialogModel.cpp | 190 +++++++++--------- .../src/mission/dialogs/VariableDialogModel.h | 31 +++ qtfred/src/ui/dialogs/VariableDialog.cpp | 3 +- 3 files changed, 128 insertions(+), 96 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 73483db4189..285dc8f1e4b 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -932,104 +932,104 @@ bool VariableDialogModel::removeListItem(int containerIndex, int index) std::pair VariableDialogModel::copyMapItem(int index, int mapIndex) { - /*auto container = lookupContainer(index); + auto container = lookupContainer(index); // any invalid case, early return - if (!container || mapIndex < 0 || mapIndex >= static_cast(container->keys.size()) - || (mapIndex >= static_cast(container->stringValues.size()) && container->string) - || (mapIndex >= static_cast(container->numberValues.size()) && !container->string)){ + if (!container) { return std::make_pair("", ""); } - for (int x = 0; x < static_cast(container->keys.size()); ++x) { - if (container->keys[x] == keyIn){ - if (container->string){ - if (x < static_cast(container->stringValues.size())){ - SCP_string copyValue = container->stringValues[x]; - SCP_string newKey; - int size = static_cast(container->keys.size()); - sprintf(newKey, "key%i", size); - - bool found = false; - - do { - found = false; - for (int y = 0; y < static_cast(container->keys.size()); ++y){ - if (container->keys[y] == newKey) { - found = true; - break; - } - } + auto key = lookupContainerKey(index, mapIndex); - // attempt did not work, try next number - if (found) { - ++size; - newKey = ""; - sprintf(newKey, "key%i", size); - } + if (!key) { + return std::make_pair("", ""); + } + + - } while (found && size < static_cast(container->keys.size()) + 100); - - // we could not generate a new key .... somehow. - if (found){ - return std::make_pair("", ""); - } + if (container->string){ + auto value = lookupContainerStringItem(index, mapIndex); - container->keys.push_back(newKey); - container->stringValues.push_back(copyValue); + // no valid value. + if (!value){ + return std::make_pair("", ""); + } + + SCP_string copyValue = *value; + SCP_string newKey = *key + "0"; + int count = 0; - return std::make_pair(newKey, copyValue); + bool found; - } else { - return std::make_pair("", ""); + do { + found = false; + for (int y = 0; y < static_cast(container->keys.size()); ++y){ + if (container->keys[y] == newKey) { + found = true; + break; } - } else { - if (x < static_cast(container->numberValues.size())){ - int copyValue = container->numberValues[x]; - SCP_string newKey; - int size = static_cast(container->keys.size()); - sprintf(newKey, "key%i", size); - - bool found = false; - - do { - found = false; - for (int y = 0; y < static_cast(container->keys.size()); ++y){ - if (container->keys[y] == newKey) { - found = true; - break; - } - } + } - // attempt did not work, try next number - if (found) { - ++size; - newKey = ""; - sprintf(newKey, "key%i", size); - } + // attempt did not work, try next number + if (found) { + sprintf(newKey, "%s%i", *key.c_str(), ++count); + } - } while (found && size < static_cast(container->keys.size()) + 100); - - // we could not generate a new key .... somehow. - if (found){ - return std::make_pair("", ""); - } + } while (found && count < 100); + + // we could not generate a new key .... somehow. + if (found){ + return std::make_pair("", ""); + } - container->keys.push_back(newKey); - container->numberValues.push_back(copyValue); + container->keys.push_back(newKey); + container->stringValues.push_back(copyValue); + + return std::make_pair(newKey, copyValue); + + } else { + auto value = lookupContainerNumberItem(index, mapIndex); - SCP_string temp; - sprintf(temp, "%i", copyValue); + // no valid value. + if (!value){ + return std::make_pair("", ""); + } - return std::make_pair(newKey, temp); + bool found; + SCP_string newKey = *key + "0"; + int count = 0; - } else { - return std::make_pair("", ""); + do { + found = false; + for (int y = 0; y < static_cast(container->keys.size()); ++y){ + if (container->keys[y] == newKey) { + found = true; + break; } } + + // attempt did not work, try next number + if (found) { + sprintf(newKey, "%s%i", *key.c_str(), ++count); + } + + } while (found && count < 100); + + + // we could not generate a new key .... somehow. + if (found){ + return std::make_pair("", ""); } + + container->keys.push_back(newKey); + container->numberValues.push_back(copyValue); + + SCP_string temp; + sprintf(temp, "%i", copyValue); + + return std::make_pair(newKey, temp); } - */ + return std::make_pair("", ""); } @@ -1040,33 +1040,33 @@ std::pair VariableDialogModel::copyMapItem(int index, in // We just need to tell the user that the data cannot be maintained. bool VariableDialogModel::removeMapItem(int index, int itemIndex) { -/* auto container = lookupContainer(index); + auto container = lookupContainer(index); if (!container){ return false; } + // container is valid. - auto item = lookupContainerItem(itemIndex); + auto item = lookupContainerKey(itemIndex); - for (int x = 0; x < static_cast(container->keys.size()); ++x) { - if (container->keys[x] == key) { - if (container->string && x < static_cast(container->stringValues.size())) { - container->stringValues.erase(container->stringValues.begin() + x); - } else if (!container->string && x < static_cast(container->numberValues.size())){ - container->numberValues.erase(container->numberValues.begin() + x); - } else { - return false; - } + if (!item){ + return false; + } + // key is valid - // if we get here, we've succeeded and it's time to bug out - container->keys.erase(container->keys.begin() + x); - // "I'm outta here!" - return true; - } + // Now double check that we have a data value. + if (container->string && lookupContainerStringItem(itemIndex)){ + container->stringValues.erase(container->stringValues.begin() + itemIndex); + } else if (!container->string && lookupContainerNumberItem(itemIndex)){ + container->numberValues.erase(container->numberValues.begin() + itemIndex); + } else { + return false; } - // NO SPRINGS!!! HEHEHEHEHE*/ - return false; + // if we get here, we've succeeded and it's time to erase the key and bug out + container->keys.erase(container->keys.begin() + itemIndex); + // "I'm outta here!" + return true; } SCP_string VariableDialogModel::replaceMapItemKey(int index, SCP_string oldKey, SCP_string newKey) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index e3e64421cdb..cf18b991a1b 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -165,6 +165,36 @@ class VariableDialogModel : public AbstractDialogModel { return nullptr; } + SCP_string* lookupContainerKey(int containerIndex, int itemIndex){ + if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ + if (itemIndex > -1 && itemIndex < static_cast(_containerItems[containerIndex].keys.size())){ + return &_containerItems[index].keys[itemIndex]; + } + } + + return nullptr; + } + + SCP_string* lookupContainerStringItem(int containerIndex, int itemIndex){ + if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ + if (itemIndex > -1 && itemIndex < static_cast(_containerItems[containerIndex].stringValues.size())){ + return &_containerItems[index].stringValues[itemIndex]; + } + } + + return nullptr; + } + + int* lookupContainerNumberItem(int containerIndex, int itemIndex){ + if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ + if (itemIndex > -1 && itemIndex < static_cast(_containerItems[containerIndex].numberValues.size())){ + return &_containerItems[index].numberValues[itemIndex]; + } + } + + return nullptr; + } + // many of the controls in this editor can lead to drastic actions, so this will be very useful. bool confirmAction(SCP_string question, SCP_string informativeText) @@ -185,6 +215,7 @@ class VariableDialogModel : public AbstractDialogModel { break; default: UNREACHABLE("Bad return value from confirmation message box in the Loadout dialog editor."); + return false; break; } } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 7a8e7d72c26..54af3a62868 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1062,8 +1062,9 @@ void VariableDialog::applyModel() } if (_currentContainer.empty() || selectedRow < 0){ - if (ui->containersTable->item(0,0) && strlen(ui->containersTable->item(0,0)->text().toStdString().c_str())){ + if (ui->containersTable->item(0,0)){ _currentContainer = ui->containersTable->item(0,0)->text().toStdString(); + ui->containersTable->item(row, 0).setSelected(true); } } From a18ac34c038adda00a1c439ff7b56d0c158756e6 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 22 Apr 2024 12:43:26 -0400 Subject: [PATCH 131/466] Fix Linter errors --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 4 ++-- qtfred/src/mission/dialogs/VariableDialogModel.h | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 285dc8f1e4b..a5ce5bf4284 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -972,7 +972,7 @@ std::pair VariableDialogModel::copyMapItem(int index, in // attempt did not work, try next number if (found) { - sprintf(newKey, "%s%i", *key.c_str(), ++count); + sprintf(newKey, "%s%i", key->c_str(), ++count); } } while (found && count < 100); @@ -1010,7 +1010,7 @@ std::pair VariableDialogModel::copyMapItem(int index, in // attempt did not work, try next number if (found) { - sprintf(newKey, "%s%i", *key.c_str(), ++count); + sprintf(newKey, "%s%i", key->c_str(), ++count); } } while (found && count < 100); diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index cf18b991a1b..5411c47e5cc 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -168,7 +168,7 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string* lookupContainerKey(int containerIndex, int itemIndex){ if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ if (itemIndex > -1 && itemIndex < static_cast(_containerItems[containerIndex].keys.size())){ - return &_containerItems[index].keys[itemIndex]; + return &_containerItems[containerIndex].keys[itemIndex]; } } @@ -178,7 +178,7 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string* lookupContainerStringItem(int containerIndex, int itemIndex){ if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ if (itemIndex > -1 && itemIndex < static_cast(_containerItems[containerIndex].stringValues.size())){ - return &_containerItems[index].stringValues[itemIndex]; + return &_containerItems[containerIndex].stringValues[itemIndex]; } } @@ -188,7 +188,7 @@ class VariableDialogModel : public AbstractDialogModel { int* lookupContainerNumberItem(int containerIndex, int itemIndex){ if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ if (itemIndex > -1 && itemIndex < static_cast(_containerItems[containerIndex].numberValues.size())){ - return &_containerItems[index].numberValues[itemIndex]; + return &_containerItems[containerIndex].numberValues[itemIndex]; } } From 880167230109480a80fc2f69905e552042a77947 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 22 Apr 2024 13:03:22 -0400 Subject: [PATCH 132/466] Fix more linter errors --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 10 ++++++---- qtfred/src/ui/dialogs/VariableDialog.cpp | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index a5ce5bf4284..901668942cd 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -995,10 +995,12 @@ std::pair VariableDialogModel::copyMapItem(int index, in return std::make_pair("", ""); } - bool found; + int copyValue = *value; SCP_string newKey = *key + "0"; int count = 0; + bool found; + do { found = false; for (int y = 0; y < static_cast(container->keys.size()); ++y){ @@ -1047,7 +1049,7 @@ bool VariableDialogModel::removeMapItem(int index, int itemIndex) } // container is valid. - auto item = lookupContainerKey(itemIndex); + auto item = lookupContainerKey(index, itemIndex); if (!item){ return false; @@ -1055,9 +1057,9 @@ bool VariableDialogModel::removeMapItem(int index, int itemIndex) // key is valid // Now double check that we have a data value. - if (container->string && lookupContainerStringItem(itemIndex)){ + if (container->string && lookupContainerStringItem(index, itemIndex)){ container->stringValues.erase(container->stringValues.begin() + itemIndex); - } else if (!container->string && lookupContainerNumberItem(itemIndex)){ + } else if (!container->string && lookupContainerNumberItem(index, itemIndex)){ container->numberValues.erase(container->numberValues.begin() + itemIndex); } else { return false; diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 54af3a62868..f95c98a0a79 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1064,7 +1064,7 @@ void VariableDialog::applyModel() if (_currentContainer.empty() || selectedRow < 0){ if (ui->containersTable->item(0,0)){ _currentContainer = ui->containersTable->item(0,0)->text().toStdString(); - ui->containersTable->item(row, 0).setSelected(true); + ui->containersTable->item(0, 0).setSelected(true); } } @@ -1111,7 +1111,7 @@ void VariableDialog::updateVariableOptions() // if nothing is selected, but something could be selected, make it so. if (row == -1 && ui->variablesTable->rowCount() > 0) { row = 0; - ui->variablesTable->item(row, 0).setSelected(true); + ui->variablesTable->item(row, 0)->setSelected(true); _currentVariable = ui->variablesTable->item(row, 0)->text().toStdString(); } From 4f0f57e711ea0897a88953617d21fc19b8b97f88 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 22 Apr 2024 16:46:04 -0400 Subject: [PATCH 133/466] Clean up the model in a few places --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 901668942cd..fd35f21e568 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -253,7 +253,7 @@ bool VariableDialogModel::getVariableType(int index) bool VariableDialogModel::getVariableNetworkStatus(int index) { auto variable = lookupVariable(index); - return (variable) ? ((variable->flags & SEXP_VARIABLE_NETWORK) > 0) : false; + return (variable) ? ((variable->flags & SEXP_VARIABLE_NETWORK) != 0) : false; } @@ -281,7 +281,7 @@ int VariableDialogModel::getVariableOnMissionCloseOrCompleteFlag(int index) bool VariableDialogModel::getVariableEternalFlag(int index) { auto variable = lookupVariable(index); - return (variable) ? ((variable->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) > 0) : false; + return (variable) ? ((variable->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) != 0) : false; } @@ -398,7 +398,7 @@ int VariableDialogModel::setVariableOnMissionCloseOrCompleteFlag(int index, int if (flags == 0) { variable->flags &= ~(SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS | SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); } else if (flags == 1) { - variable->flags &= ~(SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); + variable->flags &= ~SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE; variable->flags |= SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS; } else { variable->flags |= (SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS | SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE); @@ -580,7 +580,7 @@ bool VariableDialogModel::getContainerListOrMap(int index) bool VariableDialogModel::getContainerNetworkStatus(int index) { auto container = lookupContainer(index); - return (container) ? ((container->flags & SEXP_VARIABLE_NETWORK) > 0) : false; + return (container) ? ((container->flags & SEXP_VARIABLE_NETWORK) != 0) : false; } // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) @@ -603,7 +603,7 @@ int VariableDialogModel::getContainerOnMissionCloseOrCompleteFlag(int index) bool VariableDialogModel::getContainerEternalFlag(int index) { auto container = lookupContainer(index); - return (container) ? ((container->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) > 0) : false; + return (container) ? ((container->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) != 0) : false; } From 3a5a2ab43c5ee2090eaa702dd00e56ccb7bbeb1a Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 23 Apr 2024 12:01:17 -0400 Subject: [PATCH 134/466] Linter errors from recent changes --- qtfred/src/ui/dialogs/VariableDialog.cpp | 2 +- qtfred/ui/VariableDialog.ui | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index f95c98a0a79..282fe4413bd 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1064,7 +1064,7 @@ void VariableDialog::applyModel() if (_currentContainer.empty() || selectedRow < 0){ if (ui->containersTable->item(0,0)){ _currentContainer = ui->containersTable->item(0,0)->text().toStdString(); - ui->containersTable->item(0, 0).setSelected(true); + ui->containersTable->item(0, 0)->setSelected(true); } } diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 7417a5a0661..f43e67f7ec2 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -112,7 +112,7 @@ Persistence Options - + 10 From 55380f7e8f579dd93bb51474b86698fa3a07eedf Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 23 Apr 2024 13:02:48 -0400 Subject: [PATCH 135/466] Start on SetContainerListOrMap --- .../mission/dialogs/VariableDialogModel.cpp | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index fd35f21e568..eeb7d3b47a5 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -687,14 +687,105 @@ bool VariableDialogModel::setContainerValueType(int index, bool type) } // TODO finish these two functions. -bool VariableDialogModel::setContainerKeyType(int index, bool string) { +bool VariableDialogModel::setContainerKeyType(int index, bool string) +{ + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container){ + return false; + } + + + return false; } // This is the most complicated function, because we need to query the user on what they want to do if the had already entered data. bool VariableDialogModel::setContainerListOrMap(int index, bool list) { - return false; + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container){ + return !list; + } + + if (container->list && list) { + // no change needed + if (list){ + return list; + } + + // no data to either transfer to map/purge/ignore + if (container->string && container->stringValues.empty()){ + container->list = list; + + // still need to deal with extant keys by resizing data values. + if (!keys.empty()){ + stringValues.resize(keys.size()); + } + + return list; + } else if (!container->string && container->numberValues.empty()){ + container->list = list; + + // still need to deal with extant keys by resizing data values. + if (!keys.empty()){ + numberValues.resize(keys.size()); + } + + return list; + } + + QMessageBox msgBoxListToMapConfirm; + msgBoxListToMapConfirm.setText("This list already has data. Continue conversion to map?"); + msgBoxListToMapConfirm.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + msgBoxListToMapConfirm.setDefaultButton(QMessageBox::Cancel); + int ret = msgBoxListToMapConfirm.exec(); + + switch (ret) { + case QMessageBox::Yes: + break; + case QMessageBox::Cancel: + return container->list; + break; + default: + UNREACHABLE("Bad button value from confirmation message box in the Variable dialog editor, please report!"); + return false; + break; + } + } + + // now ask about data + QMessageBox msgBoxListToMapRetainData; + msgBoxListToMapRetainData.setText("Would you to keep the list data as keys or values, or would you like to purge the container contents?"); + msgBoxListToMapRetainData.addButton("Keep as Keys", QMessageBox::Discard); + msgBoxListToMapRetainData.addButton("Keep as Values", QMessageBox::Discard); + msgBoxListToMapRetainData.addButton("Purge", QMessageBox::Discard); + msgBoxListToMapRetainData.setStandardButtons(QMessageBox::Cancel); + msgBoxListToMapRetainData.setDefaultButton(QMessageBox::Cancel); + ret = msgBoxListToMapRetainData.exec(); + + switch (ret) { + case QMessageBox::Discard: + if () + container->list = list; + break; + case QMessageBox::Cancel: + return container->list; + break; + default: + UNREACHABLE("Bad button value from confirmation message box in the Variable dialog editor, please report!"); + return false; + break; + } + } + + } + + + return !list; } bool VariableDialogModel::setContainerNetworkStatus(int index, bool network) From 2a4894d6963b14ffafca7da975365b2c873ac015 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 24 Apr 2024 16:02:28 -0400 Subject: [PATCH 136/466] start implementing unified values in containers And write map key changing function. Revert "start implementing unified values in containers" This reverts commit 25eeae42413373bc7f12831285f8f639c8a93904. From 6134a42ef0b4e459eae28455f950f816484a1bbe Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 26 Apr 2024 00:00:48 -0400 Subject: [PATCH 137/466] Ton of fixes from testing --- .../mission/dialogs/VariableDialogModel.cpp | 27 ++++++++-------- qtfred/src/ui/dialogs/VariableDialog.cpp | 28 ++++++++--------- qtfred/ui/VariableDialog.ui | 31 ++++++++++++------- 3 files changed, 47 insertions(+), 39 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index eeb7d3b47a5..a77ee6b27b0 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -722,8 +722,8 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) container->list = list; // still need to deal with extant keys by resizing data values. - if (!keys.empty()){ - stringValues.resize(keys.size()); + if (!container->keys.empty()){ + container->stringValues.resize(container->keys.size()); } return list; @@ -731,8 +731,8 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) container->list = list; // still need to deal with extant keys by resizing data values. - if (!keys.empty()){ - numberValues.resize(keys.size()); + if (!container->keys.empty()){ + container->numberValues.resize(container->keys.size()); } return list; @@ -754,22 +754,21 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) UNREACHABLE("Bad button value from confirmation message box in the Variable dialog editor, please report!"); return false; break; - } - } + + } // now ask about data QMessageBox msgBoxListToMapRetainData; msgBoxListToMapRetainData.setText("Would you to keep the list data as keys or values, or would you like to purge the container contents?"); - msgBoxListToMapRetainData.addButton("Keep as Keys", QMessageBox::Discard); - msgBoxListToMapRetainData.addButton("Keep as Values", QMessageBox::Discard); - msgBoxListToMapRetainData.addButton("Purge", QMessageBox::Discard); + msgBoxListToMapRetainData.addButton("Keep as Keys", QMessageBox::ActionRole); + msgBoxListToMapRetainData.addButton("Keep as Values", QMessageBox::ApplyRole); + msgBoxListToMapRetainData.addButton("Purge", QMessageBox::RejectRole); msgBoxListToMapRetainData.setStandardButtons(QMessageBox::Cancel); msgBoxListToMapRetainData.setDefaultButton(QMessageBox::Cancel); ret = msgBoxListToMapRetainData.exec(); switch (ret) { case QMessageBox::Discard: - if () container->list = list; break; case QMessageBox::Cancel: @@ -779,13 +778,13 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) UNREACHABLE("Bad button value from confirmation message box in the Variable dialog editor, please report!"); return false; break; - } - } + + } } - return !list; + return !container->list; } bool VariableDialogModel::setContainerNetworkStatus(int index, bool network) @@ -1359,7 +1358,7 @@ const SCP_vector> VariableDialogModel::getContainerNam default: // this takes care of weird cases. The logic should be simple enough to not have bugs, but just in case, switch back to default. _listTextMode = 0; - listPrefix = "List of"; + listPrefix = "List of "; listPostscript = "s"; break; } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 282fe4413bd..6c55c2ed05e 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -22,7 +22,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) // Major Changes, like Applying the model, rejecting changes and updating the UI. - connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::checkValidModel); + connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::apply); connect(this, &QDialog::rejected, _model.get(), &VariableDialogModel::reject); connect(ui->variablesTable, @@ -195,7 +195,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->variablesTable->setColumnWidth(0, 90); + ui->variablesTable->setColumnWidth(0, 91); ui->variablesTable->setColumnWidth(1, 90); ui->variablesTable->setColumnWidth(2, 65); @@ -203,7 +203,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->containersTable->setColumnWidth(0, 90); + ui->containersTable->setColumnWidth(0, 91); ui->containersTable->setColumnWidth(1, 90); ui->containersTable->setColumnWidth(2, 65); @@ -213,7 +213,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); ui->containerContentsTable->setColumnWidth(0, 120); - ui->containerContentsTable->setColumnWidth(1, 120); + ui->containerContentsTable->setColumnWidth(1, 115); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't ui->setVariableAsStringRadio->setAutoExclusive(false); @@ -622,7 +622,7 @@ void VariableDialog::onSaveVariableAsEternalCheckboxClicked() } // If the model returns the old status, then the change failed and we're out of sync. - if (ui->setVariableAsEternalcheckbox->isChecked() == _model->setVariableEternalFlag(row, !ui->setVariableAsEternalcheckbox->isChecked())) { + if (ui->setVariableAsEternalcheckbox->isChecked() == _model->setVariableEternalFlag(row, ui->setVariableAsEternalcheckbox->isChecked())) { applyModel(); } else { ui->setVariableAsEternalcheckbox->setChecked(!ui->setVariableAsEternalcheckbox->isChecked()); @@ -638,7 +638,7 @@ void VariableDialog::onNetworkVariableCheckboxClicked() } // If the model returns the old status, then the change failed and we're out of sync. - if (ui->networkVariableCheckbox->isChecked() == _model->setVariableNetworkStatus(row, !ui->networkVariableCheckbox->isChecked())) { + if (ui->networkVariableCheckbox->isChecked() == _model->setVariableNetworkStatus(row, ui->networkVariableCheckbox->isChecked())) { applyModel(); } else { ui->networkVariableCheckbox->setChecked(!ui->networkVariableCheckbox->isChecked()); @@ -1054,11 +1054,11 @@ void VariableDialog::applyModel() // set the Add container row ++x; - if (ui->variablesTable->item(x, 0)){ - ui->variablesTable->item(x, 0)->setText("Add Container ..."); + if (ui->containersTable->item(x, 0)){ + ui->containersTable->item(x, 0)->setText("Add Container ..."); } else { QTableWidgetItem* item = new QTableWidgetItem("Add Container ..."); - ui->variablesTable->setItem(x, 0, item); + ui->containersTable->setItem(x, 0, item); } if (_currentContainer.empty() || selectedRow < 0){ @@ -1109,7 +1109,7 @@ void VariableDialog::updateVariableOptions() } // if nothing is selected, but something could be selected, make it so. - if (row == -1 && ui->variablesTable->rowCount() > 0) { + if (row == -1 && ui->variablesTable->rowCount() > 1) { row = 0; ui->variablesTable->item(row, 0)->setSelected(true); _currentVariable = ui->variablesTable->item(row, 0)->text().toStdString(); @@ -1196,8 +1196,8 @@ void VariableDialog::updateContainerOptions() ui->setContainerAsMapRadio->setChecked(false); // Disable Key Controls - ui->setContainerKeyAsStringRadio->setEnabled(false); - ui->setContainerKeyAsNumberRadio->setEnabled(false); + ui->setContainerKeyAsStringRadio->setEnabled(true); + ui->setContainerKeyAsNumberRadio->setEnabled(true); // Don't forget to change headings ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); @@ -1209,8 +1209,8 @@ void VariableDialog::updateContainerOptions() ui->setContainerAsMapRadio->setChecked(true); // Enabled Key Controls - ui->setContainerKeyAsStringRadio->setEnabled(true); - ui->setContainerKeyAsNumberRadio->setEnabled(true); + ui->setContainerKeyAsStringRadio->setEnabled(false); + ui->setContainerKeyAsNumberRadio->setEnabled(false); // Don't forget to change headings ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index f43e67f7ec2..3033a1c19d5 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -82,7 +82,10 @@ - QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + true QAbstractItemView::SingleSelection @@ -216,7 +219,10 @@ - QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + true QAbstractItemView::SingleSelection @@ -253,7 +259,10 @@ - QAbstractItemView::AnyKeyPressed|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + true QAbstractItemView::SingleSelection @@ -349,9 +358,9 @@ 370 - 20 - 201 - 161 + 190 + 211 + 181 @@ -409,9 +418,9 @@ 370 - 190 - 191 - 191 + 20 + 211 + 161 @@ -422,8 +431,8 @@ 10 20 - 181 - 152 + 161 + 141 From 4324b4110980d7b9a25668d3054d57a6779e8b40 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 26 Apr 2024 01:02:22 -0400 Subject: [PATCH 138/466] More Fixes and Tweaks based on testing --- qtfred/src/ui/dialogs/VariableDialog.cpp | 31 +- qtfred/ui/VariableDialog.ui | 541 ++++++++++++----------- 2 files changed, 297 insertions(+), 275 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 6c55c2ed05e..14d93a4d6ae 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -498,10 +498,6 @@ void VariableDialog::onDeleteVariableButtonPressed() void VariableDialog::onSetVariableAsStringRadioSelected() { - if (ui->setVariableAsStringRadio->isChecked()){ - return; - } - int currentRow = getCurrentVariableRow(); if (currentRow < 0){ @@ -520,10 +516,6 @@ void VariableDialog::onSetVariableAsStringRadioSelected() void VariableDialog::onSetVariableAsNumberRadioSelected() { - if (ui->setVariableAsNumberRadio->isChecked()){ - return; - } - int currentRow = getCurrentVariableRow(); if (currentRow < 0){ @@ -543,22 +535,17 @@ void VariableDialog::onSetVariableAsNumberRadioSelected() void VariableDialog::onDoNotSaveVariableRadioSelected() { - if (ui->doNotSaveVariableRadio->isChecked()){ - return; - } - int currentRow = getCurrentVariableRow(); if (currentRow < 0){ return; } - int ret = _model->setVariableOnMissionCloseOrCompleteFlag(currentRow, 1); + int ret = _model->setVariableOnMissionCloseOrCompleteFlag(currentRow, 0); - if (ret != 1){ + if (ret != 0){ applyModel(); } else { - ui->doNotSaveVariableRadio->setChecked(true); ui->saveContainerOnMissionCompletedRadio->setChecked(false); ui->saveVariableOnMissionCloseRadio->setChecked(false); } @@ -568,10 +555,6 @@ void VariableDialog::onDoNotSaveVariableRadioSelected() void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() { - if (ui->saveContainerOnMissionCompletedRadio->isChecked()){ - return; - } - int row = getCurrentVariableRow(); if (row < 0){ @@ -584,17 +567,12 @@ void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() applyModel(); } else { ui->doNotSaveVariableRadio->setChecked(false); - ui->saveContainerOnMissionCompletedRadio->setChecked(true); ui->saveVariableOnMissionCloseRadio->setChecked(false); } } void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() { - if (ui->saveContainerOnMissionCompletedRadio->isChecked()){ - return; - } - int row = getCurrentVariableRow(); if (row < 0){ @@ -609,7 +587,6 @@ void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() } else { ui->doNotSaveVariableRadio->setChecked(false); ui->saveContainerOnMissionCompletedRadio->setChecked(false); - ui->saveVariableOnMissionCloseRadio->setChecked(true); } } @@ -957,8 +934,10 @@ void VariableDialog::applyModel() for (x = 0; x < static_cast(variables.size()); ++x){ if (ui->variablesTable->item(x, 0)){ ui->variablesTable->item(x, 0)->setText(variables[x][0].c_str()); + ui->variablesTable->item(x, 0)->setFlags(ui->variablesTable->item(x, 0)->flags() | Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(variables[x][0].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); ui->variablesTable->setItem(x, 0, item); } @@ -970,8 +949,10 @@ void VariableDialog::applyModel() if (ui->variablesTable->item(x, 1)){ ui->variablesTable->item(x, 1)->setText(variables[x][1].c_str()); + ui->variablesTable->item(x, 1)->setFlags(ui->variablesTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(variables[x][1].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); ui->variablesTable->setItem(x, 1, item); } diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 3033a1c19d5..58f13ae9f3f 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -6,8 +6,8 @@ 0 0 - 608 - 665 + 641 + 678 @@ -36,7 +36,7 @@ - 270 + 330 120 81 101 @@ -77,10 +77,13 @@ 10 30 - 251 + 311 191 + + Qt::ScrollBarAlwaysOff + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed @@ -99,6 +102,9 @@ false + + false + false @@ -106,9 +112,9 @@ - 370 + 420 20 - 211 + 191 201 @@ -166,7 +172,7 @@ - 270 + 330 30 82 86 @@ -196,68 +202,36 @@ - - - - - - - 0 - 380 - - - - Containers - - + - 10 - 30 - 251 - 151 + 0 + 230 + 611 + 401 - - QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed - - - true - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - Qt::DotLine - - - false - - - - - - 10 - 190 - 351 - 191 - + + + 0 + 380 + - Container Contents + Containers - + 10 30 - 241 - 151 + 311 + 161 + + Qt::ScrollBarAlwaysOff + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed @@ -273,36 +247,141 @@ Qt::DotLine + + false + false - + - 260 + 10 + 200 + 401 + 191 + + + + Container Contents + + + + + 10 + 30 + 291 + 151 + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + Qt::DotLine + + + false + + + false + + + + + + 310 + 30 + 82 + 101 + + + + + + + Add + + + + + + + Copy + + + + + + + Item Up + + + + + + + Item Down + + + + + + + + + 310 + 160 + 80 + 23 + + + + + 8 + + + + Delete + + + + + + + 330 30 82 - 91 + 85 - + - + Add - + Copy - + 8 @@ -315,203 +394,165 @@ - - - - - 270 - 30 - 82 - 91 - - - - - - - Add - - - - - - - Copy - - - - - - - - 8 - - - - Delete - - - - - - - - - 370 - 190 - 211 - 181 - - - - Persistence Options - - + - 10 - 20 - 201 - 136 + 420 + 200 + 181 + 181 - - - - - No Persistence - - - - - - - Save on Mission Completed - - - - - - - Save on Mission Close - - - - - - - Eternal - - - - - - - Network Variable - - - - + + Persistence Options + + + + + 10 + 20 + 171 + 161 + + + + + + + No Persistence + + + + + + + Save on Mission Completed + + + + + + + Save on Mission Close + + + + + + + Eternal + + + + + + + Network Variable + + + + + - - - - - 370 - 20 - 211 - 161 - - - - Type Options - - + - 10 + 420 20 - 161 - 141 + 181 + 171 - - - - - Container Type - - - - - - - - - List - - - - - - - Map - - - - - - - - - Key Type - - - - - - - - - Number - - - - - - - String - - - - - - - - - Data Type - - - - - - - - - Number - - - - - - - String - - - - - - + + Type Options + + + + + 10 + 20 + 161 + 141 + + + + + + + Container Type + + + + + + + + + List + + + + + + + Map + + + + + + + + + Key Type + + + + + + + + + Number + + + + + + + String + + + + + + + + + Data Type + + + + + + + + + Number + + + + + + + String + + + + + + + From 5503aec7b4dca3ed27e45870b782e937f72be599 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 26 Apr 2024 15:57:33 -0400 Subject: [PATCH 139/466] Fix filtering out zero by accident --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index a77ee6b27b0..89341e2f317 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1475,7 +1475,7 @@ SCP_string VariableDialogModel::trimNumberString(SCP_string source) case '0': if (foundNonZero) return true; - else + else return false; case '1': case '2': @@ -1503,6 +1503,14 @@ SCP_string VariableDialogModel::trimNumberString(SCP_string source) } ); + // -0 is not a possible edge case because if we haven't found a digit, we don't copy zero. + // but "-" is and an empty string that should be zero is possible as well. + + // if we had a zero, but it was the only type of digit included and got filtered out. + if (ret.empty() && source.find('0') != std::string::npos){ + return "0"; + } + // if all that made it out was a dash, then return nothing. if (ret == "-"){ return ""; From 4bb8456f69f4b4bb12f2db15cf5914164d6619ff Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 26 Apr 2024 16:06:58 -0400 Subject: [PATCH 140/466] Further Fix and Rename function for clarity It turns out that an empty string is not the only possible desired value if the filtered string is "-". "-0" should return "0", but would return "" before this. --- .../mission/dialogs/VariableDialogModel.cpp | 18 +++++++++++++----- .../src/mission/dialogs/VariableDialogModel.h | 2 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 2 +- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 89341e2f317..88784aa3881 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1462,7 +1462,8 @@ const SCP_vector> VariableDialogModel::getContainerNam return outStrings; } -SCP_string VariableDialogModel::trimNumberString(SCP_string source) +// This function should not normally return an error. +SCP_string VariableDialogModel::trimIntegerString(SCP_string source) { SCP_string ret; bool foundNonZero = false; @@ -1471,7 +1472,7 @@ SCP_string VariableDialogModel::trimNumberString(SCP_string source) std::copy_if(source.begin(), source.end(), std::back_inserter(ret), [&foundNonZero, &ret](char c) -> bool { switch (c) { - // ignore leading zeros + // ignore leading zeros. If all digits are zero, this will be handled elsewhere case '0': if (foundNonZero) return true; @@ -1503,16 +1504,23 @@ SCP_string VariableDialogModel::trimNumberString(SCP_string source) } ); - // -0 is not a possible edge case because if we haven't found a digit, we don't copy zero. - // but "-" is and an empty string that should be zero is possible as well. + // -0 as a string value is not a possible edge case because if we haven't found a digit, we don't copy zero. + // "-" is possible and could be zero, however, and an empty string that should be zero is possible as well. + + // if we had a zero, but it was the only type of digit included and got filtered out. if (ret.empty() && source.find('0') != std::string::npos){ return "0"; } - // if all that made it out was a dash, then return nothing. + // if all that made it out was a dash, then return either zero or nothing. if (ret == "-"){ + // Checked for a filtered out 0 + if (source.find('0') != std::string::npos){ + return "0"; + } + return ""; } diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 5411c47e5cc..5bc452e10d2 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -121,7 +121,7 @@ class VariableDialogModel : public AbstractDialogModel { void initializeData(); - static SCP_string trimNumberString(SCP_string source); + static SCP_string trimIntegerString(SCP_string source); private: SCP_vector _variableItems; diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 14d93a4d6ae..c3d93ac383c 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -295,7 +295,7 @@ void VariableDialog::onVariablesTableUpdated() } } else { SCP_string source = item->text().toStdString(); - SCP_string temp = _model->trimNumberString(source); + SCP_string temp = _model->trimIntegerString(source); if (temp != source){ item->setText(temp.c_str()); From c24457ff6d942a13b2d88a159f3ea3856f86791e Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 26 Apr 2024 16:41:13 -0400 Subject: [PATCH 141/466] Add Clamping Function And more cleanup for trimIntegerString --- .../mission/dialogs/VariableDialogModel.cpp | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 88784aa3881..686f3326057 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -2,6 +2,7 @@ #include "parse/sexp.h" #include "parse/sexp_container.h" #include +#include namespace fso { namespace fred { @@ -1462,11 +1463,16 @@ const SCP_vector> VariableDialogModel::getContainerNam return outStrings; } -// This function should not normally return an error. +// This function is for cleaning up input strings that should be numbers. We could use std::stoi, +// but this helps to not erase the entire string if user ends up mistyping just one digit. +// If we ever allowed float types in sexp variables ... *shudder* ... we would definitely need a float +// version of this cleanup. SCP_string VariableDialogModel::trimIntegerString(SCP_string source) { SCP_string ret; - bool foundNonZero = false; + bool foundNonZero = false; + // I was tempted to prevent exceeding the max length of the destination c-string here, but no integer + // can exceed the 31 digit limit. And we *will* have an integer at the end of this. // filter out non-numeric digits std::copy_if(source.begin(), source.end(), std::back_inserter(ret), @@ -1506,25 +1512,32 @@ SCP_string VariableDialogModel::trimIntegerString(SCP_string source) // -0 as a string value is not a possible edge case because if we haven't found a digit, we don't copy zero. // "-" is possible and could be zero, however, and an empty string that should be zero is possible as well. + if (ret.empty() || ret == "-"){ + ret = "0"; + } + return std::move(clampIntegerString(ret)); +} +// Helper function for trimIntegerString that makes sure we don't try to save a value that overflows or underflows +// I don't recommend using outside of there, as there can be data loss if the input string is not cleaned first. +SCP_string VariableDialogModel::clampIntegerString(SCP_string source) +{ + try { + long test = std::stol(source); - // if we had a zero, but it was the only type of digit included and got filtered out. - if (ret.empty() && source.find('0') != std::string::npos){ - return "0"; - } - - // if all that made it out was a dash, then return either zero or nothing. - if (ret == "-"){ - // Checked for a filtered out 0 - if (source.find('0') != std::string::npos){ - return "0"; + if (test > INT_MAX) { + return "2147483647"; + } else if (test < INT_MIN) { + return "-2147483648"; } - return ""; + return std::move(source); + } + // most truly ludicrous cases should be caught before here in the calling function, so this should not cause much if any data loss + catch (...){ + return "0"; } - - return ret; } } // dialogs From ed3fcc8acaa9c0b1e5be2784378faad1fb46a3ac Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 26 Apr 2024 17:16:34 -0400 Subject: [PATCH 142/466] Switch Key Options and Data Options The data type options are going to be used much more regularly and always enabled, and so should be higher on the layout. --- qtfred/ui/VariableDialog.ui | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 58f13ae9f3f..20405741511 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -502,23 +502,23 @@ - + - Key Type + Data Type - + - + Number - + String @@ -527,23 +527,23 @@ - + - Data Type + Key Type - + - + Number - + String From d143add82d62c989fe08b8b1282fae892489e06d Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 26 Apr 2024 17:17:10 -0400 Subject: [PATCH 143/466] Fixes to container options updates And other fixes and adjustments --- .../mission/dialogs/VariableDialogModel.cpp | 22 +++++++------ .../src/mission/dialogs/VariableDialogModel.h | 4 ++- qtfred/src/ui/dialogs/VariableDialog.cpp | 31 ++++++++++++++----- 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 686f3326057..7cc6be14be9 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -258,7 +258,6 @@ bool VariableDialogModel::getVariableNetworkStatus(int index) } - // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) int VariableDialogModel::getVariableOnMissionCloseOrCompleteFlag(int index) { @@ -278,7 +277,6 @@ int VariableDialogModel::getVariableOnMissionCloseOrCompleteFlag(int index) return returnValue; } - bool VariableDialogModel::getVariableEternalFlag(int index) { auto variable = lookupVariable(index); @@ -571,6 +569,12 @@ bool VariableDialogModel::getContainerValueType(int index) return (container) ? container->string : true; } +bool VariableDialogModel::getContainerKeyType(int index) +{ + auto container = lookupContainer(index); + return (container) ? container-> +} + // true on list, false on map bool VariableDialogModel::getContainerListOrMap(int index) { @@ -955,10 +959,10 @@ std::pair VariableDialogModel::addMapItem(int index) do { conflict = false; - if (container->integerKeys){ - sprintf(newKey, "%i", count); - } else { + if (container->stringKeys){ sprintf(newKey, "key%i", count); + } else { + sprintf(newKey, "%i", count); } for (int x = 0; x < static_cast(container->keys.size()); ++x) { @@ -1316,7 +1320,7 @@ const SCP_vector> VariableDialogModel::getVariableValu const SCP_vector> VariableDialogModel::getContainerNames() { - // This logic makes the mode which we use to display, easily configureable. + // This logic makes the string which we use to display the type of the container, based on the specific mode we're using. SCP_string listPrefix; SCP_string listPostscript; @@ -1431,10 +1435,10 @@ const SCP_vector> VariableDialogModel::getContainerNam type = mapPrefix; - if (item.integerKeys){ - type += "Number"; - } else { + if (item.stringKeys){ type += "String"; + } else { + type += "Number"; } type += mapMidScript; diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 5bc452e10d2..f9566a8ab9a 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -25,7 +25,7 @@ struct containerInfo { bool deleted = false; bool list = true; bool string = true; - bool integerKeys = false; + bool stringKeys = false; int flags = 0; // this will allow us to look up the original values used in the mission previously. @@ -76,6 +76,8 @@ class VariableDialogModel : public AbstractDialogModel { // true on string, false on number bool getContainerValueType(int index); + // true on string, false on number -- this returns nonsense if it's not a map, please use responsibly! + bool getContainerKeyType(int index); // true on list, false on map bool getContainerListOrMap(int index); bool getContainerNetworkStatus(int index); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index c3d93ac383c..8dbc36861cd 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -212,14 +212,17 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) // Default to list ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setColumnWidth(0, 120); - ui->containerContentsTable->setColumnWidth(1, 115); + ui->containerContentsTable->setColumnWidth(0, 118); + ui->containerContentsTable->setColumnWidth(1, 117); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't + // and I don't mind just manually toggling them. ui->setVariableAsStringRadio->setAutoExclusive(false); ui->setVariableAsNumberRadio->setAutoExclusive(false); - ui->saveContainerOnMissionCompletedRadio->setAutoExclusive(false); + ui->doNotSaveVariableRadio->setAutoExclusive(false); + ui->saveVariableOnMissionCompletedRadio->setAutoExclusive(false); ui->saveVariableOnMissionCloseRadio->setAutoExclusive(false); + ui->setContainerAsMapRadio->setAutoExclusive(false); ui->setContainerAsListRadio->setAutoExclusive(false); ui->setContainerAsStringRadio->setAutoExclusive(false); @@ -1149,6 +1152,7 @@ void VariableDialog::updateContainerOptions() // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { row = item->row(); + break; } @@ -1177,8 +1181,8 @@ void VariableDialog::updateContainerOptions() ui->setContainerAsMapRadio->setChecked(false); // Disable Key Controls - ui->setContainerKeyAsStringRadio->setEnabled(true); - ui->setContainerKeyAsNumberRadio->setEnabled(true); + ui->setContainerKeyAsStringRadio->setEnabled(false); + ui->setContainerKeyAsNumberRadio->setEnabled(false); // Don't forget to change headings ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); @@ -1189,9 +1193,20 @@ void VariableDialog::updateContainerOptions() ui->setContainerAsListRadio->setChecked(false); ui->setContainerAsMapRadio->setChecked(true); - // Enabled Key Controls - ui->setContainerKeyAsStringRadio->setEnabled(false); - ui->setContainerKeyAsNumberRadio->setEnabled(false); + // Enable Key Controls + ui->setContainerKeyAsStringRadio->setEnabled(true); + ui->setContainerKeyAsNumberRadio->setEnabled(true); + + // string keys + if (_model->getContainerKeyType(row)){ + ui->setContainerKeyAsStringRadio->setChecked(true); + ui->setContainerKeyAsNumberRadio->setChecked(false); + + // number keys + } else { + ui->setContainerKeyAsStringRadio->setChecked(false); + ui->setContainerKeyAsNumberRadio->setChecked(true); + } // Don't forget to change headings ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); From ec4e50481bf5e13bcbe6f634eeb52770097c53ac Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 26 Apr 2024 17:21:18 -0400 Subject: [PATCH 144/466] Hopefully fix Variable Persistence Not saving --- qtfred/src/ui/dialogs/VariableDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 8dbc36861cd..8405cc0934d 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -85,7 +85,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onDoNotSaveVariableRadioSelected); - connect(ui->saveContainerOnMissionCompletedRadio, + connect(ui->saveVariableOnMissionCompletedRadio, &QRadioButton::clicked, this, &VariableDialog::onSaveVariableOnMissionCompleteRadioSelected); From fc11ddcd450ab9f82be65d30ccf1e3c1f173f0d2 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 26 Apr 2024 18:15:59 -0400 Subject: [PATCH 145/466] Fixes Based On Linter and Testing --- .../mission/dialogs/VariableDialogModel.cpp | 8 +- .../src/mission/dialogs/VariableDialogModel.h | 2 + qtfred/src/ui/dialogs/VariableDialog.cpp | 32 +++++-- qtfred/ui/VariableDialog.ui | 92 +++++++++++-------- 4 files changed, 87 insertions(+), 47 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 7cc6be14be9..d937e7aa86c 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -572,7 +572,7 @@ bool VariableDialogModel::getContainerValueType(int index) bool VariableDialogModel::getContainerKeyType(int index) { auto container = lookupContainer(index); - return (container) ? container-> + return (container) ? container->stringKeys : true; } // true on list, false on map @@ -1301,7 +1301,7 @@ const SCP_vector> VariableDialogModel::getVariableValu SCP_string notes = ""; if (item.deleted) { - notes = "Marked for Deletion"; + notes = "Flagged for Deletion"; } else if (item.originalName == "") { notes = "New"; } else if (item.name != item.originalName){ @@ -1454,7 +1454,7 @@ const SCP_vector> VariableDialogModel::getContainerNam if (item.deleted) { - notes = "Flaged for Deletion"; + notes = "Flagged for Deletion"; } else if (item.originalName == "") { notes = "New"; } else if (item.name != item.originalName){ @@ -1520,7 +1520,7 @@ SCP_string VariableDialogModel::trimIntegerString(SCP_string source) ret = "0"; } - return std::move(clampIntegerString(ret)); + return std::move(clampIntegerString(ret)); } // Helper function for trimIntegerString that makes sure we don't try to save a value that overflows or underflows diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index f9566a8ab9a..201b0e817e9 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -131,6 +131,8 @@ class VariableDialogModel : public AbstractDialogModel { int _listTextMode = 0; int _mapTextMode = 0; + static SCP_string clampIntegerString(SCP_string source); + variableInfo* lookupVariable(int index){ if(index > -1 && index < static_cast(_variableItems.size()) ){ return &_variableItems[index]; diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 8405cc0934d..b80d54492be 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -195,17 +195,17 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->variablesTable->setColumnWidth(0, 91); - ui->variablesTable->setColumnWidth(1, 90); - ui->variablesTable->setColumnWidth(2, 65); + ui->variablesTable->setColumnWidth(0, 95); + ui->variablesTable->setColumnWidth(1, 95); + ui->variablesTable->setColumnWidth(2, 120); ui->containersTable->setColumnCount(3); ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->containersTable->setColumnWidth(0, 91); - ui->containersTable->setColumnWidth(1, 90); - ui->containersTable->setColumnWidth(2, 65); + ui->containersTable->setColumnWidth(0, 95); + ui->containersTable->setColumnWidth(1, 95); + ui->containersTable->setColumnWidth(2, 120); ui->containerContentsTable->setColumnCount(2); @@ -1133,6 +1133,8 @@ void VariableDialog::updateContainerOptions() ui->deleteContainerButton->setEnabled(false); ui->setContainerAsStringRadio->setEnabled(false); ui->setContainerAsNumberRadio->setEnabled(false); + ui->setContainerKeyAsStringRadio->setEnabled(false); + ui->setContainerKeyAsNumberRadio->setEnabled(false); ui->doNotSaveContainerRadio->setEnabled(false); ui->saveContainerOnMissionCompletedRadio->setEnabled(false); ui->saveContainerOnMissionCloseRadio->setEnabled(false); @@ -1145,6 +1147,16 @@ void VariableDialog::updateContainerOptions() ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); ui->containerContentsTable->setRowCount(0); + // if there's no container, there's no container items + ui->addContainerItemButton->setEnabled(false); + ui->copyContainerItemButton->setEnabled(false); + ui->deleteContainerItemButton->setEnabled(false); + ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + ui->containerContentsTable->setRowCount(0); + ui->shiftItemDownButton->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + } else { auto items = ui->containersTable->selectedItems(); int row = -1; @@ -1240,7 +1252,7 @@ void VariableDialog::updateContainerDataOptions(bool list) { int row = getCurrentContainerRow(); - // No overarching container, no container contents + // Just in case, No overarching container, no container contents if (row < 0){ ui->addContainerItemButton->setEnabled(false); ui->copyContainerItemButton->setEnabled(false); @@ -1248,14 +1260,20 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); ui->containerContentsTable->setRowCount(0); + ui->shiftItemDownButton->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); return; // list type container } else if (list) { + // if there's no container, there's no container items ui->addContainerItemButton->setEnabled(true); ui->copyContainerItemButton->setEnabled(true); ui->deleteContainerItemButton->setEnabled(true); + ui->containerContentsTable->setRowCount(0); + ui->shiftItemDownButton->setEnabled(true); + ui->shiftItemUpButton->setEnabled(true); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 20405741511..e4f4c9043ad 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -7,13 +7,13 @@ 0 0 641 - 678 + 693 - 608 - 665 + 641 + 693 @@ -208,7 +208,7 @@ 0 230 611 - 401 + 411 @@ -226,7 +226,7 @@ 10 30 311 - 161 + 171 @@ -258,7 +258,7 @@ 10 - 200 + 210 401 191 @@ -302,8 +302,8 @@ 310 30 - 82 - 101 + 85 + 157 @@ -322,39 +322,59 @@ - + + + Shift Up + + + + + - Item Up + Shift Down - + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 8 + + - Item Down + Delete - - - - 310 - 160 - 80 - 23 - - - - - 8 - - - - Delete - - @@ -398,9 +418,9 @@ 420 - 200 + 210 181 - 181 + 191 @@ -411,8 +431,8 @@ 10 20 - 171 - 161 + 174 + 171 @@ -460,7 +480,7 @@ 420 20 181 - 171 + 181 @@ -472,7 +492,7 @@ 10 20 161 - 141 + 152 From 6e6b4098f32e3d90452a2b7f6b6d51b6b0d9a2ed Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 26 Apr 2024 19:03:22 -0400 Subject: [PATCH 146/466] Fix add container and add item rows They were not appearing. --- qtfred/src/ui/dialogs/VariableDialog.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index b80d54492be..22ce76e4218 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1037,7 +1037,6 @@ void VariableDialog::applyModel() } // set the Add container row - ++x; if (ui->containersTable->item(x, 0)){ ui->containersTable->item(x, 0)->setText("Add Container ..."); } else { @@ -1302,7 +1301,6 @@ void VariableDialog::updateContainerDataOptions(bool list) } } - ++x; if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText("Add item ..."); } else { From 94dbd95530a19dda928a2b99240032cc31323964 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 26 Apr 2024 19:10:24 -0400 Subject: [PATCH 147/466] Fix nullptr on copy --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index d937e7aa86c..5790db0c964 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -518,18 +518,18 @@ SCP_string VariableDialogModel::copyVariable(int index) // open slot found! if (!variableSearch){ // create the new entry in the model - _variableItems.emplace_back(); + variableSearch = &_variableItems.back(); // and set everything as a copy from the original, except original name and deleted. auto& newVariable = _variableItems.back(); newVariable.name = newName; - newVariable.flags = variableSearch->flags; - newVariable.string = variableSearch->string; + newVariable.flags = variable->flags; + newVariable.string = variable->string; if (newVariable.string) { - newVariable.stringValue = variableSearch->stringValue; + newVariable.stringValue = variable->stringValue; } else { - newVariable.numberValue = variableSearch->numberValue; + newVariable.numberValue = variable->numberValue; } return newName; From dac5fbeb826ab8d1762fa9611cbe29227ee105e8 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 26 Apr 2024 20:11:40 -0400 Subject: [PATCH 148/466] Fix more Container issues based on testing --- .../mission/dialogs/VariableDialogModel.cpp | 6 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 55 ++++--------------- 2 files changed, 13 insertions(+), 48 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 5790db0c964..6979dd51ee2 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -512,13 +512,13 @@ SCP_string VariableDialogModel::copyVariable(int index) do { SCP_string newName; - sprintf(newName, "%s_copy%i", variable->name.substr(0, TOKEN_LENGTH - 6).c_str(), count); + sprintf(newName, "%s_cpy%i", variable->name.substr(0, TOKEN_LENGTH - 6).c_str(), count); variableSearch = lookupVariableByName(newName); // open slot found! if (!variableSearch){ // create the new entry in the model - variableSearch = &_variableItems.back(); + _variableItems.emplace_back(); // and set everything as a copy from the original, except original name and deleted. auto& newVariable = _variableItems.back(); @@ -887,7 +887,7 @@ SCP_string VariableDialogModel::copyContainer(int index) return ""; } - // K.I.S.S. + // K.I.S.S. We could guarantee the names be unique, but so can the user, and there will definitely be a lower number of containers _containerItems.push_back(*container); _containerItems.back().name = "copy_" + _containerItems.back().name; _containerItems.back().name = _containerItems.back().name.substr(0, TOKEN_LENGTH - 1); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 22ce76e4218..1c28b345eb4 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -230,6 +230,13 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->saveContainerOnMissionCloseRadio->setAutoExclusive(false); ui->saveContainerOnMissionCompletedRadio->setAutoExclusive(false); + ui->variablesTable->setRowCount(0); + ui->containersTable->setRowCount(0); + ui->containerContentsTable->setRowCount(0); + ui->variablesTable->clearSelection(); + ui->containersTable->clearSelection(); + ui->containerContentsTable->clearSelection(); + applyModel(); } @@ -663,19 +670,11 @@ void VariableDialog::onDeleteContainerButtonPressed() return; } - // UI is somehow out of sync with the model, so update UI. - if (!_model->removeContainer(row)) { - applyModel(); - } - + applyModel(); } void VariableDialog::onSetContainerAsMapRadioSelected() { - if (ui->setContainerAsMapRadio->isChecked()){ - return; - } - int row = getCurrentContainerRow(); if (row < 0){ @@ -688,10 +687,6 @@ void VariableDialog::onSetContainerAsMapRadioSelected() void VariableDialog::onSetContainerAsListRadioSelected() { - if (ui->setContainerAsListRadio->isChecked()){ - return; - } - int row = getCurrentContainerRow(); if (row < 0){ @@ -705,10 +700,6 @@ void VariableDialog::onSetContainerAsListRadioSelected() void VariableDialog::onSetContainerAsStringRadioSelected() { - if (ui->setContainerAsStringRadio->isChecked()){ - return; - } - int row = getCurrentContainerRow(); if (row < 0){ @@ -721,11 +712,6 @@ void VariableDialog::onSetContainerAsStringRadioSelected() void VariableDialog::onSetContainerAsNumberRadioSelected() { - if (ui->setContainerAsNumberRadio->isChecked()){ - return; - } - - int row = getCurrentContainerRow(); if (row < 0){ @@ -738,10 +724,6 @@ void VariableDialog::onSetContainerAsNumberRadioSelected() void VariableDialog::onSetContainerKeyAsStringRadioSelected() { - if (ui->setContainerKeyAsStringRadio->isChecked()){ - return; - } - int row = getCurrentContainerRow(); if (row < 0){ @@ -755,10 +737,6 @@ void VariableDialog::onSetContainerKeyAsStringRadioSelected() void VariableDialog::onSetContainerKeyAsNumberRadioSelected() { - if (ui->setContainerKeyAsNumberRadio->isChecked()){ - return; - } - int row = getCurrentContainerRow(); if (row < 0){ @@ -771,10 +749,6 @@ void VariableDialog::onSetContainerKeyAsNumberRadioSelected() void VariableDialog::onDoNotSaveContainerRadioSelected() { - if (ui->doNotSaveContainerRadio->isChecked()){ - return; - } - int row = getCurrentContainerRow(); if (row < 0){ @@ -791,10 +765,6 @@ void VariableDialog::onDoNotSaveContainerRadioSelected() } void VariableDialog::onSaveContainerOnMissionCloseRadioSelected() { - if (ui->saveContainerOnMissionCloseRadio->isChecked()){ - return; - } - int row = getCurrentContainerRow(); if (row < 0){ @@ -812,10 +782,6 @@ void VariableDialog::onSaveContainerOnMissionCloseRadioSelected() void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() { - if (ui->saveContainerOnMissionCompletedRadio->isChecked()){ - return; - } - int row = getCurrentContainerRow(); if (row < 0){ @@ -970,7 +936,6 @@ void VariableDialog::applyModel() } // set the Add variable row - // TODO, fix this not appearing if (ui->variablesTable->item(x, 0)){ ui->variablesTable->item(x, 0)->setText("Add Variable ..."); } else { @@ -1005,7 +970,7 @@ void VariableDialog::applyModel() updateVariableOptions(); auto containers = _model->getContainerNames(); - ui->containersTable->setRowCount(static_cast(containers.size())); + ui->containersTable->setRowCount(static_cast(containers.size() + 1)); selectedRow = -1; for (x = 0; x < static_cast(containers.size()); ++x){ @@ -1045,7 +1010,7 @@ void VariableDialog::applyModel() } if (_currentContainer.empty() || selectedRow < 0){ - if (ui->containersTable->item(0,0)){ + if (ui->containersTable->item(0,0) && ui->containersTable->item(0,0)->text().toStdString() != "Add Container ..."){ _currentContainer = ui->containersTable->item(0,0)->text().toStdString(); ui->containersTable->item(0, 0)->setSelected(true); } From c984faeda1443e3e96c1c2809e38d54fa9147337 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 28 Apr 2024 01:37:29 -0400 Subject: [PATCH 149/466] Add Accept and Reject Signal Reactions --- .../mission/dialogs/VariableDialogModel.cpp | 6 ++-- .../src/mission/dialogs/VariableDialogModel.h | 2 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 30 +++++++++++++++++-- qtfred/src/ui/dialogs/VariableDialog.h | 3 ++ qtfred/ui/VariableDialog.ui | 12 ++++---- 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 6979dd51ee2..f1b8967660e 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -21,7 +21,7 @@ void VariableDialogModel::reject() _containerItems.clear(); } -void VariableDialogModel::checkValidModel() +bool VariableDialogModel::checkValidModel() { std::unordered_set namesTaken; std::unordered_set duplicates; @@ -100,7 +100,7 @@ void VariableDialogModel::checkValidModel() } if (messageOut1.empty()){ - apply(); + return true; } else { messageOut1 = "Please correct these variable, container and key names. The editor cannot apply your changes until they are fixed:\n\n" + messageOut1; @@ -108,6 +108,8 @@ void VariableDialogModel::checkValidModel() msgBox.setText(messageOut1.c_str()); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.exec(); + + return false; } diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 201b0e817e9..03c696881d5 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -116,7 +116,7 @@ class VariableDialogModel : public AbstractDialogModel { const SCP_vector> getVariableValues(); const SCP_vector> getContainerNames(); - void checkValidModel(); + bool checkValidModel(); bool apply() override; void reject() override; diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 1c28b345eb4..5f5e7ff9486 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -20,8 +20,11 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->setupUi(this); resize(QDialog::sizeHint()); // The best I can tell without some research, when a dialog doesn't use an underlying grid or layout, it needs to be resized this way before anything will show up - // Major Changes, like Applying the model, rejecting changes and updating the UI. + // Here we need to check that there are no issues with variable names or container names, or with maps having duplicate keys. + connect(ui->OkCancelButtons, &QDialogButtonBox::accepted, this, &VariableDialog::checkValidModel); + // Reject if the user wants to. + connect(ui->OkCancelButtons, &QDialogButtonBox::rejected, this, &VariableDialog::preReject); connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::apply); connect(this, &QDialog::rejected, _model.get(), &VariableDialogModel::reject); @@ -1422,7 +1425,8 @@ int VariableDialog::getCurrentVariableRow() return -1; } -int VariableDialog::getCurrentContainerRow(){ +int VariableDialog::getCurrentContainerRow() +{ auto items = ui->containersTable->selectedItems(); // yes, selected items returns a list, but we really should only have one item because multiselect will be off. @@ -1435,7 +1439,8 @@ int VariableDialog::getCurrentContainerRow(){ return -1; } -int VariableDialog::getCurrentContainerItemRow(){ +int VariableDialog::getCurrentContainerItemRow() +{ auto items = ui->containerContentsTable->selectedItems(); // yes, selected items returns a list, but we really should only have one item because multiselect will be off. @@ -1446,6 +1451,25 @@ int VariableDialog::getCurrentContainerItemRow(){ return -1; } +void VariableDialog::preReject() +{ + QMessageBox msgBox; + msgBox.setText("Are you sure you want to discard your changes?"); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + int ret = msgBox.exec(); + + if (ret == QMessageBox::Yes) { + this->reject(); + } +} + +void VariableDialog::checkValidModel() +{ + if (_model->checkValidModel()) { + accept(); + } +} + } // namespace dialogs } // namespace fred } // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 2d4bf1425fa..5faa2bb5045 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -27,6 +27,9 @@ class VariableDialog : public QDialog { // basically UpdateUI, but called when there is an inconsistency between model and UI void applyModel(); + void preReject(); + void checkValidModel(); + // Helper functions for this void updateVariableOptions(); void updateContainerOptions(); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index e4f4c9043ad..ec9f894a7ba 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -36,7 +36,7 @@ - 330 + 320 120 81 101 @@ -77,7 +77,7 @@ 10 30 - 311 + 301 191 @@ -172,7 +172,7 @@ - 330 + 320 30 82 86 @@ -225,7 +225,7 @@ 10 30 - 311 + 301 171 @@ -379,7 +379,7 @@ - 330 + 320 30 82 85 @@ -580,7 +580,7 @@ - + QDialogButtonBox::Cancel|QDialogButtonBox::Ok From 0c2d1a7a10a25464d770e2f6366ef2e6401e7424 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 28 Apr 2024 01:52:30 -0400 Subject: [PATCH 150/466] Save Changes --- qtfred/src/ui/dialogs/VariableDialog.cpp | 2 +- qtfred/ui/VariableDialog.ui | 71 ++++++++++++------------ 2 files changed, 38 insertions(+), 35 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 5f5e7ff9486..4b99b580c3d 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1459,7 +1459,7 @@ void VariableDialog::preReject() int ret = msgBox.exec(); if (ret == QMessageBox::Yes) { - this->reject(); + reject(); } } diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index ec9f894a7ba..665e78f7742 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -220,40 +220,6 @@ Containers - - - - 10 - 30 - 301 - 171 - - - - Qt::ScrollBarAlwaysOff - - - QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed - - - true - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - Qt::DotLine - - - false - - - false - - @@ -574,6 +540,43 @@ + + + + 10 + 30 + 301 + 171 + + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + Qt::DotLine + + + false + + + false + + + false + + From e902f6d07521bded8353b89f4ac69e1125ed9f7f Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 28 Apr 2024 13:38:40 -0400 Subject: [PATCH 151/466] More change based on testing --- qtfred/src/ui/dialogs/VariableDialog.cpp | 61 ++++++++++-------------- 1 file changed, 24 insertions(+), 37 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 4b99b580c3d..6c97ae5dfa4 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -200,7 +200,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); ui->variablesTable->setColumnWidth(0, 95); ui->variablesTable->setColumnWidth(1, 95); - ui->variablesTable->setColumnWidth(2, 120); + ui->variablesTable->setColumnWidth(2, 105); ui->containersTable->setColumnCount(3); ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); @@ -208,15 +208,15 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); ui->containersTable->setColumnWidth(0, 95); ui->containersTable->setColumnWidth(1, 95); - ui->containersTable->setColumnWidth(2, 120); + ui->containersTable->setColumnWidth(2, 105); ui->containerContentsTable->setColumnCount(2); // Default to list ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setColumnWidth(0, 118); - ui->containerContentsTable->setColumnWidth(1, 117); + ui->containerContentsTable->setColumnWidth(0, 150); + ui->containerContentsTable->setColumnWidth(1, 150); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't // and I don't mind just manually toggling them. @@ -290,9 +290,6 @@ void VariableDialog::onVariablesTableUpdated() // check if data column was altered // TODO! Set up comparison between last and current value - // TODO! Also this crashes because item->(x, 1) is null - // TODO! Variable is not editable - // TODO! Network container does not turn off if (item->column() == 1) { // Variable is a string @@ -341,10 +338,11 @@ void VariableDialog::onVariablesSelectionChanged() int row = getCurrentVariableRow(); if (row < 0){ + updateVariableOptions(); return; } - SCP_string newVariableName = ""; + SCP_string newVariableName; auto item = ui->variablesTable->item(row, 0); @@ -407,16 +405,16 @@ void VariableDialog::onContainersSelectionChanged() auto items = ui->containersTable->selectedItems(); - SCP_string newContainerName = ""; + int row = getCurrentContainerRow(); - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for(const auto& item : items) { - if (item->column() == 0){ - newContainerName = item->text().toStdString(); - break; - } + if (row < 0) { + updateContainerOptions(); + return; } + // guaranteed not to be null, since getCurrentContainerRow already checked. + SCP_string newContainerName = ui->containersTable->item(row, 0)->text().toStdString(); + if (newContainerName != _currentContainer){ _currentContainer = newContainerName; applyModel(); @@ -1027,7 +1025,9 @@ void VariableDialog::applyModel() void VariableDialog::updateVariableOptions() { - if (_currentVariable.empty()){ + int row = getCurrentVariableRow(); + + if (row < 0){ ui->copyVariableButton->setEnabled(false); ui->deleteVariableButton->setEnabled(false); ui->setVariableAsStringRadio->setEnabled(false); @@ -1051,14 +1051,6 @@ void VariableDialog::updateVariableOptions() ui->setVariableAsEternalcheckbox->setEnabled(true); ui->networkVariableCheckbox->setEnabled(true); - auto items = ui->variablesTable->selectedItems(); - int row = -1; - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - row = item->row(); - } - // if nothing is selected, but something could be selected, make it so. if (row == -1 && ui->variablesTable->rowCount() > 1) { row = 0; @@ -1095,7 +1087,9 @@ void VariableDialog::updateVariableOptions() void VariableDialog::updateContainerOptions() { - if (_currentContainer.empty()){ + int row = getCurrentContainerRow(); + + if (row < 0){ ui->copyContainerButton->setEnabled(false); ui->deleteContainerButton->setEnabled(false); ui->setContainerAsStringRadio->setEnabled(false); @@ -1126,14 +1120,6 @@ void VariableDialog::updateContainerOptions() } else { auto items = ui->containersTable->selectedItems(); - int row = -1; - - // yes, selected items returns a list, but we really should only have one item because multiselect will be off. - for (const auto& item : items) { - row = item->row(); - break; - } - ui->copyContainerButton->setEnabled(true); ui->deleteContainerButton->setEnabled(true); @@ -1310,7 +1296,6 @@ void VariableDialog::updateContainerDataOptions(bool list) } } - ++x; if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText("Add item ..."); } else { @@ -1417,7 +1402,7 @@ int VariableDialog::getCurrentVariableRow() // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { - if (item){ + if (item && item->column() == 0 && item->text().toStdString() != "Add Variable ...") { return item->row(); } } @@ -1431,7 +1416,7 @@ int VariableDialog::getCurrentContainerRow() // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { - if (item) { + if (item && item->column() == 0 && item->text().toStdString() != "Add Container ...") { return item->row(); } } @@ -1445,7 +1430,9 @@ int VariableDialog::getCurrentContainerItemRow() // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { - return item->row(); + if (item && item->column() == 0 && item->text().toStdString() != "Add Item ...") { + return item->row(); + } } return -1; From a8cc7d289606229c55c572d1c2270c23ef7da85b Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 28 Apr 2024 14:44:38 -0400 Subject: [PATCH 152/466] Appease the linter --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index f1b8967660e..705c7e0f3e9 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1522,7 +1522,7 @@ SCP_string VariableDialogModel::trimIntegerString(SCP_string source) ret = "0"; } - return std::move(clampIntegerString(ret)); + return clampIntegerString(ret); } // Helper function for trimIntegerString that makes sure we don't try to save a value that overflows or underflows @@ -1538,7 +1538,7 @@ SCP_string VariableDialogModel::clampIntegerString(SCP_string source) return "-2147483648"; } - return std::move(source); + return source; } // most truly ludicrous cases should be caught before here in the calling function, so this should not cause much if any data loss catch (...){ From f1dd86477aab364a93ff5ebb74653063f9dc45ae Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 28 Apr 2024 23:30:23 -0400 Subject: [PATCH 153/466] More Fixes Based on testing --- qtfred/src/ui/dialogs/VariableDialog.cpp | 20 +++++++++++++++++++- qtfred/ui/VariableDialog.ui | 16 ++++++++-------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 6c97ae5dfa4..82c7d9f683a 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -819,7 +819,7 @@ void VariableDialog::onSetContainerAsEternalCheckboxClicked() return; } - if (ui->setContainerAsEternalCheckbox->isChecked() != _model->setContainerNetworkStatus(row, ui->setContainerAsEternalCheckbox->isChecked())){ + if (ui->setContainerAsEternalCheckbox->isChecked() != _model->setContainerEternalFlag(row, ui->setContainerAsEternalCheckbox->isChecked())){ applyModel(); } } @@ -1010,6 +1010,24 @@ void VariableDialog::applyModel() ui->containersTable->setItem(x, 0, item); } + if (ui->containersTable->item(x, 1)){ + ui->containersTable->item(x, 1)->setFlags(ui->containersTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); + ui->containersTable->item(x, 1)->setText(""); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containersTable->setItem(x, 1, item); + } + + if (ui->containersTable->item(x, 2)){ + ui->containersTable->item(x, 2)->setFlags(ui->containersTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); + ui->containersTable->item(x, 2)->setText(""); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containersTable->setItem(x, 2, item); + } + if (_currentContainer.empty() || selectedRow < 0){ if (ui->containersTable->item(0,0) && ui->containersTable->item(0,0)->text().toStdString() != "Add Container ..."){ _currentContainer = ui->containersTable->item(0,0)->text().toStdString(); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 665e78f7742..8d03cac8d52 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -497,16 +497,16 @@ - + - Number + String - + - String + Number @@ -522,16 +522,16 @@ - + - Number + String - + - String + Number From c5b033235ad68f7ea37d11999c708f3b5eabc271 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Mon, 29 Apr 2024 00:03:02 -0400 Subject: [PATCH 154/466] More Fixes Based On Testing --- qtfred/src/ui/dialogs/VariableDialog.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 82c7d9f683a..2cd8bd04712 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -963,8 +963,12 @@ void VariableDialog::applyModel() } if (_currentVariable.empty() || selectedRow < 0){ - if (ui->variablesTable->item(0,0) && strlen(ui->variablesTable->item(0,0)->text().toStdString().c_str())){ - _currentVariable = ui->variablesTable->item(0,0)->text().toStdString(); + if (ui->variablesTable->item(0, 0) && !ui->variablesTable->item(0, 0)->text().toStdString().empty()){ + _currentVariable = ui->variablesTable->item(0, 0)->text().toStdString(); + } + + if (ui->variablesTable->item(0, 1)) { + _currentVariableData = ui->variablesTable->item(0, 1)->text().toStdString(); } } @@ -977,8 +981,10 @@ void VariableDialog::applyModel() for (x = 0; x < static_cast(containers.size()); ++x){ if (ui->containersTable->item(x, 0)){ ui->containersTable->item(x, 0)->setText(containers[x][0].c_str()); + ui->containersTable->item(x, 0)->setFlags(ui->containersTable->item(x, 0)->flags() | Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem(containers[x][0].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containersTable->setItem(x, 0, item); } @@ -1028,9 +1034,9 @@ void VariableDialog::applyModel() ui->containersTable->setItem(x, 2, item); } - if (_currentContainer.empty() || selectedRow < 0){ - if (ui->containersTable->item(0,0) && ui->containersTable->item(0,0)->text().toStdString() != "Add Container ..."){ - _currentContainer = ui->containersTable->item(0,0)->text().toStdString(); + if (selectedRow < 0 && ui->containersTable->rowCount() > 1) { + if (ui->containersTable->item(0, 0) && ui->containersTable->item(0, 0)->text().toStdString() != "Add Container ..."){ + _currentContainer = ui->containersTable->item(0, 0)->text().toStdString(); ui->containersTable->item(0, 0)->setSelected(true); } } @@ -1070,7 +1076,7 @@ void VariableDialog::updateVariableOptions() ui->networkVariableCheckbox->setEnabled(true); // if nothing is selected, but something could be selected, make it so. - if (row == -1 && ui->variablesTable->rowCount() > 1) { + if (row < 0 && ui->variablesTable->rowCount() > 1) { row = 0; ui->variablesTable->item(row, 0)->setSelected(true); _currentVariable = ui->variablesTable->item(row, 0)->text().toStdString(); @@ -1393,7 +1399,6 @@ void VariableDialog::updateContainerDataOptions(bool list) } } - ++x; if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText("Add key ..."); } else { From 898f8a99634aa75c8ffd3f09c6d59ade93baf964 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Mon, 29 Apr 2024 00:54:02 -0400 Subject: [PATCH 155/466] Increase size of dialog to store info better And ... more Fixes Based On Testing --- .../mission/dialogs/VariableDialogModel.cpp | 25 ++++++----- qtfred/src/ui/dialogs/VariableDialog.cpp | 14 +++--- qtfred/ui/VariableDialog.ui | 44 ++++++++++++------- 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 705c7e0f3e9..beb68060ba0 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -511,32 +511,35 @@ SCP_string VariableDialogModel::copyVariable(int index) int count = 1; variableInfo* variableSearch; + SCP_string newName; do { - SCP_string newName; - sprintf(newName, "%s_cpy%i", variable->name.substr(0, TOKEN_LENGTH - 6).c_str(), count); + sprintf(newName, "%i_%s", count, variable->name.substr(0, TOKEN_LENGTH - 4).c_str()); variableSearch = lookupVariableByName(newName); // open slot found! if (!variableSearch){ // create the new entry in the model - _variableItems.emplace_back(); + variableInfo newInfo; // and set everything as a copy from the original, except original name and deleted. - auto& newVariable = _variableItems.back(); - newVariable.name = newName; - newVariable.flags = variable->flags; - newVariable.string = variable->string; + newInfo.name = newName; + newInfo.flags = variable->flags; + newInfo.string = variable->string; - if (newVariable.string) { - newVariable.stringValue = variable->stringValue; + if (newInfo.string) { + newInfo.stringValue = variable->stringValue; } else { - newVariable.numberValue = variable->numberValue; + newInfo.numberValue = variable->numberValue; } + _variableItems.push_back(std::move(newInfo)); + return newName; } - } while (variableSearch != nullptr && count < 51); + + ++count; + } while (variableSearch != nullptr && count < 100); return ""; } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 2cd8bd04712..380c349aa13 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -358,8 +358,9 @@ void VariableDialog::onVariablesSelectionChanged() if (newVariableName != _currentVariable){ _currentVariable = newVariableName; - applyModel(); } + + applyModel(); } @@ -403,8 +404,6 @@ void VariableDialog::onContainersSelectionChanged() return; } - auto items = ui->containersTable->selectedItems(); - int row = getCurrentContainerRow(); if (row < 0) { @@ -417,8 +416,9 @@ void VariableDialog::onContainersSelectionChanged() if (newContainerName != _currentContainer){ _currentContainer = newContainerName; - applyModel(); } + + applyModel(); // Seems to be buggy unless I have this outside the if. } // TODO, finish this function @@ -1347,11 +1347,11 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); // keys I didn't bother to make separate. Should have done the same with values. - auto keys = _model->getMapKeys(row); + auto& keys = _model->getMapKeys(row); // string valued map. if (_model->getContainerValueType(row)){ - auto strings = _model->getStringValues(row); + auto& strings = _model->getStringValues(row); // use the map as the size because map containers are only as good as their keys anyway. ui->containerContentsTable->setRowCount(static_cast(keys.size()) + 1); @@ -1377,7 +1377,7 @@ void VariableDialog::updateContainerDataOptions(bool list) // number valued map } else { - auto numbers = _model->getNumberValues(row); + auto& numbers = _model->getNumberValues(row); ui->containerContentsTable->setRowCount(static_cast(keys.size()) + 1); int x; diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 8d03cac8d52..fa71f7e0888 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -6,13 +6,19 @@ 0 0 - 641 + 754 693 - 641 + 754 + 693 + + + + + 754 693 @@ -26,8 +32,14 @@ - 0 - 0 + 734 + 643 + + + + + 734 + 643 @@ -36,7 +48,7 @@ - 320 + 430 120 81 101 @@ -77,7 +89,7 @@ 10 30 - 301 + 411 191 @@ -112,7 +124,7 @@ - 420 + 530 20 191 201 @@ -172,7 +184,7 @@ - 320 + 430 30 82 86 @@ -207,7 +219,7 @@ 0 230 - 611 + 721 411 @@ -225,7 +237,7 @@ 10 210 - 401 + 511 191 @@ -237,7 +249,7 @@ 10 30 - 291 + 401 151 @@ -266,7 +278,7 @@ - 310 + 420 30 85 157 @@ -345,7 +357,7 @@ - 320 + 430 30 82 85 @@ -383,7 +395,7 @@ - 420 + 530 210 181 191 @@ -443,7 +455,7 @@ - 420 + 530 20 181 181 @@ -545,7 +557,7 @@ 10 30 - 301 + 411 171 From 5ee211203a4865c01c09d32e009b5f806fe8179c Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Mon, 29 Apr 2024 01:06:04 -0400 Subject: [PATCH 156/466] Fix Containers not deleting --- qtfred/src/ui/dialogs/VariableDialog.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 380c349aa13..5565cb10755 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -671,6 +671,7 @@ void VariableDialog::onDeleteContainerButtonPressed() return; } + _model->removeContainer(row); applyModel(); } From 4677fb8801ecb8f63da2478ad92adbaf025e9b1e Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 29 Apr 2024 14:02:13 -0400 Subject: [PATCH 157/466] Start Applying List values to Keys, etc. --- .../mission/dialogs/VariableDialogModel.cpp | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index beb68060ba0..7a478cc2e59 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -721,12 +721,11 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) return !list; } - if (container->list && list) { - // no change needed - if (list){ - return list; - } + if (container->list == list){ + return list; + } + if (container->list && !list) { // no data to either transfer to map/purge/ignore if (container->string && container->stringValues.empty()){ container->list = list; @@ -770,7 +769,7 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) // now ask about data QMessageBox msgBoxListToMapRetainData; msgBoxListToMapRetainData.setText("Would you to keep the list data as keys or values, or would you like to purge the container contents?"); - msgBoxListToMapRetainData.addButton("Keep as Keys", QMessageBox::ActionRole); + msgBoxListToMapRetainData.addButton("Convert to Keys", QMessageBox::ActionRole); msgBoxListToMapRetainData.addButton("Keep as Values", QMessageBox::ApplyRole); msgBoxListToMapRetainData.addButton("Purge", QMessageBox::RejectRole); msgBoxListToMapRetainData.setStandardButtons(QMessageBox::Cancel); @@ -778,8 +777,36 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) ret = msgBoxListToMapRetainData.exec(); switch (ret) { - case QMessageBox::Discard: + case QMessageBox::ActionRole: + // TODO! overwite the current keys with the current list values. Empty list values. + + case QMessageBox::ApplyRole: + + if ((container->string && container->stringValues.size() == container->keys.size()) + || (!container->string && container->numberValues.size() == container->keys.size())){ + container->list = list; + return; + } + + auto currentSize = (container->string) ? container->stringValues.size() : container->numberValues.size(); + int index = 0; + + + if (currentSize < containe) + + for (; currentSize < container->keys.size(); ++currentSize){ + if (container->string){ + + } + } + + case QMessageBox::RejectRole: + container->list = list; + container->stringValues.clear(); + container->numberValues.clear(); + container->keys.clear(); + return container->list; break; case QMessageBox::Cancel: return container->list; From cf356ed3dfb582ab57f88675699f0cfd733f898c Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 29 Apr 2024 17:26:57 -0400 Subject: [PATCH 158/466] Finish writing change from list to map --- .../mission/dialogs/VariableDialogModel.cpp | 85 +++++++++++++++---- .../src/mission/dialogs/VariableDialogModel.h | 12 +++ 2 files changed, 80 insertions(+), 17 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 7a478cc2e59..39bc5d16d90 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -756,21 +756,23 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) switch (ret) { case QMessageBox::Yes: break; + case QMessageBox::Cancel: return container->list; break; + default: UNREACHABLE("Bad button value from confirmation message box in the Variable dialog editor, please report!"); return false; - break; - + break; } // now ask about data QMessageBox msgBoxListToMapRetainData; msgBoxListToMapRetainData.setText("Would you to keep the list data as keys or values, or would you like to purge the container contents?"); - msgBoxListToMapRetainData.addButton("Convert to Keys", QMessageBox::ActionRole); + msgBoxListToMapRetainData.setInformativeText("Converting to keys will erase current keys and cannot be undone. Purging all container data cannot be undone.") msgBoxListToMapRetainData.addButton("Keep as Values", QMessageBox::ApplyRole); + msgBoxListToMapRetainData.addButton("Convert to Keys", QMessageBox::ActionRole); msgBoxListToMapRetainData.addButton("Purge", QMessageBox::RejectRole); msgBoxListToMapRetainData.setStandardButtons(QMessageBox::Cancel); msgBoxListToMapRetainData.setDefaultButton(QMessageBox::Cancel); @@ -778,27 +780,76 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) switch (ret) { case QMessageBox::ActionRole: - // TODO! overwite the current keys with the current list values. Empty list values. + // The easy version. (I know ... I should have standardized all storage as strings internally.... Now I'm in too deep) + if (container->string){ + container->keys = contianer->stringValues; + container->stringValues.clear(); + container->list = list; + return container->list; + } - case QMessageBox::ApplyRole: + // The hard version ...... I guess it's not that bad, actually + container->keys.clear(); - if ((container->string && container->stringValues.size() == container->keys.size()) - || (!container->string && container->numberValues.size() == container->keys.size())){ - container->list = list; - return; + for (auto& number : container->numberValues){ + SCP_string temp; + sprintf(temp, "%i", number); + contianer->keys.push_back(temp); } + + container->numberValues.clear(); + container->list = list; + return container->list; + + case QMessageBox::ApplyRole: auto currentSize = (container->string) ? container->stringValues.size() : container->numberValues.size(); - int index = 0; + // Keys and data are already set to the correct sizes. Key type should persist from the last time it was a map, so no need + // to adjust keys. + if (currentSize == container->keys.size()){ + container->list = list; + return container->list; + } - if (currentSize < containe) + // not enough data items. + if (currentSize < container->keys.size()){ + // just put the default value in them. Any string I specify for string values will + // be inconvenient to someone. + SCP_string newValue = ""; - for (; currentSize < container->keys.size(); ++currentSize){ if (container->string){ + container->stringValues.resize(container->keys.size(), newValue); + } else { + // But differentiating numbers by having zero be the default is a good idea and does + newvalue = "0"; + container->numberValues.resize(container->keys.size(), newValue); + } + + } else { + // here currentSize must be greater than the key size, because we already dealt with equal size. + // So let's add a few keys to make them level. + while (currentSize > container->keys.size() ){ + int keyIndex = 0; + SCPstring newKey; + if (container->stringKeys){ + sprintf(newKey, "key%i", keyIndex); + } else { + sprintf(newKey, "%i", KeyIndex); + } + + // avoid duplicates + if (!lookupContainerKeyByName(index, newKey)) { + container->keys.push_back(newKey); + } + + ++keyIndex; } - } + } + + container->list = list; + return container->list; case QMessageBox::RejectRole: @@ -808,20 +859,20 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) container->keys.clear(); return container->list; break; + case QMessageBox::Cancel: - return container->list; + return !list; break; + default: UNREACHABLE("Bad button value from confirmation message box in the Variable dialog editor, please report!"); return false; break; } - } - - return !container->list; + return !list; } bool VariableDialogModel::setContainerNetworkStatus(int index, bool network) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 03c696881d5..37c778c9c1e 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -179,6 +179,18 @@ class VariableDialogModel : public AbstractDialogModel { return nullptr; } + SCP_string* lookupContainerKeyByName(int containerIndex, SCP_string keyIn){ + if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ + for (const auto& key ; _containerItems[containerIndex].keys){ + if (key == keyIn){ + return &key; + } + } + } + + return nullptr; + } + SCP_string* lookupContainerStringItem(int containerIndex, int itemIndex){ if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ if (itemIndex > -1 && itemIndex < static_cast(_containerItems[containerIndex].stringValues.size())){ From d333694a0902d1b8945013132062f2f468422ea5 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Tue, 30 Apr 2024 00:50:26 -0400 Subject: [PATCH 159/466] Adding Button for Key/Value Swap And Add more width for table views --- qtfred/ui/VariableDialog.ui | 83 +++++++++++++------------------------ 1 file changed, 29 insertions(+), 54 deletions(-) diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index fa71f7e0888..ffea1219e2a 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -6,20 +6,20 @@ 0 0 - 754 - 693 + 899 + 729 - 754 - 693 + 899 + 729 - 754 - 693 + 899 + 729 @@ -36,19 +36,13 @@ 643 - - - 734 - 643 - - Variables - 430 + 550 120 81 101 @@ -89,7 +83,7 @@ 10 30 - 411 + 521 191 @@ -124,7 +118,7 @@ - 530 + 650 20 191 201 @@ -138,7 +132,7 @@ 10 20 - 221 + 181 181 @@ -184,7 +178,7 @@ - 430 + 550 30 82 86 @@ -219,8 +213,8 @@ 0 230 - 721 - 411 + 881 + 451 @@ -237,8 +231,8 @@ 10 210 - 511 - 191 + 631 + 221 @@ -249,7 +243,7 @@ 10 30 - 401 + 471 151 @@ -278,10 +272,10 @@ - 420 - 30 - 85 - 157 + 520 + 20 + 106 + 175 @@ -314,30 +308,11 @@ - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Vertical - - - - 20 - 40 - + + + Swap Key/Value - + @@ -357,7 +332,7 @@ - 430 + 550 30 82 85 @@ -395,7 +370,7 @@ - 530 + 650 210 181 191 @@ -409,7 +384,7 @@ 10 20 - 174 + 171 171 @@ -455,7 +430,7 @@ - 530 + 650 20 181 181 @@ -557,7 +532,7 @@ 10 30 - 411 + 521 171 From ba263d68892ecdf6d21c9c16d087420560eecf3b Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Tue, 30 Apr 2024 00:50:55 -0400 Subject: [PATCH 160/466] Trying to fixup The map/list switch function --- .../mission/dialogs/VariableDialogModel.cpp | 129 +++++++++--------- .../src/mission/dialogs/VariableDialogModel.h | 8 +- 2 files changed, 72 insertions(+), 65 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 39bc5d16d90..9513e6f7c0c 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -725,7 +725,7 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) return list; } - if (container->list && !list) { + if (container->list) { // no data to either transfer to map/purge/ignore if (container->string && container->stringValues.empty()){ container->list = list; @@ -769,21 +769,23 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) // now ask about data QMessageBox msgBoxListToMapRetainData; + msgBoxListToMapRetainData.setWindowTitle("List to Map Conversion"); msgBoxListToMapRetainData.setText("Would you to keep the list data as keys or values, or would you like to purge the container contents?"); - msgBoxListToMapRetainData.setInformativeText("Converting to keys will erase current keys and cannot be undone. Purging all container data cannot be undone.") - msgBoxListToMapRetainData.addButton("Keep as Values", QMessageBox::ApplyRole); - msgBoxListToMapRetainData.addButton("Convert to Keys", QMessageBox::ActionRole); - msgBoxListToMapRetainData.addButton("Purge", QMessageBox::RejectRole); - msgBoxListToMapRetainData.setStandardButtons(QMessageBox::Cancel); - msgBoxListToMapRetainData.setDefaultButton(QMessageBox::Cancel); + msgBoxListToMapRetainData.setInformativeText("Converting to keys will erase current keys and cannot be undone. Purging all container data cannot be undone."); + msgBoxListToMapRetainData.addButton("Keep as Values", QMessageBox::ActionRole); // No, these categories don't make sense, but QT makes underlying assumptions about where each button will be + msgBoxListToMapRetainData.addButton("Convert to Keys", QMessageBox::RejectRole); // Instead of putting them in order of input to the + msgBoxListToMapRetainData.addButton("Purge", QMessageBox::ApplyRole); + auto defaultButton = msgBoxListToMapRetainData.addButton("Cancel", QMessageBox::HelpRole); + msgBoxListToMapRetainData.setDefaultButton(defaultButton); ret = msgBoxListToMapRetainData.exec(); switch (ret) { - case QMessageBox::ActionRole: + case QMessageBox::RejectRole: // The easy version. (I know ... I should have standardized all storage as strings internally.... Now I'm in too deep) if (container->string){ - container->keys = contianer->stringValues; + container->keys = container->stringValues; container->stringValues.clear(); + container->stringValues.resize(container->keys.size(), ""); container->list = list; return container->list; } @@ -794,65 +796,66 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) for (auto& number : container->numberValues){ SCP_string temp; sprintf(temp, "%i", number); - contianer->keys.push_back(temp); + container->keys.push_back(temp); } container->numberValues.clear(); container->list = list; return container->list; + case QMessageBox::ActionRole: + { + auto currentSize = (container->string) ? container->stringValues.size() : container->numberValues.size(); + + // Keys and data are already set to the correct sizes. Key type should persist from the last time it was a map, so no need + // to adjust keys. + if (currentSize == container->keys.size()) { + container->list = list; + return container->list; + } + + // not enough data items. + if (currentSize < container->keys.size()) { + // just put the default value in them. Any string I specify for string values will + // be inconvenient to someone. + if (container->string) { + SCP_string newValue = ""; + container->stringValues.resize(container->keys.size(), newValue); + } + else { + // But differentiating numbers by having zero be the default is a good idea and does + container->numberValues.resize(container->keys.size(), 0); + } + + } + else { + // here currentSize must be greater than the key size, because we already dealt with equal size. + // So let's add a few keys to make them level. + while (currentSize > container->keys.size()) { + int keyIndex = 0; + SCP_string newKey; + + if (container->stringKeys) { + sprintf(newKey, "key%i", keyIndex); + } + else { + sprintf(newKey, "%i", keyIndex); + } + + // avoid duplicates + if (!lookupContainerKeyByName(index, newKey)) { + container->keys.push_back(newKey); + } + + ++keyIndex; + } + } + + container->list = list; + return container->list; + } case QMessageBox::ApplyRole: - auto currentSize = (container->string) ? container->stringValues.size() : container->numberValues.size(); - - // Keys and data are already set to the correct sizes. Key type should persist from the last time it was a map, so no need - // to adjust keys. - if (currentSize == container->keys.size()){ - container->list = list; - return container->list; - } - - // not enough data items. - if (currentSize < container->keys.size()){ - // just put the default value in them. Any string I specify for string values will - // be inconvenient to someone. - SCP_string newValue = ""; - - if (container->string){ - container->stringValues.resize(container->keys.size(), newValue); - } else { - // But differentiating numbers by having zero be the default is a good idea and does - newvalue = "0"; - container->numberValues.resize(container->keys.size(), newValue); - } - - } else { - // here currentSize must be greater than the key size, because we already dealt with equal size. - // So let's add a few keys to make them level. - while (currentSize > container->keys.size() ){ - int keyIndex = 0; - SCPstring newKey; - - if (container->stringKeys){ - sprintf(newKey, "key%i", keyIndex); - } else { - sprintf(newKey, "%i", KeyIndex); - } - - // avoid duplicates - if (!lookupContainerKeyByName(index, newKey)) { - container->keys.push_back(newKey); - } - - ++keyIndex; - } - } - - container->list = list; - return container->list; - - case QMessageBox::RejectRole: - container->list = list; container->stringValues.clear(); container->numberValues.clear(); @@ -860,7 +863,7 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) return container->list; break; - case QMessageBox::Cancel: + case QMessageBox::HelpRole: return !list; break; @@ -870,6 +873,10 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) break; } + } else { + // why yes, in this case it really is that simple. It doesn't matter what keys are doing, and there should already be valid values. + container->list = list; + return container->list; } return !list; diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 37c778c9c1e..afa76eb5ad0 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -25,7 +25,7 @@ struct containerInfo { bool deleted = false; bool list = true; bool string = true; - bool stringKeys = false; + bool stringKeys = true; int flags = 0; // this will allow us to look up the original values used in the mission previously. @@ -181,9 +181,9 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string* lookupContainerKeyByName(int containerIndex, SCP_string keyIn){ if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ - for (const auto& key ; _containerItems[containerIndex].keys){ - if (key == keyIn){ - return &key; + for (auto key = _containerItems[containerIndex].keys.begin(); key != _containerItems[containerIndex].keys.end(); ++key) { + if (*key == keyIn){ + return &(*key); } } } From 237bd53b6df8fa88ab34051ad268ef358ee8d931 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 30 Apr 2024 17:47:55 -0400 Subject: [PATCH 161/466] Rename this variable for clarity And add a comment --- .../mission/dialogs/VariableDialogModel.cpp | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 9513e6f7c0c..1055b504d29 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -33,19 +33,19 @@ bool VariableDialogModel::checkValidModel() } SCP_string messageOut1; - SCP_string messageOut2; + SCP_string messageBuffer; if (!duplicates.empty()){ for (const auto& item : duplicates){ - if (messageOut2.empty()){ - messageOut2 = "\"" + item + "\""; + if (messageBuffer.empty()){ + messageBuffer = "\"" + item + "\""; } else { - messageOut2 += ", ""\"" + item + "\""; + messageBuffer += ", ""\"" + item + "\""; } } sprintf(messageOut1, "There are %zu duplicate variables:\n", duplicates.size()); - messageOut1 += messageOut2 + "\n\n"; + messageOut1 += messageBuffer + "\n\n"; } duplicates.clear(); @@ -69,34 +69,34 @@ bool VariableDialogModel::checkValidModel() } } - messageOut2.clear(); + messageBuffer.clear(); if (!duplicates.empty()){ for (const auto& item : duplicates){ - if (messageOut2.empty()){ - messageOut2 = "\"" + item + "\""; + if (messageBuffer.empty()){ + messageBuffer = "\"" + item + "\""; } else { - messageOut2 += ", ""\"" + item + "\""; + messageBuffer += ", ""\"" + item + "\""; } } SCP_string temp; sprintf(temp, "There are %zu duplicate containers:\n\n", duplicates.size()); - messageOut1 += messageOut2 + "\n"; + messageOut1 += temp + messageBuffer + "\n"; } - messageOut2.clear(); + messageBuffer.clear(); if (!duplicateKeys.empty()){ for (const auto& key : duplicateKeys){ - messageOut2 += key; + messageBuffer += key; } SCP_string temp; sprintf(temp, "There are %zu duplicate map keys:\n\n", duplicateKeys.size()); - messageOut1 += messageOut2 + "\n"; + messageOut1 += messageBuffer + "\n"; } if (messageOut1.empty()){ @@ -111,11 +111,9 @@ bool VariableDialogModel::checkValidModel() return false; } - - - } +// TODO! This function in general just needs a lot of work. bool VariableDialogModel::apply() { From 2a8ec9cc450ff4258bfcb4a25df8bfe25b187219 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 30 Apr 2024 18:51:29 -0400 Subject: [PATCH 162/466] Write setContainerKeyType --- .../mission/dialogs/VariableDialogModel.cpp | 82 +++++++++++++++++-- 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 1055b504d29..2572025cad8 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -32,7 +32,7 @@ bool VariableDialogModel::checkValidModel() } } - SCP_string messageOut1; + SCP_string messageOut; SCP_string messageBuffer; if (!duplicates.empty()){ @@ -44,8 +44,8 @@ bool VariableDialogModel::checkValidModel() } } - sprintf(messageOut1, "There are %zu duplicate variables:\n", duplicates.size()); - messageOut1 += messageBuffer + "\n\n"; + sprintf(messageOut, "There are %zu duplicate variables:\n", duplicates.size()); + messageOut += messageBuffer + "\n\n"; } duplicates.clear(); @@ -83,7 +83,7 @@ bool VariableDialogModel::checkValidModel() SCP_string temp; sprintf(temp, "There are %zu duplicate containers:\n\n", duplicates.size()); - messageOut1 += temp + messageBuffer + "\n"; + messageOut += temp + messageBuffer + "\n"; } messageBuffer.clear(); @@ -96,13 +96,13 @@ bool VariableDialogModel::checkValidModel() SCP_string temp; sprintf(temp, "There are %zu duplicate map keys:\n\n", duplicateKeys.size()); - messageOut1 += messageBuffer + "\n"; + messageOut += messageBuffer + "\n"; } if (messageOut1.empty()){ return true; } else { - messageOut1 = "Please correct these variable, container and key names. The editor cannot apply your changes until they are fixed:\n\n" + messageOut1; + messageOut = "Please correct these issues. The editor cannot apply your changes until they are fixed:\n\n" + messageOut1; QMessageBox msgBox; msgBox.setText(messageOut1.c_str()); @@ -694,7 +694,6 @@ bool VariableDialogModel::setContainerValueType(int index, bool type) return container->string; } -// TODO finish these two functions. bool VariableDialogModel::setContainerKeyType(int index, bool string) { auto container = lookupContainer(index); @@ -704,7 +703,72 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) return false; } + if (container->stringKeys == string){ + return container->stringKeys; + } + + if (container->stringKeys) { + // Ok, this is the complicated type. First check if all keys can just quickly be transferred to numbers. + bool quickConvert = true; + + for (auto& key : container->keys) { + try { + std::stoi(key); + } + catch (...) { + quickConvert = false; + } + } + + // Don't even notify the user. Switching back is exceedingly easy. + if (quickConvert) { + container->stringKeys = string; + return container->stringKeys; + } + // If we couldn't convert easily, then we need some input from the user + // now ask about data + QMessageBox msgBoxListToMapRetainData; + msgBoxListToMapRetainData.setWindowTitle("Key Type Conversion"); + msgBoxListToMapRetainData.setText("Fred could not convert all string keys to numbers automatically. Would you like to use default keys, filter out integers from the current keys or cancel the operation?"); + msgBoxListToMapRetainData.setInformativeText("Current keys will be overwritten unless you cancel and cannot be restored. Filtering will keep *any* numerical digits and starting \"-\" in the string. Filtering also does not prevent duplicate keys."); + msgBoxListToMapRetainData.addButton("Use Default Keys", QMessageBox::ActionRole); // No, these categories don't make sense, but QT makes underlying assumptions about where each button will be + msgBoxListToMapRetainData.addButton("Filter Current Keys ", QMessageBox::RejectRole); + auto defaultButton = msgBoxListToMapRetainData.addButton("Cancel", QMessageBox::HelpRole); + msgBoxListToMapRetainData.setDefaultButton(defaultButton); + ret = msgBoxListToMapRetainData.exec(); + + switch(ret){ + // just use default keys + case QMessageBox::ActionRole: + { + int current = 0; + for (auto& key : container->keys){ + sprintf(key, "%i", current); + key = temp; + ++current; + } + + } + + // filter out current keys + case QMessageBox::RejectRole: + for (auto& key: container->keys){ + key = trimIntegerString(key); + } + + // cancel the operation + case QMessageBox::HelpRole: + return !string; + default: + UNREACHABLE("Bad button value from confirmation message box in the Variable editor, please report!"); + } + + } else { + // transferring to keys to string type. This can just change because a valid number is always a valid string. + container->stringKeys = string; + return container->stringKeys; + } return false; } @@ -760,7 +824,7 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) break; default: - UNREACHABLE("Bad button value from confirmation message box in the Variable dialog editor, please report!"); + UNREACHABLE("Bad button value from confirmation message box in the Variable editor, please report!"); return false; break; } @@ -866,7 +930,7 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) break; default: - UNREACHABLE("Bad button value from confirmation message box in the Variable dialog editor, please report!"); + UNREACHABLE("Bad button value from confirmation message box in the Variable editor, please report!"); return false; break; From 824b5bbb37659fc0f61a01f220621f60ec0f5d2b Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 30 Apr 2024 18:59:24 -0400 Subject: [PATCH 163/466] Write addNewVariable overload for typed items --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 6 ++++++ qtfred/src/mission/dialogs/VariableDialogModel.h | 1 + 2 files changed, 7 insertions(+) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 2572025cad8..2f967aee2c1 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -474,6 +474,12 @@ SCP_string VariableDialogModel::addNewVariable() return name; } +SCP_string VariableDialogModel::addNewVariable(SCP_string nameIn){ + _variableItems.emplace_back(); + _variableItems.back().name = nameIn; + return _variableItems.back().name; +} + SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName) { auto variable = lookupVariable(index); diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index afa76eb5ad0..78e56df0602 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -67,6 +67,7 @@ class VariableDialogModel : public AbstractDialogModel { int setVariableNumberValue(int index, int value); SCP_string addNewVariable(); + SCP_string addNewVariable(SCP_string nameIn); SCP_string changeVariableName(int index, SCP_string newName); SCP_string copyVariable(int index); // returns whether it succeeded From 72b13631c86f384894b105f298b0d9cfa0723192 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 30 Apr 2024 20:56:04 -0400 Subject: [PATCH 164/466] Implement sift up and down buttons --- .../mission/dialogs/VariableDialogModel.cpp | 48 +++++++++++ .../src/mission/dialogs/VariableDialogModel.h | 3 + qtfred/src/ui/dialogs/VariableDialog.cpp | 85 ++++++++++++++++--- qtfred/src/ui/dialogs/VariableDialog.h | 2 + 4 files changed, 128 insertions(+), 10 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 2f967aee2c1..93830b3654f 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1288,6 +1288,54 @@ std::pair VariableDialogModel::copyMapItem(int index, in return std::make_pair("", ""); } +// requires a model reload anyway, so no return value. +void VariableDialogModel::shiftListItemUp(int containerIndex, int itemIndex) +{ + auto container = lookupContainer(containerIndex); + + // handle bogus cases; < 1 is not a typo, since shifting the top item up should do nothing. + if (!container || !container->list || itemIndex < 1) { + return; + } + + // handle itemIndex out of bounds + if ( (container->string && itemIndex <= static_cast(container->stringValues.size())) + || (!container->string && itemIndex <= static_cast(container->numberValues.size())) ){ + return; + } + + // now that we know it's going to work, just swap em. + if (container->string) { + std::swap(container->stringValues[itemIndex], container->stringValues[itemIndex - 1]); + } else { + std::swap(container->numberValues[itemIndex], container->numberValues[itemIndex - 1]); + } +} + +// requires a model reload anyway, so no return value. +void VariableDialogModel::shiftListItemDown(int containerIndex, int itemIndex) +{ + auto container = lookupContainer(containerIndex); + + // handle bogus cases + if (!container || !container->list || itemIndex < 0) { + return; + } + + // handle itemIndex out of bounds. -1 is necessary. since the bottom item is cannot be moved down. + if ( (container->string && itemIndex <= static_cast(container->stringValues.size()) - 1) + || (!container->string && itemIndex <= static_cast(container->numberValues.size()) - 1) ){ + return; + } + + // now that we know it's going to work, just swap em. + if (container->string) { + std::swap(container->stringValues[itemIndex], container->stringValues[itemIndex + 1]); + } else { + std::swap(container->numberValues[itemIndex], container->numberValues[itemIndex + 1]); + } +} + // it's really because of this feature that we need data to only be in one or the other vector for maps. // If we attempted to maintain data automatically and there was a deletion, deleting the data in // both of the map's data vectors might be undesired, and not deleting takes the map immediately diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 78e56df0602..612d73663ee 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -107,6 +107,9 @@ class VariableDialogModel : public AbstractDialogModel { std::pair copyMapItem(int index, int itemIndex); bool removeMapItem(int index, int rowIndex); + void shiftListItemUp(int containerIndex, int itemIndex); + void shiftListItemDown(int containerIndex, int itemIndex); + SCP_string replaceMapItemKey(int index, SCP_string oldKey, SCP_string newKey); SCP_string changeMapItemStringValue(int index, SCP_string key, SCP_string newValue); SCP_string changeMapItemNumberValue(int index, SCP_string key, int newValue); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 5565cb10755..9dc629ce165 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -193,6 +193,16 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onDeleteContainerItemButtonPressed); + connect(ui->shiftItemUpButton, + &QPushButton::clicked, + this, + &VarableDialog::onShiftItemUpButtonPressed); + + connect(ui->shiftItemDownButton, + &QPushButton::clicked, + this, + &VarableDialog::onShiftItemDownButtonPressed); + ui->variablesTable->setColumnCount(3); ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); @@ -451,17 +461,12 @@ void VariableDialog::onContainerContentsSelectionChanged() } newContainerItemName = item->text().toStdString(); - item = ui->containerContentsTable->item(row, 1); + SCP_string newContainerDataText = (item) ? item->text().toStdString() : ""; - if (item){ - _currentContainerItemData = item->text().toStdString(); - } else { - _currentContainerItemData = ""; - } - - if (newContainerItemName != _currentContainerItem){ + if (newContainerItemName != _currentContainerItem || _currentContainerItemData != newContainerDataText){ _currentContainerItem = newContainerItemName; + _currentContainerItemData = newContainerDataText; applyModel(); } } @@ -888,6 +893,40 @@ void VariableDialog::onDeleteContainerItemButtonPressed() applyModel(); } +VariableDialog::onShiftItemUpButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + return; + } + + int itemRow = getCurrentContainerItemRow(); + + if (itemRow < 0){ + return; + } + + _model->shiftListItemUp(containerRow, itemRow); +} + +VariableDialog::onShiftItemDownButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + return; + } + + int itemRow = getCurrentContainerItemRow(); + + if (itemRow < 0){ + return; + } + + _model->shiftListItemUp(containerRow, itemRow); +} + VariableDialog::~VariableDialog(){}; // NOLINT @@ -1036,9 +1075,11 @@ void VariableDialog::applyModel() } if (selectedRow < 0 && ui->containersTable->rowCount() > 1) { - if (ui->containersTable->item(0, 0) && ui->containersTable->item(0, 0)->text().toStdString() != "Add Container ..."){ + if (ui->containersTable->item(0, 0)){ _currentContainer = ui->containersTable->item(0, 0)->text().toStdString(); + ui->containersTable->clearSelection(); ui->containersTable->item(0, 0)->setSelected(true); + } } @@ -1062,7 +1103,8 @@ void VariableDialog::updateVariableOptions() ui->saveVariableOnMissionCloseRadio->setEnabled(false); ui->setVariableAsEternalcheckbox->setEnabled(false); ui->networkVariableCheckbox->setEnabled(false); - + ui->onShiftItemUpButton->setEnabled(false); + ui->onShiftItemDownButton->setEnabled(false); return; } @@ -1128,6 +1170,8 @@ void VariableDialog::updateContainerOptions() ui->setContainerAsMapRadio->setEnabled(false); ui->setContainerAsListRadio->setEnabled(false); ui->networkContainerCheckbox->setEnabled(false); + ui->onShiftItemUpButton->setEnabled(false); + ui->onShiftItemDownButton->setEnabled(false); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); @@ -1177,6 +1221,7 @@ void VariableDialog::updateContainerOptions() // Don't forget to change headings ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + updateContainerDataOptions(true); } else { @@ -1255,6 +1300,7 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + // with string contents if (_model->getContainerValueType(row)){ auto strings = _model->getStringValues(row); @@ -1269,6 +1315,21 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setItem(x, 0, item); } + // set selected and enable shifting functions + if (strings[x] == _currentContainerItem){ + ui->containerContentsTable->clearSelection(); + ui->containerContentsTable->item(x,0)->setSelected(true); + + // more than one item and not already at the top of the list. + if (x > 0 && x < static_cast(strings.size())){ + ui->onShiftItemUpButton->setEnabled(false); + } + + if (x > -1 && x < static_Cast(strings.size()) - 1){ + ui->onShiftItemDownButton->setEnabled(false); + } + } + // empty out the second column as it's not needed in list mode if (ui->containerContentsTable->item(x, 1)){ ui->containerContentsTable->item(x, 1)->setText(""); @@ -1347,6 +1408,10 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + // Enable shift up and down buttons are off in Map mode. + ui->onShiftItemUpButton->setEnabled(false); + ui->onShiftItemDownButton->setEnabled(false); + // keys I didn't bother to make separate. Should have done the same with values. auto& keys = _model->getMapKeys(row); diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 5faa2bb5045..919c813e6e1 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -69,6 +69,8 @@ class VariableDialog : public QDialog { void onAddContainerItemButtonPressed(); void onCopyContainerItemButtonPressed(); void onDeleteContainerItemButtonPressed(); + void onShiftItemUpButtonPressed(); + void onShiftItemDownButtonPressed(); int getCurrentVariableRow(); int getCurrentContainerRow(); From eda645dcfc53722d09fd99007bcc49f1a947ee6b Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 30 Apr 2024 22:03:50 -0400 Subject: [PATCH 165/466] Make sure delete button changes text The button should now say restore when the item has already been deleted. Also, this expands how well we warn about deletions and will dynamically decide if we need to warn about deletion. --- .../mission/dialogs/VariableDialogModel.cpp | 89 ++++++++++++++++--- .../src/mission/dialogs/VariableDialogModel.h | 10 ++- qtfred/src/ui/dialogs/VariableDialog.cpp | 27 +++++- 3 files changed, 109 insertions(+), 17 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 93830b3654f..47b25fe9d7a 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -549,7 +549,7 @@ SCP_string VariableDialogModel::copyVariable(int index) } // returns whether it succeeded -bool VariableDialogModel::removeVariable(int index) +bool VariableDialogModel::removeVariable(int index, bool toDelete) { auto variable = lookupVariable(index); @@ -558,14 +558,32 @@ bool VariableDialogModel::removeVariable(int index) return false; } - SCP_string question = "Are you sure you want to delete this variable? Any references to it will have to be replaced."; - SCP_string info = ""; - if (!confirmAction(question, info)){ - return false; + if (variable->deleted == toDelete){ + return variable->deleted; } - variable->deleted = true; - return true; + if (toDelete){ + if (_deleteWarningCount < 3){ + + SCP_string question = "Are you sure you want to delete this variable? Any references to it will have to be changed."; + SCP_string info = ""; + + if (!confirmAction(question, info)){ + --_deleteWarningCount; + return variable->deleted; + } + + // adjust to the user's actions. If they are deleting variable after variable, allow after a while. + ++_deleteWarningCount; + } + + variable->deleted = toDelete; + return variable->deleted; + + } else { + variable->deleted = toDelete; + return variable->deleted; + } } @@ -1070,7 +1088,7 @@ SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newNam return newName; } -bool VariableDialogModel::removeContainer(int index) +bool VariableDialogModel::removeContainer(int index, bool toDelete) { auto container = lookupContainer(index); @@ -1078,8 +1096,31 @@ bool VariableDialogModel::removeContainer(int index) return false; } - container->deleted = true; - return container->deleted; + if (container->deleted == toDelete){ + return container->deleted; + } + + if (toDelete){ + + if (_deleteWarningCount < 3){ + SCP_string question = "Are you sure you want to delete this container? Any references to it will have to be changed."; + SCP_string info = ""; + + if (!confirmAction(question, info)){ + return variable->deleted; + } + + // adjust to the user's actions. If they are deleting container after container, allow after a while. + ++_deleteWarningCount; + } + + variable->deleted = toDelete; + return variable->deleted; + + } else { + variable->deleted = toDelete; + return variable->deleted; + } } SCP_string VariableDialogModel::addListItem(int index) @@ -1173,6 +1214,19 @@ bool VariableDialogModel::removeListItem(int containerIndex, int index) return false; } + if (_deleteWarningCount < 3){ + SCP_string question = "Are you sure you want to delete this list item? This can't be undone."; + SCP_string info = ""; + + if (!confirmAction(question, info)){ + --_deleteWarningCount; + return variable->deleted; + } + + // adjust to the user's actions. If they are deleting variable after variable, allow after a while. + ++_deleteWarningCount; + } + // Most efficient, given the situation (single deletions) if (container->string) { container->stringValues.erase(container->stringValues.begin() + index); @@ -1357,6 +1411,21 @@ bool VariableDialogModel::removeMapItem(int index, int itemIndex) } // key is valid + // double check that we want to delete + if (_deleteWarningCount < 3){ + SCP_string question = "Are you sure you want to delete this map item? This can't be undone."; + SCP_string info = ""; + + if (!confirmAction(question, info)){ + --_deleteWarningCount; + return variable->deleted; + } + + // adjust to the user's actions. If they are deleting variable after variable, allow after a while. + ++_deleteWarningCount; + } + + // Now double check that we have a data value. if (container->string && lookupContainerStringItem(index, itemIndex)){ container->stringValues.erase(container->stringValues.begin() + itemIndex); diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 612d73663ee..780729cb0ed 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -71,7 +71,7 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string changeVariableName(int index, SCP_string newName); SCP_string copyVariable(int index); // returns whether it succeeded - bool removeVariable(int index); + bool removeVariable(int index, bool toDelete); // Container Section @@ -97,15 +97,15 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string addContainer(SCP_string nameIn); SCP_string copyContainer(int index); SCP_string changeContainerName(int index, SCP_string newName); - bool removeContainer(int index); + bool removeContainer(int index, bool toDelete); SCP_string addListItem(int index); SCP_string copyListItem(int containerIndex, int index); - bool removeListItem(int containerindex, int index); + bool removeListItem(int containerindex, int index, bool toDelete); std::pair addMapItem(int index); std::pair copyMapItem(int index, int itemIndex); - bool removeMapItem(int index, int rowIndex); + bool removeMapItem(int index, int rowIndex, bool toDelete); void shiftListItemUp(int containerIndex, int itemIndex); void shiftListItemDown(int containerIndex, int itemIndex); @@ -135,6 +135,8 @@ class VariableDialogModel : public AbstractDialogModel { int _listTextMode = 0; int _mapTextMode = 0; + static int _deleteWarningCount = 0; + static SCP_string clampIntegerString(SCP_string source); variableInfo* lookupVariable(int index){ diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 9dc629ce165..e6f8aba01ca 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -508,8 +508,14 @@ void VariableDialog::onDeleteVariableButtonPressed() } // Because of the text update we'll need, this needs an applyModel, whether it fails or not. - _model->removeVariable(currentRow); - applyModel(); + if (ui->deleteVariableButton->item(currentRow, 2) && ui->deleteVariableButton->item(currentRow, 2)->text().toStdString() == "Flagged For Deletion"){ + _model->removeVariable(currentRow, false); + applyModel(); + } else { + _model->removeVariable(currentRow, true); + applyModel(); + } + } void VariableDialog::onSetVariableAsStringRadioSelected() @@ -1048,6 +1054,13 @@ void VariableDialog::applyModel() } } + // do we need to switch the delete button to a restore button? + if (ui->containersTable->item(row, 2) && ui->containersTable->item(row, 2)->text().toStdString() == "Flagged for Deletion"){ + ui->deleteContainerButton->setText("Restore"); + } else { + ui->deleteContainerButton->setText("Delete"); + } + // set the Add container row if (ui->containersTable->item(x, 0)){ ui->containersTable->item(x, 0)->setText("Add Container ..."); @@ -1096,6 +1109,7 @@ void VariableDialog::updateVariableOptions() if (row < 0){ ui->copyVariableButton->setEnabled(false); ui->deleteVariableButton->setEnabled(false); + ui->deleteVariableButton->setText("Delete"); ui->setVariableAsStringRadio->setEnabled(false); ui->setVariableAsNumberRadio->setEnabled(false); ui->doNotSaveVariableRadio->setEnabled(false); @@ -1125,12 +1139,18 @@ void VariableDialog::updateVariableOptions() _currentVariable = ui->variablesTable->item(row, 0)->text().toStdString(); } - // start populating values bool string = _model->getVariableType(row); ui->setVariableAsStringRadio->setChecked(string); ui->setVariableAsNumberRadio->setChecked(!string); + // do we need to switch the delete button to a restore button? + if (ui->variablesTable->item(row, 2) && ui->variablesTable->item(row, 2)->text().toStdString() == "Flagged for Deletion"){ + ui->deleteVariableButton->setText("Restore"); + } else { + ui->deleteVariableButton->setText("Delete"); + } + int ret = _model->getVariableOnMissionCloseOrCompleteFlag(row); if (ret == 0){ @@ -1159,6 +1179,7 @@ void VariableDialog::updateContainerOptions() if (row < 0){ ui->copyContainerButton->setEnabled(false); ui->deleteContainerButton->setEnabled(false); + ui->deleteContainerButton->setText("Delete"); ui->setContainerAsStringRadio->setEnabled(false); ui->setContainerAsNumberRadio->setEnabled(false); ui->setContainerKeyAsStringRadio->setEnabled(false); From 83404273889d22ce8b10238eb6b3f9a32d2d2973 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 30 Apr 2024 22:15:31 -0400 Subject: [PATCH 166/466] Make sure shift buttons apply the model --- qtfred/src/ui/dialogs/VariableDialog.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index e6f8aba01ca..ae107c155f4 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -914,6 +914,7 @@ VariableDialog::onShiftItemUpButtonPressed() } _model->shiftListItemUp(containerRow, itemRow); + applyModel(); } VariableDialog::onShiftItemDownButtonPressed() @@ -931,6 +932,7 @@ VariableDialog::onShiftItemDownButtonPressed() } _model->shiftListItemUp(containerRow, itemRow); + applyModel(); } @@ -1505,7 +1507,6 @@ void VariableDialog::updateContainerDataOptions(bool list) } } - int VariableDialog::getCurrentVariableRow() { auto items = ui->variablesTable->selectedItems(); From d04ce617fe2ba47b576de86fad8787ecf623f857 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 1 May 2024 00:53:45 -0400 Subject: [PATCH 167/466] Add swap keys and values button --- .../mission/dialogs/VariableDialogModel.cpp | 101 ++++++++++++++++++ .../src/mission/dialogs/VariableDialogModel.h | 6 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 23 +++- qtfred/src/ui/dialogs/VariableDialog.h | 1 + 4 files changed, 125 insertions(+), 6 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 47b25fe9d7a..bd014348f35 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1483,6 +1483,106 @@ SCP_string VariableDialogModel::changeMapItemStringValue(int index, SCP_string k return ""; } +void VariableDialogModel::swapKeyAndValues(int index) +{ + auto container = lookupContainer(index); + + // bogus cases + if (!container || container->list){ + return; + } + + // data type is the same as the key type + if (container->string == container->stringKeys){ + // string-string is the easiest case + if (container->string){ + std::swap(container->stringValues, container->keys); + + // Complicated + } else { + // All right, make a copy. + SCP_vector keysCopy = container->keys; + + // easy part 1 + for (int x = 0; x < static_cast(container->numberValues.size()); ++x) { + // Honestly, if we did our job correctly, this shouldn't happen, but just in case. + if (x >= static_cast(container->keys.size()) ){ + // emplacing should be sufficient since we start at index 0. + container->keys.emplace_back(); + keysCopy.emplace_back(); + } + + container->keys[x] = ""; + sprintf(container->keys[x], "%i", container->numberValues[x]); + } + + // not as easy part 2 + for (int x = 0; x < static_cast(container->keysCopy)) { + if (container->keysCopy[x] == ""){ + container->numberValues[x] = 0; + } else { + try { + // why *yes* it did occur to me that I made a mistake when I designed this + int temp = std::stoi(container->keysCopy[x]); + container->numberValues[x] = temp; + } + catch(...){ + container->numberValues[x] = 0; + } + } + } + } + // not the same types + } else { + // Ok. Because keys are always strings, it will be easier when keys are numbers, because they are underlied by strings. + if (container->string){ + // Make a copy of the keys.... + SCP_vector keysCopy = container->keys; + // make the easy transfer from stringvalues to keys. Requiring that key values change type. + container->keys = container->stringValues; + container->stringKeys = true; + + for (int x = 0; x < static_cast(container->keysCopy.size()); ++x){ + // This *is* likely to happen as these sizes were not in sync. + if (x >= static_cast(container->numberValues.size())){ + container->numberValues.emplace_back(); + } + + try { + // why *yes* it did occur to me that I made a mistake when I designed this + int temp = std::stoi(container->keysCopy[x]); + container->numberValues[x] = temp; + } + catch(...){ + container->numberValues[x] = 0; + } + } + + container->string = false; + + // so here values are numbers and keys are strings. This might actually be easier than I thought. + } else { + // Directly copy key strings to the string values + container->stringValues = container->keys; + + // Transfer the number values to a temporary string, then place that string in the keys vector + for (int x = 0; x < static_cast(container->numberValues.size()); ++x){ + // Here, this shouldn't happen, but just in case. The direct assignment above is where it could have been mis-aligned. + if (x >= static_cast(container->keys.size())){ + container->keys.emplace_back(); + } + + sprintf(container->keys[x], "%i", container->numberValues[x]); + } + + // change the types of the container keys and values. + container->string = true; + container->stringKeys = false; + } + + } +} + SCP_string VariableDialogModel::changeMapItemNumberValue(int index, SCP_string key, int newValue) { auto container = lookupContainer(index); @@ -1508,6 +1608,7 @@ SCP_string VariableDialogModel::changeMapItemNumberValue(int index, SCP_string k return ""; } + // These functions should only be called when the container is guaranteed to exist! const SCP_vector& VariableDialogModel::getMapKeys(int index) { diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 780729cb0ed..92fceba010b 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -101,11 +101,11 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string addListItem(int index); SCP_string copyListItem(int containerIndex, int index); - bool removeListItem(int containerindex, int index, bool toDelete); + bool removeListItem(int containerindex, int index); std::pair addMapItem(int index); std::pair copyMapItem(int index, int itemIndex); - bool removeMapItem(int index, int rowIndex, bool toDelete); + bool removeMapItem(int index, int rowIndex); void shiftListItemUp(int containerIndex, int itemIndex); void shiftListItemDown(int containerIndex, int itemIndex); @@ -118,6 +118,8 @@ class VariableDialogModel : public AbstractDialogModel { const SCP_vector& getStringValues(int index); const SCP_vector& getNumberValues(int index); + void swapKeyAndValues(int index); + const SCP_vector> getVariableValues(); const SCP_vector> getContainerNames(); bool checkValidModel(); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index ae107c155f4..384a21288f7 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -196,13 +196,17 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) connect(ui->shiftItemUpButton, &QPushButton::clicked, this, - &VarableDialog::onShiftItemUpButtonPressed); + &VariableDialog::onShiftItemUpButtonPressed); connect(ui->shiftItemDownButton, &QPushButton::clicked, this, - &VarableDialog::onShiftItemDownButtonPressed); + &VariableDialog::onShiftItemDownButtonPressed); + connect(ui->swapKeysAndValuesButton, + &QPushButton::clicked, + this, + &VariableDialog::onSwapKeysAndValuesButtonPressed); ui->variablesTable->setColumnCount(3); ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); @@ -899,7 +903,7 @@ void VariableDialog::onDeleteContainerItemButtonPressed() applyModel(); } -VariableDialog::onShiftItemUpButtonPressed() +void VariableDialog::onShiftItemUpButtonPressed() { int containerRow = getCurrentContainerRow(); @@ -917,7 +921,7 @@ VariableDialog::onShiftItemUpButtonPressed() applyModel(); } -VariableDialog::onShiftItemDownButtonPressed() +void VariableDialog::onShiftItemDownButtonPressed() { int containerRow = getCurrentContainerRow(); @@ -935,6 +939,17 @@ VariableDialog::onShiftItemDownButtonPressed() applyModel(); } +void VariableDialog::onSwapKeysAndValuesButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + return; + } + + _model->swapKeyAndValues(containerRow); + applyModel(); +} VariableDialog::~VariableDialog(){}; // NOLINT diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 919c813e6e1..10b1704e3a1 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -71,6 +71,7 @@ class VariableDialog : public QDialog { void onDeleteContainerItemButtonPressed(); void onShiftItemUpButtonPressed(); void onShiftItemDownButtonPressed(); + void onSwapKeysAndValuesButtonPressed(); int getCurrentVariableRow(); int getCurrentContainerRow(); From 75853a0cb95ef81f56353d7011a711564d6e75d0 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 2 May 2024 02:25:17 -0400 Subject: [PATCH 168/466] sync modified MessageBox call PR 6121 in the SCP repository modifies the MessageBox call for the FSM import. This replicates the same change for the XWI import. --- fred2/freddoc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fred2/freddoc.cpp b/fred2/freddoc.cpp index 1462ab08f4c..49817803bd8 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -688,7 +688,7 @@ void CFREDDoc::OnFileImportXWI() if (num_files > 1) { create_new_mission(); - MessageBox(NULL, "Import complete. Please check the destination folder to verify all missions were imported successfully.", "Status", MB_OK); + Fred_view_wnd->MessageBox("Import complete. Please check the destination folder to verify all missions were imported successfully.", "Status", MB_OK); } else if (num_files == 1) { From 50762246cbf2e52966519bcf763cbc6fd3da423d Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 3 May 2024 23:22:27 -0400 Subject: [PATCH 169/466] Fixes based on linter And size adjustments --- .../mission/dialogs/VariableDialogModel.cpp | 36 +++++++++---------- .../src/mission/dialogs/VariableDialogModel.h | 2 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 35 ++++++++++-------- qtfred/ui/VariableDialog.ui | 22 ++++++------ 4 files changed, 50 insertions(+), 45 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index bd014348f35..e9465b9f16c 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -12,7 +12,8 @@ namespace dialogs { VariableDialogModel::VariableDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { - initializeData(); + _deleteWarningCount = 0; + initializeData(); } void VariableDialogModel::reject() @@ -99,13 +100,13 @@ bool VariableDialogModel::checkValidModel() messageOut += messageBuffer + "\n"; } - if (messageOut1.empty()){ + if (messageOut.empty()){ return true; } else { - messageOut = "Please correct these issues. The editor cannot apply your changes until they are fixed:\n\n" + messageOut1; + messageOut = "Please correct these issues. The editor cannot apply your changes until they are fixed:\n\n" + messageOut; QMessageBox msgBox; - msgBox.setText(messageOut1.c_str()); + msgBox.setText(messageOut.c_str()); msgBox.setStandardButtons(QMessageBox::Ok); msgBox.exec(); @@ -760,7 +761,7 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) msgBoxListToMapRetainData.addButton("Filter Current Keys ", QMessageBox::RejectRole); auto defaultButton = msgBoxListToMapRetainData.addButton("Cancel", QMessageBox::HelpRole); msgBoxListToMapRetainData.setDefaultButton(defaultButton); - ret = msgBoxListToMapRetainData.exec(); + auto ret = msgBoxListToMapRetainData.exec(); switch(ret){ // just use default keys @@ -769,7 +770,6 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) int current = 0; for (auto& key : container->keys){ sprintf(key, "%i", current); - key = temp; ++current; } @@ -1107,19 +1107,19 @@ bool VariableDialogModel::removeContainer(int index, bool toDelete) SCP_string info = ""; if (!confirmAction(question, info)){ - return variable->deleted; + return container->deleted; } // adjust to the user's actions. If they are deleting container after container, allow after a while. ++_deleteWarningCount; } - variable->deleted = toDelete; - return variable->deleted; + container->deleted = toDelete; + return container->deleted; } else { - variable->deleted = toDelete; - return variable->deleted; + container->deleted = toDelete; + return container->deleted; } } @@ -1220,7 +1220,7 @@ bool VariableDialogModel::removeListItem(int containerIndex, int index) if (!confirmAction(question, info)){ --_deleteWarningCount; - return variable->deleted; + return container->deleted; } // adjust to the user's actions. If they are deleting variable after variable, allow after a while. @@ -1418,7 +1418,7 @@ bool VariableDialogModel::removeMapItem(int index, int itemIndex) if (!confirmAction(question, info)){ --_deleteWarningCount; - return variable->deleted; + return container->deleted; } // adjust to the user's actions. If they are deleting variable after variable, allow after a while. @@ -1517,13 +1517,13 @@ void VariableDialogModel::swapKeyAndValues(int index) } // not as easy part 2 - for (int x = 0; x < static_cast(container->keysCopy)) { - if (container->keysCopy[x] == ""){ + for (int x = 0; x < static_cast(keysCopy.size()); ++x) { + if (keysCopy[x] == ""){ container->numberValues[x] = 0; } else { try { // why *yes* it did occur to me that I made a mistake when I designed this - int temp = std::stoi(container->keysCopy[x]); + int temp = std::stoi(keysCopy[x]); container->numberValues[x] = temp; } catch(...){ @@ -1542,7 +1542,7 @@ void VariableDialogModel::swapKeyAndValues(int index) container->keys = container->stringValues; container->stringKeys = true; - for (int x = 0; x < static_cast(container->keysCopy.size()); ++x){ + for (int x = 0; x < static_cast(keysCopy.size()); ++x){ // This *is* likely to happen as these sizes were not in sync. if (x >= static_cast(container->numberValues.size())){ container->numberValues.emplace_back(); @@ -1550,7 +1550,7 @@ void VariableDialogModel::swapKeyAndValues(int index) try { // why *yes* it did occur to me that I made a mistake when I designed this - int temp = std::stoi(container->keysCopy[x]); + int temp = std::stoi(keysCopy[x]); container->numberValues[x] = temp; } catch(...){ diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 92fceba010b..2bc4709dd01 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -137,7 +137,7 @@ class VariableDialogModel : public AbstractDialogModel { int _listTextMode = 0; int _mapTextMode = 0; - static int _deleteWarningCount = 0; + int _deleteWarningCount; static SCP_string clampIntegerString(SCP_string source); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 384a21288f7..f78b5a34548 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -276,7 +276,7 @@ void VariableDialog::onVariablesTableUpdated() // so if the user just removed the name, mark it as deleted *before changing the name* if (_currentVariable != "" && !strlen(item->text().toStdString().c_str())) { - if (!_model->removeVariable(item->row())) { + if (!_model->removeVariable(item->row(), true)) { // marking a variable as deleted failed, resync UI apply = true; } else { @@ -512,14 +512,13 @@ void VariableDialog::onDeleteVariableButtonPressed() } // Because of the text update we'll need, this needs an applyModel, whether it fails or not. - if (ui->deleteVariableButton->item(currentRow, 2) && ui->deleteVariableButton->item(currentRow, 2)->text().toStdString() == "Flagged For Deletion"){ + if (ui->variablesTable->item(currentRow, 2) && ui->variablesTable->item(currentRow, 2)->text().toStdString() == "Flagged For Deletion"){ _model->removeVariable(currentRow, false); applyModel(); } else { _model->removeVariable(currentRow, true); applyModel(); } - } void VariableDialog::onSetVariableAsStringRadioSelected() @@ -686,8 +685,14 @@ void VariableDialog::onDeleteContainerButtonPressed() return; } - _model->removeContainer(row); - applyModel(); + // Because of the text update we'll need, this needs an applyModel, whether it fails or not. + if (ui->containersTable->item(row, 2) && ui->containersTable->item(row, 2)->text().toStdString() == "Flagged For Deletion"){ + _model->removeVariable(row, false); + applyModel(); + } else { + _model->removeVariable(row, true); + applyModel(); + } } void VariableDialog::onSetContainerAsMapRadioSelected() @@ -1072,7 +1077,7 @@ void VariableDialog::applyModel() } // do we need to switch the delete button to a restore button? - if (ui->containersTable->item(row, 2) && ui->containersTable->item(row, 2)->text().toStdString() == "Flagged for Deletion"){ + if (selectedRow > -1 && ui->containersTable->item(selectedRow, 2) && ui->containersTable->item(selectedRow, 2)->text().toStdString() == "Flagged for Deletion") { ui->deleteContainerButton->setText("Restore"); } else { ui->deleteContainerButton->setText("Delete"); @@ -1134,8 +1139,8 @@ void VariableDialog::updateVariableOptions() ui->saveVariableOnMissionCloseRadio->setEnabled(false); ui->setVariableAsEternalcheckbox->setEnabled(false); ui->networkVariableCheckbox->setEnabled(false); - ui->onShiftItemUpButton->setEnabled(false); - ui->onShiftItemDownButton->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + ui->shiftItemDownButton->setEnabled(false); return; } @@ -1208,8 +1213,8 @@ void VariableDialog::updateContainerOptions() ui->setContainerAsMapRadio->setEnabled(false); ui->setContainerAsListRadio->setEnabled(false); ui->networkContainerCheckbox->setEnabled(false); - ui->onShiftItemUpButton->setEnabled(false); - ui->onShiftItemDownButton->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + ui->shiftItemDownButton->setEnabled(false); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); @@ -1360,11 +1365,11 @@ void VariableDialog::updateContainerDataOptions(bool list) // more than one item and not already at the top of the list. if (x > 0 && x < static_cast(strings.size())){ - ui->onShiftItemUpButton->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); } - if (x > -1 && x < static_Cast(strings.size()) - 1){ - ui->onShiftItemDownButton->setEnabled(false); + if (x > -1 && x < static_cast(strings.size()) - 1){ + ui->shiftItemDownButton->setEnabled(false); } } @@ -1447,8 +1452,8 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); // Enable shift up and down buttons are off in Map mode. - ui->onShiftItemUpButton->setEnabled(false); - ui->onShiftItemDownButton->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + ui->shiftItemDownButton->setEnabled(false); // keys I didn't bother to make separate. Should have done the same with values. auto& keys = _model->getMapKeys(row); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index ffea1219e2a..4b2d503fb4b 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -7,19 +7,19 @@ 0 0 899 - 729 + 711 899 - 729 + 711 899 - 729 + 711 @@ -214,7 +214,7 @@ 0 230 881 - 451 + 431 @@ -244,7 +244,7 @@ 10 30 471 - 151 + 171 @@ -371,8 +371,8 @@ 650 - 210 - 181 + 30 + 191 191 @@ -384,7 +384,7 @@ 10 20 - 171 + 181 171 @@ -431,8 +431,8 @@ 650 - 20 - 181 + 230 + 191 181 @@ -444,7 +444,7 @@ 10 20 - 161 + 181 152 From d7c4c544e69f52d9ab1f66d05a0e080440a96cae Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 3 May 2024 23:54:07 -0400 Subject: [PATCH 170/466] Bug fixes according to testing --- .../mission/dialogs/VariableDialogModel.cpp | 35 +++++-------------- qtfred/src/ui/dialogs/VariableDialog.cpp | 25 +++++++------ 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index e9465b9f16c..3a9abc8c9ea 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -311,7 +311,6 @@ bool VariableDialogModel::setVariableType(int index, bool string) return !string; } - // Here we change the variable type! // this variable is currently a string if (variable->string) { @@ -320,26 +319,18 @@ bool VariableDialogModel::setVariableType(int index, bool string) variable->string = string; return variable->string; } else { - SCP_string question; - sprintf(question, "Changing variable %s to number variable type will make its string value irrelevant. Continue?", variable->name.c_str()); - SCP_string info; - sprintf(info, "If the string cleanly converts to an integer and a number has not previously been set for this variable, the converted number value will be retained."); - - // if this was a misclick, let the user say so - if (!confirmAction(question, info)) { - return variable->string; - } - // if there was no previous number value if (variable->numberValue == 0){ try { variable->numberValue = std::stoi(variable->stringValue); } - // nothing to do here, because that just means we can't convert. + // nothing to do here, because that just means we can't convert and we have to use the old value. catch (...) {} + } - return string; + variable->string = string; + return variable->string; } // this variable is currently a number @@ -349,22 +340,13 @@ bool VariableDialogModel::setVariableType(int index, bool string) variable->string = string; return variable->string; } else { - SCP_string question; - sprintf(question, "Changing variable %s to a string variable type will make the number value irrelevant. Continue?", variable->name.c_str()); - SCP_string info; - sprintf(info, "If no string value has been previously set for this variable, then the number value specified will be set as the default string value."); - - // if this was a misclick, let the user say so - if (!confirmAction(question, info)) { - return variable->string; - } - // if there was no previous string value if (variable->stringValue == ""){ sprintf(variable->stringValue, "%i", variable->numberValue); } - return string; + variable->string = string; + return variable->string; } } } @@ -735,13 +717,14 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) if (container->stringKeys) { // Ok, this is the complicated type. First check if all keys can just quickly be transferred to numbers. bool quickConvert = true; - + int test; for (auto& key : container->keys) { try { - std::stoi(key); + test = std::stoi(key); } catch (...) { quickConvert = false; + nprintf(("Cyborg", "This is Cyborg. Long story short, I don't need this variable, but c++ thinks I do. Last good number on conversion was: %i\n", test)); } } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index f78b5a34548..55749586792 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -571,7 +571,7 @@ void VariableDialog::onDoNotSaveVariableRadioSelected() if (ret != 0){ applyModel(); } else { - ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCompletedRadio->setChecked(false); ui->saveVariableOnMissionCloseRadio->setChecked(false); } } @@ -611,7 +611,7 @@ void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() applyModel(); } else { ui->doNotSaveVariableRadio->setChecked(false); - ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCompletedRadio->setChecked(false); } } @@ -697,6 +697,8 @@ void VariableDialog::onDeleteContainerButtonPressed() void VariableDialog::onSetContainerAsMapRadioSelected() { + // to avoid visual weirdness, make it false. + ui->setContainerAsListRadio->setChecked(false); int row = getCurrentContainerRow(); if (row < 0){ @@ -709,6 +711,8 @@ void VariableDialog::onSetContainerAsMapRadioSelected() void VariableDialog::onSetContainerAsListRadioSelected() { + // to avoid visual weirdness, make it false. + ui->setContainerAsMapRadio->setChecked(false); int row = getCurrentContainerRow(); if (row < 0){ @@ -785,7 +789,8 @@ void VariableDialog::onDoNotSaveContainerRadioSelected() ui->saveContainerOnMissionCompletedRadio->setChecked(false); } } -void VariableDialog::onSaveContainerOnMissionCloseRadioSelected() + +void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() { int row = getCurrentContainerRow(); @@ -793,16 +798,16 @@ void VariableDialog::onSaveContainerOnMissionCloseRadioSelected() return; } - if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 2) != 2) + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 1) != 1) applyModel(); else { ui->doNotSaveContainerRadio->setChecked(false); - ui->saveContainerOnMissionCloseRadio->setChecked(true); - ui->saveContainerOnMissionCompletedRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(true); } } -void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() +void VariableDialog::onSaveContainerOnMissionCloseRadioSelected() { int row = getCurrentContainerRow(); @@ -810,12 +815,12 @@ void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() return; } - if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 1) != 1) + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 2) != 2) applyModel(); else { ui->doNotSaveContainerRadio->setChecked(false); - ui->saveContainerOnMissionCloseRadio->setChecked(false); - ui->saveContainerOnMissionCompletedRadio->setChecked(true); + ui->saveContainerOnMissionCloseRadio->setChecked(true); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); } } From c9e11d465771157907df818131dfd6661f520e4e Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 4 May 2024 00:35:46 -0400 Subject: [PATCH 171/466] Fixes Based On Testing --- qtfred/src/ui/dialogs/VariableDialog.cpp | 110 ++++++++++++++--------- qtfred/ui/VariableDialog.ui | 15 +++- 2 files changed, 81 insertions(+), 44 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 55749586792..5861e46d444 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -212,25 +212,25 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->variablesTable->setColumnWidth(0, 95); - ui->variablesTable->setColumnWidth(1, 95); - ui->variablesTable->setColumnWidth(2, 105); + ui->variablesTable->setColumnWidth(0, 175); + ui->variablesTable->setColumnWidth(1, 175); + ui->variablesTable->setColumnWidth(2, 140); ui->containersTable->setColumnCount(3); ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->containersTable->setColumnWidth(0, 95); - ui->containersTable->setColumnWidth(1, 95); - ui->containersTable->setColumnWidth(2, 105); + ui->containersTable->setColumnWidth(0, 175); + ui->containersTable->setColumnWidth(1, 175); + ui->containersTable->setColumnWidth(2, 140); ui->containerContentsTable->setColumnCount(2); // Default to list ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setColumnWidth(0, 150); - ui->containerContentsTable->setColumnWidth(1, 150); + ui->containerContentsTable->setColumnWidth(0, 240); + ui->containerContentsTable->setColumnWidth(1, 240); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't // and I don't mind just manually toggling them. @@ -258,7 +258,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) } // TODO! make sure that when a variable is added that the whole model is reloaded. -// TODO! Fix me. This function does not work as intended because it must process both, not just one. +// TODO! Fix me. This function does not work as intended because it must process both varaible and contents, not just one. void VariableDialog::onVariablesTableUpdated() { if (_applyingModel){ @@ -346,6 +346,7 @@ void VariableDialog::onVariablesTableUpdated() void VariableDialog::onVariablesSelectionChanged() { if (_applyingModel){ + applyModel(); return; } @@ -381,6 +382,7 @@ void VariableDialog::onVariablesSelectionChanged() void VariableDialog::onContainersTableUpdated() { if (_applyingModel){ + applyModel(); return; } @@ -388,6 +390,7 @@ void VariableDialog::onContainersTableUpdated() // just in case something is goofy, return if (row < 0){ + applyModel(); return; } @@ -415,6 +418,7 @@ void VariableDialog::onContainersTableUpdated() void VariableDialog::onContainersSelectionChanged() { if (_applyingModel){ + applyModel(); return; } @@ -426,19 +430,15 @@ void VariableDialog::onContainersSelectionChanged() } // guaranteed not to be null, since getCurrentContainerRow already checked. - SCP_string newContainerName = ui->containersTable->item(row, 0)->text().toStdString(); - - if (newContainerName != _currentContainer){ - _currentContainer = newContainerName; - } - - applyModel(); // Seems to be buggy unless I have this outside the if. + _currentContainer = ui->containersTable->item(row, 0)->text().toStdString(); + applyModel(); } // TODO, finish this function void VariableDialog::onContainerContentsTableUpdated() { if (_applyingModel){ + applyModel(); return; } @@ -448,12 +448,14 @@ void VariableDialog::onContainerContentsTableUpdated() void VariableDialog::onContainerContentsSelectionChanged() { if (_applyingModel){ + applyModel(); return; } int row = getCurrentContainerItemRow(); if (row < 0){ + applyModel(); return; } @@ -461,6 +463,7 @@ void VariableDialog::onContainerContentsSelectionChanged() SCP_string newContainerItemName; if (!item){ + applyModel(); return; } @@ -485,12 +488,14 @@ void VariableDialog::onAddVariableButtonPressed() void VariableDialog::onCopyVariableButtonPressed() { if (_currentVariable.empty()){ + applyModel(); return; } int currentRow = getCurrentVariableRow(); if (currentRow < 0){ + applyModel(); return; } @@ -502,12 +507,14 @@ void VariableDialog::onCopyVariableButtonPressed() void VariableDialog::onDeleteVariableButtonPressed() { if (_currentVariable.empty()){ + applyModel(); return; } int currentRow = getCurrentVariableRow(); if (currentRow < 0){ + applyModel(); return; } @@ -526,17 +533,14 @@ void VariableDialog::onSetVariableAsStringRadioSelected() int currentRow = getCurrentVariableRow(); if (currentRow < 0){ + applyModel(); return; } // this doesn't return succeed or fail directly, // but if it doesn't return true then it failed since this is the string radio - if(!_model->setVariableType(currentRow, true)){ - applyModel(); - } else { - ui->setVariableAsStringRadio->setChecked(true); - ui->setVariableAsNumberRadio->setChecked(false); - } + _model->setVariableType(currentRow, true); + applyModel(); } void VariableDialog::onSetVariableAsNumberRadioSelected() @@ -544,25 +548,22 @@ void VariableDialog::onSetVariableAsNumberRadioSelected() int currentRow = getCurrentVariableRow(); if (currentRow < 0){ + applyModel(); return; } // this doesn't return succeed or fail directly, // but if it doesn't return false then it failed since this is the number radio - if (!_model->setVariableType(currentRow, false)) { - applyModel(); - } - else { - ui->setVariableAsStringRadio->setChecked(false); - ui->setVariableAsNumberRadio->setChecked(true); - } + _model->setVariableType(currentRow, false); + applyModel(); } void VariableDialog::onDoNotSaveVariableRadioSelected() { int currentRow = getCurrentVariableRow(); - if (currentRow < 0){ + if (currentRow < 0 || !ui->doNotSaveVariableRadio->isChecked()){ + applyModel(); return; } @@ -576,13 +577,12 @@ void VariableDialog::onDoNotSaveVariableRadioSelected() } } - - void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() { int row = getCurrentVariableRow(); - if (row < 0){ + if (row < 0 || !ui->saveVariableOnMissionCompletedRadio->isChecked()){ + applyModel(); return; } @@ -600,7 +600,8 @@ void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() { int row = getCurrentVariableRow(); - if (row < 0){ + if (row < 0 || !ui->saveVariableOnMissionCloseRadio->isChecked()){ + applyModel(); return; } @@ -645,7 +646,6 @@ void VariableDialog::onNetworkVariableCheckboxClicked() } else { ui->networkVariableCheckbox->setChecked(!ui->networkVariableCheckbox->isChecked()); } - } void VariableDialog::onAddContainerButtonPressed() @@ -682,17 +682,18 @@ void VariableDialog::onDeleteContainerButtonPressed() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } // Because of the text update we'll need, this needs an applyModel, whether it fails or not. if (ui->containersTable->item(row, 2) && ui->containersTable->item(row, 2)->text().toStdString() == "Flagged For Deletion"){ _model->removeVariable(row, false); - applyModel(); } else { _model->removeVariable(row, true); - applyModel(); } + + applyModel(); } void VariableDialog::onSetContainerAsMapRadioSelected() @@ -702,6 +703,7 @@ void VariableDialog::onSetContainerAsMapRadioSelected() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -716,6 +718,7 @@ void VariableDialog::onSetContainerAsListRadioSelected() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -729,6 +732,7 @@ void VariableDialog::onSetContainerAsStringRadioSelected() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -741,6 +745,7 @@ void VariableDialog::onSetContainerAsNumberRadioSelected() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -753,6 +758,7 @@ void VariableDialog::onSetContainerKeyAsStringRadioSelected() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -766,6 +772,7 @@ void VariableDialog::onSetContainerKeyAsNumberRadioSelected() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -778,6 +785,7 @@ void VariableDialog::onDoNotSaveContainerRadioSelected() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -795,6 +803,7 @@ void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -812,6 +821,7 @@ void VariableDialog::onSaveContainerOnMissionCloseRadioSelected() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -829,6 +839,7 @@ void VariableDialog::onNetworkContainerCheckboxClicked() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -842,6 +853,7 @@ void VariableDialog::onSetContainerAsEternalCheckboxClicked() int row = getCurrentContainerRow(); if (row < 0){ + applyModel(); return; } @@ -855,6 +867,7 @@ void VariableDialog::onAddContainerItemButtonPressed() int containerRow = getCurrentContainerRow(); if (containerRow < 0){ + applyModel(); return; } @@ -872,12 +885,14 @@ void VariableDialog::onCopyContainerItemButtonPressed() int containerRow = getCurrentContainerRow(); if (containerRow < 0){ + applyModel(); return; } int itemRow = getCurrentContainerItemRow(); if (itemRow < 0){ + applyModel(); return; } @@ -895,12 +910,14 @@ void VariableDialog::onDeleteContainerItemButtonPressed() int containerRow = getCurrentContainerRow(); if (containerRow < 0){ + applyModel(); return; } int itemRow = getCurrentContainerItemRow(); if (itemRow < 0){ + applyModel(); return; } @@ -918,12 +935,14 @@ void VariableDialog::onShiftItemUpButtonPressed() int containerRow = getCurrentContainerRow(); if (containerRow < 0){ + applyModel(); return; } int itemRow = getCurrentContainerItemRow(); if (itemRow < 0){ + applyModel(); return; } @@ -936,12 +955,14 @@ void VariableDialog::onShiftItemDownButtonPressed() int containerRow = getCurrentContainerRow(); if (containerRow < 0){ + applyModel(); return; } int itemRow = getCurrentContainerItemRow(); if (itemRow < 0){ + applyModel(); return; } @@ -954,6 +975,7 @@ void VariableDialog::onSwapKeysAndValuesButtonPressed() int containerRow = getCurrentContainerRow(); if (containerRow < 0){ + applyModel(); return; } @@ -966,7 +988,10 @@ VariableDialog::~VariableDialog(){}; // NOLINT void VariableDialog::applyModel() { - // TODO! We need an undelete action. Best way is to change the text on the button if the notes say "Deleted" + if (_applyingModel) { + return; + } + _applyingModel = true; auto variables = _model->getVariableValues(); @@ -986,7 +1011,7 @@ void VariableDialog::applyModel() // check if this is the current variable. This keeps us selecting the correct variable even when // there's a deletion. - if (!_currentVariable.empty() && variables[x][0] == _currentVariable){ + if (selectedRow < 0 && !_currentVariable.empty() && variables[x][0] == _currentVariable){ selectedRow = x; } @@ -1062,7 +1087,7 @@ void VariableDialog::applyModel() } // check if this is the current variable. - if (!_currentVariable.empty() && containers[x][0] == _currentVariable){ + if (selectedRow < 0 && !_currentContainer.empty() && containers[x][0] == _currentContainer){ selectedRow = x; } @@ -1119,7 +1144,6 @@ void VariableDialog::applyModel() _currentContainer = ui->containersTable->item(0, 0)->text().toStdString(); ui->containersTable->clearSelection(); ui->containersTable->item(0, 0)->setSelected(true); - } } @@ -1220,6 +1244,7 @@ void VariableDialog::updateContainerOptions() ui->networkContainerCheckbox->setEnabled(false); ui->shiftItemUpButton->setEnabled(false); ui->shiftItemDownButton->setEnabled(false); + ui->swapKeysAndValuesButton->setEnabled(false); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); @@ -1333,6 +1358,7 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setRowCount(0); ui->shiftItemDownButton->setEnabled(false); ui->shiftItemUpButton->setEnabled(false); + ui->swapKeysAndValuesButton->setEnabled(false); return; @@ -1347,6 +1373,7 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->shiftItemUpButton->setEnabled(true); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); + ui->swapKeysAndValuesButton->setEnabled(false); // with string contents @@ -1459,6 +1486,7 @@ void VariableDialog::updateContainerDataOptions(bool list) // Enable shift up and down buttons are off in Map mode. ui->shiftItemUpButton->setEnabled(false); ui->shiftItemDownButton->setEnabled(false); + ui->swapKeysAndValuesButton->setEnabled(true); // keys I didn't bother to make separate. Should have done the same with values. auto& keys = _model->getMapKeys(row); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 4b2d503fb4b..8a9bf4783b4 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -87,6 +87,9 @@ 191 + + Qt::ScrollBarAlwaysOn + Qt::ScrollBarAlwaysOff @@ -94,7 +97,7 @@ QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed - true + false QAbstractItemView::SingleSelection @@ -247,11 +250,14 @@ 171 + + Qt::ScrollBarAlwaysOn + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed - true + false QAbstractItemView::SingleSelection @@ -536,6 +542,9 @@ 171 + + Qt::ScrollBarAlwaysOn + Qt::ScrollBarAlwaysOff @@ -543,7 +552,7 @@ QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed - true + false QAbstractItemView::SingleSelection From e1b9832faff2cf0e260cdeec042f18ea69908292 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 4 May 2024 00:51:39 -0400 Subject: [PATCH 172/466] Fixes Based on Testing --- qtfred/src/ui/dialogs/VariableDialog.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 5861e46d444..135940aaae0 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -212,16 +212,16 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->variablesTable->setColumnWidth(0, 175); - ui->variablesTable->setColumnWidth(1, 175); + ui->variablesTable->setColumnWidth(0, 180); + ui->variablesTable->setColumnWidth(1, 180); ui->variablesTable->setColumnWidth(2, 140); ui->containersTable->setColumnCount(3); ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->containersTable->setColumnWidth(0, 175); - ui->containersTable->setColumnWidth(1, 175); + ui->containersTable->setColumnWidth(0, 180); + ui->containersTable->setColumnWidth(1, 180); ui->containersTable->setColumnWidth(2, 140); ui->containerContentsTable->setColumnCount(2); @@ -229,8 +229,8 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) // Default to list ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setColumnWidth(0, 240); - ui->containerContentsTable->setColumnWidth(1, 240); + ui->containerContentsTable->setColumnWidth(0, 237); + ui->containerContentsTable->setColumnWidth(1, 237); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't // and I don't mind just manually toggling them. @@ -519,7 +519,7 @@ void VariableDialog::onDeleteVariableButtonPressed() } // Because of the text update we'll need, this needs an applyModel, whether it fails or not. - if (ui->variablesTable->item(currentRow, 2) && ui->variablesTable->item(currentRow, 2)->text().toStdString() == "Flagged For Deletion"){ + if (ui->deleteVariableButton->text().toStdString() == "Restore") { _model->removeVariable(currentRow, false); applyModel(); } else { @@ -688,9 +688,9 @@ void VariableDialog::onDeleteContainerButtonPressed() // Because of the text update we'll need, this needs an applyModel, whether it fails or not. if (ui->containersTable->item(row, 2) && ui->containersTable->item(row, 2)->text().toStdString() == "Flagged For Deletion"){ - _model->removeVariable(row, false); + _model->removeContainer(row, false); } else { - _model->removeVariable(row, true); + _model->removeContainer(row, true); } applyModel(); From 98e78ec086be62b5cbc472c705c07f3f65f18df0 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 4 May 2024 01:03:34 -0400 Subject: [PATCH 173/466] Fix Containers Not Restoring Deleted Items And Change Deletion Text for size savings --- qtfred/src/ui/dialogs/VariableDialog.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 135940aaae0..b5b75b92da1 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -229,8 +229,8 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) // Default to list ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setColumnWidth(0, 237); - ui->containerContentsTable->setColumnWidth(1, 237); + ui->containerContentsTable->setColumnWidth(0, 230); + ui->containerContentsTable->setColumnWidth(1, 230); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't // and I don't mind just manually toggling them. @@ -687,7 +687,7 @@ void VariableDialog::onDeleteContainerButtonPressed() } // Because of the text update we'll need, this needs an applyModel, whether it fails or not. - if (ui->containersTable->item(row, 2) && ui->containersTable->item(row, 2)->text().toStdString() == "Flagged For Deletion"){ + if (ui->deleteContainerButton->text().toStdString() == "Restore"){ _model->removeContainer(row, false); } else { _model->removeContainer(row, true); @@ -1107,7 +1107,7 @@ void VariableDialog::applyModel() } // do we need to switch the delete button to a restore button? - if (selectedRow > -1 && ui->containersTable->item(selectedRow, 2) && ui->containersTable->item(selectedRow, 2)->text().toStdString() == "Flagged for Deletion") { + if (selectedRow > -1 && ui->containersTable->item(selectedRow, 2) && ui->containersTable->item(selectedRow, 2)->text().toStdString() == "Deleted") { ui->deleteContainerButton->setText("Restore"); } else { ui->deleteContainerButton->setText("Delete"); @@ -1196,7 +1196,7 @@ void VariableDialog::updateVariableOptions() ui->setVariableAsNumberRadio->setChecked(!string); // do we need to switch the delete button to a restore button? - if (ui->variablesTable->item(row, 2) && ui->variablesTable->item(row, 2)->text().toStdString() == "Flagged for Deletion"){ + if (ui->variablesTable->item(row, 2) && ui->variablesTable->item(row, 2)->text().toStdString() == "Deleted"){ ui->deleteVariableButton->setText("Restore"); } else { ui->deleteVariableButton->setText("Delete"); From 8dde6d47cbdad1392c9e7e8b899a77eef7d913ed Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 4 May 2024 01:19:42 -0400 Subject: [PATCH 174/466] More spacing adjustments and text changes --- .../src/mission/dialogs/VariableDialogModel.cpp | 4 ++-- qtfred/src/ui/dialogs/VariableDialog.cpp | 16 ++++++++-------- qtfred/ui/VariableDialog.ui | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 3a9abc8c9ea..6813afbd926 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1660,7 +1660,7 @@ const SCP_vector> VariableDialogModel::getVariableValu SCP_string notes = ""; if (item.deleted) { - notes = "Flagged for Deletion"; + notes = "Deleted"; } else if (item.originalName == "") { notes = "New"; } else if (item.name != item.originalName){ @@ -1813,7 +1813,7 @@ const SCP_vector> VariableDialogModel::getContainerNam if (item.deleted) { - notes = "Flagged for Deletion"; + notes = "Deleted"; } else if (item.originalName == "") { notes = "New"; } else if (item.name != item.originalName){ diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index b5b75b92da1..fccd2b3e9c3 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -212,25 +212,25 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->variablesTable->setColumnWidth(0, 180); - ui->variablesTable->setColumnWidth(1, 180); - ui->variablesTable->setColumnWidth(2, 140); + ui->variablesTable->setColumnWidth(0, 190); + ui->variablesTable->setColumnWidth(1, 190); + ui->variablesTable->setColumnWidth(2, 120); ui->containersTable->setColumnCount(3); ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->containersTable->setColumnWidth(0, 180); - ui->containersTable->setColumnWidth(1, 180); - ui->containersTable->setColumnWidth(2, 140); + ui->containersTable->setColumnWidth(0, 190); + ui->containersTable->setColumnWidth(1, 190); + ui->containersTable->setColumnWidth(2, 120); ui->containerContentsTable->setColumnCount(2); // Default to list ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setColumnWidth(0, 230); - ui->containerContentsTable->setColumnWidth(1, 230); + ui->containerContentsTable->setColumnWidth(0, 225); + ui->containerContentsTable->setColumnWidth(1, 225); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't // and I don't mind just manually toggling them. diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 8a9bf4783b4..d9e10475e65 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -377,7 +377,7 @@ 650 - 30 + 20 191 191 @@ -437,9 +437,9 @@ 650 - 230 + 220 191 - 181 + 191 From 3120d6dda143235b8478b3a652eac85f645f8455 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 4 May 2024 16:57:30 -0400 Subject: [PATCH 175/466] Fix the Addition of variables by typing And start adding container Items by typing. --- .../mission/dialogs/VariableDialogModel.cpp | 28 ++++ .../src/mission/dialogs/VariableDialogModel.h | 2 + qtfred/src/ui/dialogs/VariableDialog.cpp | 130 +++++++++++++++--- qtfred/ui/VariableDialog.ui | 47 ++++++- 4 files changed, 180 insertions(+), 27 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 6813afbd926..9e881ef72d6 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1123,6 +1123,34 @@ SCP_string VariableDialogModel::addListItem(int index) } } +SCP_string VariableDialogModel::addListItem(int index, SCP_string item) +{ + auto container = lookupContainer(index); + + if (!container){ + return ""; + } + + if (container->string) { + container->stringValues.push_back(item); + return container->stringValues.back(); + } else { + auto temp = trimIntegerString(item); + + try { + int tempNumber = std::stoi(temp); + container->numberValues.push_back(tempNumber); + } + catch(...){ + container->numberValues.push_back(0); + return "0"; + } + + sprintf(temp, "%i", container->numberValues.back()); + return temp; + } +} + std::pair VariableDialogModel::addMapItem(int index) { auto container = lookupContainer(index); diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 2bc4709dd01..a1b0eddeba3 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -100,10 +100,12 @@ class VariableDialogModel : public AbstractDialogModel { bool removeContainer(int index, bool toDelete); SCP_string addListItem(int index); + SCP_string addListItem(int index, SCP_string item); SCP_string copyListItem(int containerIndex, int index); bool removeListItem(int containerindex, int index); std::pair addMapItem(int index); + std::pair addMapItem(int index, SCP_string key, SCP_string value); std::pair copyMapItem(int index, int itemIndex); bool removeMapItem(int index, int rowIndex); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index fccd2b3e9c3..6a2f7a32b73 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -272,19 +272,56 @@ void VariableDialog::onVariablesTableUpdated() } auto item = ui->variablesTable->item(currentRow, 0); + SCP_string itemText = item->text().toStdString(); bool apply = false; - // so if the user just removed the name, mark it as deleted *before changing the name* - if (_currentVariable != "" && !strlen(item->text().toStdString().c_str())) { - if (!_model->removeVariable(item->row(), true)) { + // This will only be true if the user is trying to add a new variable. + if (currentRow == ui->variablesTable->rowCount() - 1) { + + // make sure the item exists before we dereference + if (ui->variablesTable->item(currentRow, 0)) { + + // Add the new container. + if (!itemText.empty() && itemText != "Add Variable ...") { + _model->addNewVariable(itemText); + _currentVariable = itemText; + _currentVariableData = ""; + applyModel(); + } + + } else { + // reapply the model if the item is null. + applyModel(); + } + + // we're done here because we cannot edit the data column on the add variable row + return; + } + + // so if the user just removed the name, mark it as deleted + if (itemText.empty() && !_currentVariable.empty()) { + if (!_model->removeVariable(item->row(), true) ) { // marking a variable as deleted failed, resync UI apply = true; } else { + // now that we know that the variable was deleted updateVariableOptions(); } - } else { - - auto ret = _model->changeVariableName(item->row(), item->text().toStdString()); + + // if the user is restoring a deleted variable by inserting a name.... + } else if (!itemText.empty() && _currentVariable.empty()){ + + if (!_model->removeVariable(item->row(), true) ) { + // marking a variable as deleted failed, resync UI + apply = true; + } else { + // now that we know that the variable was deleted + updateVariableOptions(); + } + + } else if (itemText != _currentVariable){ + + auto ret = _model->changeVariableName(item->row(), itemText); // we put something in the cell, but the model couldn't process it. if (strlen(item->text().toStdString().c_str()) && ret == ""){ @@ -296,19 +333,18 @@ void VariableDialog::onVariablesTableUpdated() item->setText(ret.c_str()); _currentVariable = ret; } - } - // empty return and cell was handled earlier. + }// No action needed if the first cell was not changed. + // now work on the variable data cell item = ui->variablesTable->item(currentRow, 1); + itemText = item->text().toStdString(); // check if data column was altered - // TODO! Set up comparison between last and current value - if (item->column() == 1) { - + if (itemText != _currentVariableData) { // Variable is a string if (_model->getVariableType(item->row())){ - SCP_string temp = item->text().toStdString().c_str(); + SCP_string temp = itemText; temp = temp.substr(0, NAME_LENGTH - 1); SCP_string ret = _model->setVariableStringValue(item->row(), temp); @@ -316,15 +352,14 @@ void VariableDialog::onVariablesTableUpdated() apply = true; } else { item->setText(ret.c_str()); - } + _currentVariableData = ret; + } + + // Variable is a number } else { SCP_string source = item->text().toStdString(); SCP_string temp = _model->trimIntegerString(source); - if (temp != source){ - item->setText(temp.c_str()); - } - try { int ret = _model->setVariableNumberValue(item->row(), std::stoi(temp)); temp = ""; @@ -332,12 +367,16 @@ void VariableDialog::onVariablesTableUpdated() item->setText(temp.c_str()); } catch (...) { - applyModel(); + // that's not good.... + apply = true; } + + // best we can do is to set this to temp, whether conversion fails or not. + _currentContainerItemData = temp; } + } - // if the user somehow edited the info that should only come from the model and should not be editable, reload everything. - } else { + if (apply) { applyModel(); } } @@ -404,6 +443,11 @@ void VariableDialog::onContainersTableUpdated() applyModel(); } } + else { + applyModel(); + } + + return; // are they editing an existing container name? } else if (ui->containersTable->item(row, 0)){ @@ -442,8 +486,52 @@ void VariableDialog::onContainerContentsTableUpdated() return; } + int containerRow = getCurrentContainerRow(); + int row = getCurrentContainerItemRow(); + + // just in case something is goofy, return + if (row < 0 || containerRow < 0){ + applyModel(); + return; + } + + // Are they adding a new item? + if (row == ui->containerContentsTable->rowCount() - 1){ + if (ui->containerContentsTable->item(row, 0)) { + SCP_string newString = ui->containerContentsTable->item(row, 0)->text().toStdString(); + if (!newString.empty() && newString != "Add Item ..."){ + + if (_model->getContainerListOrMap(containerRow)) { + _model->addListItem(containerRow, newString); + + } else { + + _model->addMapItem(containerRow, newString); + } + + _currentContainer = newString; + applyModel(); + } + } + else { + applyModel(); + } + + return; + + // are they editing an existing container name? + } else if (ui->containerContentsTable->item(row, 0)){ + _currentContainer = ui->containersTable->item(row,0)->text().toStdString(); + + if (_currentContainer != _model->changeContainerName(row, ui->containersTable->item(row,0)->text().toStdString())){ + applyModel(); + } + } + + + -} // could be new key or new value +} void VariableDialog::onContainerContentsSelectionChanged() { diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index d9e10475e65..20d19c58e92 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -279,7 +279,7 @@ 520 - 20 + 30 106 175 @@ -379,7 +379,7 @@ 650 20 191 - 191 + 181 @@ -391,7 +391,7 @@ 10 20 181 - 171 + 161 @@ -437,9 +437,9 @@ 650 - 220 + 210 191 - 191 + 221 @@ -451,7 +451,7 @@ 10 20 181 - 152 + 181 @@ -590,6 +590,41 @@ + + variablesTable + addVariableButton + copyVariableButton + deleteVariableButton + setVariableAsStringRadio + setVariableAsNumberRadio + doNotSaveVariableRadio + saveVariableOnMissionCompletedRadio + saveVariableOnMissionCloseRadio + setVariableAsEternalcheckbox + networkVariableCheckbox + containersTable + addContainerButton + copyContainerButton + deleteContainerButton + doNotSaveContainerRadio + saveContainerOnMissionCompletedRadio + saveContainerOnMissionCloseRadio + setContainerAsEternalCheckbox + networkContainerCheckbox + containerContentsTable + addContainerItemButton + copyContainerItemButton + shiftItemUpButton + shiftItemDownButton + swapKeysAndValuesButton + deleteContainerItemButton + setContainerAsListRadio + setContainerAsMapRadio + setContainerAsStringRadio + setContainerAsNumberRadio + setContainerKeyAsStringRadio + setContainerKeyAsNumberRadio + From 03083d1a91bc5f972165261721848615cf65625c Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 4 May 2024 23:25:09 -0400 Subject: [PATCH 176/466] Implement a few missing container item features --- .../mission/dialogs/VariableDialogModel.cpp | 156 +++++++++++++----- .../src/mission/dialogs/VariableDialogModel.h | 7 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 106 ++++++++++-- qtfred/src/ui/dialogs/VariableDialog.h | 4 +- 4 files changed, 213 insertions(+), 60 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 9e881ef72d6..c7ca3506236 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1191,14 +1191,79 @@ std::pair VariableDialogModel::addMapItem(int index) ret.first = newKey; - if (container->string) + + if (container->string){ ret.second = ""; - else + container->stringValues.push_back(""); + } else { ret.second = "0"; + container->numberValues.push_back(0); + } return ret; } +std::pair VariableDialogModel::addMapItem(int index, SCP_string key, SCP_string value) +{ + auto container = lookupContainer(index); + + std::pair ret = { "", "" }; + + // no container available + if (!container) { + return ret; + } + + bool conflict; + int count = 0; + SCP_string newKey; + + if (key.empty()) { + do { + conflict = false; + + if (container->stringKeys){ + sprintf(newKey, "key%i", count); + } else { + sprintf(newKey, "%i", count); + } + + for (int x = 0; x < static_cast(container->keys.size()); ++x) { + if (container->keys[x] == newKey){ + conflict = true; + break; + } + } + + ++count; + } while (conflict && count < 101); + } else { + newKey = key; + } + + if (conflict) { + return ret; + } + + ret.first = newKey; + container->keys.push_back(ret.first); + + if (container->string) { + ret.second = value; + } else { + try { + container->numberValues.push_back(std::stoi(value)); + ret.second = value; + } + catch (...) { + ret.second = "0"; + container->numberValues.push_back(0); + } + } + + return ret; +} + SCP_string VariableDialogModel::copyListItem(int containerIndex, int index) { auto container = lookupContainer(containerIndex); @@ -1217,6 +1282,43 @@ SCP_string VariableDialogModel::copyListItem(int containerIndex, int index) } +SCP_string VariableDialogModel::changeListItem(int containerIndex, int index, SCP_string newString) +{ + auto container = lookupContainer(containerIndex); + + if (!container){ + return ""; + } + + if (container->string){ + auto listItem = lookupContainerStringItem(containerIndex, index); + + if (!listItem){ + return ""; + } + + *listItem = newString; + + } else { + auto listItem = lookupContainerNumberItem(containerIndex, index); + + if (!listItem){ + return ""; + } + + try{ + *listItem = std::stoi(newString); + } + catch(...){ + SCP_string temp; + sprintf(temp, "%i", *listItem); + return temp; + } + } + + return ""; +} + bool VariableDialogModel::removeListItem(int containerIndex, int index) { auto container = lookupContainer(containerIndex); @@ -1452,7 +1554,7 @@ bool VariableDialogModel::removeMapItem(int index, int itemIndex) return true; } -SCP_string VariableDialogModel::replaceMapItemKey(int index, SCP_string oldKey, SCP_string newKey) +SCP_string VariableDialogModel::changeMapItemKey(int index, SCP_string oldKey, SCP_string newKey) { auto container = lookupContainer(index); @@ -1471,27 +1573,17 @@ SCP_string VariableDialogModel::replaceMapItemKey(int index, SCP_string oldKey, return oldKey; } -SCP_string VariableDialogModel::changeMapItemStringValue(int index, SCP_string key, SCP_string newValue) +SCP_string VariableDialogModel::changeMapItemStringValue(int index, int itemIndex, SCP_string newValue) { - auto container = lookupContainer(index); - - if (!container || !container->string){ + auto item = lookupContainerStringItem(index, itemIndex); + + if (!item){ return ""; } - for (int x = 0; x < static_cast(container->keys.size()); ++x){ - if (container->keys[x] == key) { - if (x < static_cast(container->stringValues.size())){ - container->stringValues[x] = newValue; - return newValue; - } else { - return ""; - } - } - } + *item = newValue; - // Failure - return ""; + return *item; } void VariableDialogModel::swapKeyAndValues(int index) @@ -1594,29 +1686,19 @@ void VariableDialogModel::swapKeyAndValues(int index) } } -SCP_string VariableDialogModel::changeMapItemNumberValue(int index, SCP_string key, int newValue) +SCP_string VariableDialogModel::changeMapItemNumberValue(int index, int itemIndex, int newValue) { - auto container = lookupContainer(index); + auto mapItem = lookupContainerNumberItem(index, itemIndex); - if (!container || !container->string){ + if (!mapItem){ return ""; } - for (int x = 0; x < static_cast(container->keys.size()); ++x){ - if (container->keys[x] == key) { - if (x < static_cast(container->numberValues.size())){ - container->numberValues[x] = newValue; - SCP_string returnValue; - sprintf(returnValue, "%i", newValue); - return returnValue; - } else { - return ""; - } - } - } - - // Failure - return ""; + *mapItem = newValue; + + SCP_string ret; + sprintf(ret, "%i", newValue); + return ret; } diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index a1b0eddeba3..f10d73cd01b 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -107,14 +107,15 @@ class VariableDialogModel : public AbstractDialogModel { std::pair addMapItem(int index); std::pair addMapItem(int index, SCP_string key, SCP_string value); std::pair copyMapItem(int index, int itemIndex); + SCP_string changeListItem(int containerIndex, int index, SCP_string newString); bool removeMapItem(int index, int rowIndex); void shiftListItemUp(int containerIndex, int itemIndex); void shiftListItemDown(int containerIndex, int itemIndex); - SCP_string replaceMapItemKey(int index, SCP_string oldKey, SCP_string newKey); - SCP_string changeMapItemStringValue(int index, SCP_string key, SCP_string newValue); - SCP_string changeMapItemNumberValue(int index, SCP_string key, int newValue); + SCP_string changeMapItemKey(int index, SCP_string oldKey, SCP_string newKey); + SCP_string changeMapItemStringValue(int index, int itemIndex, SCP_string newValue); + SCP_string changeMapItemNumberValue(int index, int itemIndex, int newValue); const SCP_vector& getMapKeys(int index); const SCP_vector& getStringValues(int index); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 6a2f7a32b73..de7d571f464 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -372,7 +372,7 @@ void VariableDialog::onVariablesTableUpdated() } // best we can do is to set this to temp, whether conversion fails or not. - _currentContainerItemData = temp; + _currentContainerItemCol2 = temp; } } @@ -497,8 +497,13 @@ void VariableDialog::onContainerContentsTableUpdated() // Are they adding a new item? if (row == ui->containerContentsTable->rowCount() - 1){ + + bool newItemCreated = false; + SCP_string newString; + if (ui->containerContentsTable->item(row, 0)) { - SCP_string newString = ui->containerContentsTable->item(row, 0)->text().toStdString(); + newString = ui->containerContentsTable->item(row, 0)->text().toStdString(); + if (!newString.empty() && newString != "Add Item ..."){ if (_model->getContainerListOrMap(containerRow)) { @@ -506,31 +511,94 @@ void VariableDialog::onContainerContentsTableUpdated() } else { - _model->addMapItem(containerRow, newString); + _model->addMapItem(containerRow, newString, ""); } _currentContainer = newString; applyModel(); + return; } - } - else { + + } + + if (!ui->containerContentsTable->item(row, 1)) { + // At this point there's nothing else we can do and something may be off, anyway. applyModel(); + return; } + // if we got here, we know that the second cell is valid. + newString = ui->containerContentsTable->item(row, 0)->text().toStdString(); + + // But we can only create a new map item here. Ignore if this container is a list. + if (!newString.empty() && newString.substr(0, 10) != "Add Item ..."){ + if (!_model->getContainerListOrMap(containerRow)) { + auto ret = _model->addMapItem(containerRow, "", newString); + _currentContainerItemCol1 = ret.first; + _currentContainerItemCol2 = ret.second; + } + } + + // nothing else to determine at this point. + applyModel(); return; - // are they editing an existing container name? + // are they editing an existing container item column 1? } else if (ui->containerContentsTable->item(row, 0)){ - _currentContainer = ui->containersTable->item(row,0)->text().toStdString(); + SCP_string newText = ui->containerContentsTable->item(row, 0)->text().toStdString(); + + if (_model->getContainerListOrMap(containerRow)){ - if (_currentContainer != _model->changeContainerName(row, ui->containersTable->item(row,0)->text().toStdString())){ - applyModel(); - } - } + if (newText != _currentContainerItemCol1){ + // Trim the string if necessary + if (!_model->getContainerValueType(containerRow)){ + newText = _model->trimIntegerString(newText); + } + + // Finally change the list item + _currentContainerItemCol1 = _model->changeListItem(containerRow, row, newText); + return; + } + + } else if (newText != _currentContainerItemCol1){ + + if (!_model->getContainerKeyType(containerRow)){ + + if (!_model->getContainerKeyType(containerRow)){ + newText = _model->trimIntegerString(newText); + } + + // TODO! Write a key change function so you can put something here. + } + return; + } + } + + // if we're here, nothing has changed so far. So let's attempt column 2 + if (ui->containerContentsTable->item(row, 1) && !_model->getContainerListOrMap(containerRow)){ + + SCP_string newText = ui->containerContentsTable->item(row, 1)->text().toStdString(); + + if(newText != _currentContainerItemCol2){ + + if (_model->getContainerValueType(containerRow)){ + _currentContainerItemCol2 = _model->changeMapItemStringValue(containerRow, row, newText); + + } else { + try{ + _currentContainerItemCol2 = _model->changeMapItemNumberValue(containerRow, row, std::stoi(_model->trimIntegerString(newText))); + } + catch(...) { + _currentContainerItemCol2 = _model->changeMapItemNumberValue(containerRow, row, 0); + } + } + applyModel(); + } + } } void VariableDialog::onContainerContentsSelectionChanged() @@ -559,9 +627,9 @@ void VariableDialog::onContainerContentsSelectionChanged() item = ui->containerContentsTable->item(row, 1); SCP_string newContainerDataText = (item) ? item->text().toStdString() : ""; - if (newContainerItemName != _currentContainerItem || _currentContainerItemData != newContainerDataText){ - _currentContainerItem = newContainerItemName; - _currentContainerItemData = newContainerDataText; + if (newContainerItemName != _currentContainerItemCol1 || _currentContainerItemCol2 != newContainerDataText){ + _currentContainerItemCol1 = newContainerItemName; + _currentContainerItemCol2 = newContainerDataText; applyModel(); } } @@ -1336,7 +1404,7 @@ void VariableDialog::updateContainerOptions() ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setRowCount(0); + // if there's no container, there's no container items ui->addContainerItemButton->setEnabled(false); @@ -1344,9 +1412,11 @@ void VariableDialog::updateContainerOptions() ui->deleteContainerItemButton->setEnabled(false); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setRowCount(0); ui->shiftItemDownButton->setEnabled(false); ui->shiftItemUpButton->setEnabled(false); + ui->containerContentsTable->clearSelection(); + ui->containerContentsTable->setRowCount(0); + } else { auto items = ui->containersTable->selectedItems(); @@ -1479,7 +1549,7 @@ void VariableDialog::updateContainerDataOptions(bool list) } // set selected and enable shifting functions - if (strings[x] == _currentContainerItem){ + if (strings[x] == _currentContainerItemCol1){ ui->containerContentsTable->clearSelection(); ui->containerContentsTable->item(x,0)->setSelected(true); @@ -1704,7 +1774,7 @@ void VariableDialog::preReject() void VariableDialog::checkValidModel() { - if (_model->checkValidModel()) { + if (ui->OkCancelButtons->button(QDialogButtonBox::Ok)->hasFocus() && _model->checkValidModel()){ accept(); } } diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 10b1704e3a1..ade50f77958 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -81,8 +81,8 @@ class VariableDialog : public QDialog { SCP_string _currentVariable = ""; SCP_string _currentVariableData = ""; SCP_string _currentContainer = ""; - SCP_string _currentContainerItem = ""; - SCP_string _currentContainerItemData = ""; + SCP_string _currentContainerItemCol1 = ""; + SCP_string _currentContainerItemCol2 = ""; }; From 87fd99f9f6fc4a6c4488d9ded538a2bd23b1b095 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 5 May 2024 00:18:47 -0400 Subject: [PATCH 177/466] Finish implementation of type formatting option --- .../mission/dialogs/VariableDialogModel.cpp | 12 ++-- .../src/mission/dialogs/VariableDialogModel.h | 4 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 67 ++++++++++++------- qtfred/src/ui/dialogs/VariableDialog.h | 1 + qtfred/ui/VariableDialog.ui | 26 +++++++ 5 files changed, 80 insertions(+), 30 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index c7ca3506236..280d0c90a59 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -7,7 +7,7 @@ namespace fso { namespace fred { namespace dialogs { - + static int _textMode = 0; VariableDialogModel::VariableDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) @@ -1797,7 +1797,7 @@ const SCP_vector> VariableDialogModel::getContainerNam SCP_string mapMidScript; SCP_string mapPostscript; - switch (_listTextMode) { + switch (_textMode) { case 1: listPrefix = ""; listPostscript = " List"; @@ -1831,13 +1831,13 @@ const SCP_vector> VariableDialogModel::getContainerNam default: // this takes care of weird cases. The logic should be simple enough to not have bugs, but just in case, switch back to default. - _listTextMode = 0; + _textMode = 0; listPrefix = "List of "; listPostscript = "s"; break; } - switch (_mapTextMode) { + switch (_textMode) { case 1: mapPrefix = ""; mapMidScript = "-keyed Map of "; @@ -1876,7 +1876,7 @@ const SCP_vector> VariableDialogModel::getContainerNam break; default: - _mapTextMode = 0; + _textMode = 0; mapPrefix = "Map with "; mapMidScript = " Keys and "; mapPostscript = " Values"; @@ -1936,6 +1936,8 @@ const SCP_vector> VariableDialogModel::getContainerNam return outStrings; } +void VariableDialogModel::setTextMode(int modeIn) { _textMode = modeIn;} + // This function is for cleaning up input strings that should be numbers. We could use std::stoi, // but this helps to not erase the entire string if user ends up mistyping just one digit. // If we ever allowed float types in sexp variables ... *shudder* ... we would definitely need a float diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index f10d73cd01b..50a2d1dea8d 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -125,6 +125,8 @@ class VariableDialogModel : public AbstractDialogModel { const SCP_vector> getVariableValues(); const SCP_vector> getContainerNames(); + void setTextMode(int modeIn); + bool checkValidModel(); bool apply() override; @@ -137,8 +139,6 @@ class VariableDialogModel : public AbstractDialogModel { private: SCP_vector _variableItems; SCP_vector _containerItems; - int _listTextMode = 0; - int _mapTextMode = 0; int _deleteWarningCount; diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index de7d571f464..433d2466de9 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -208,6 +208,11 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) this, &VariableDialog::onSwapKeysAndValuesButtonPressed); + connect(ui->selectFormatCombobox, + QOverload::of(&QComboBox::currentIndexChanged), + this, + &VariableDialog::onSelectFormatComboboxSelectionChanged); + ui->variablesTable->setColumnCount(3); ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); @@ -254,11 +259,17 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->containersTable->clearSelection(); ui->containerContentsTable->clearSelection(); + ui->selectFormatCombobox->addItem("Verbose"); + ui->selectFormatCombobox->addItem("Simplified"); + ui->selectFormatCombobox->addItem("Type and ()"); + ui->selectFormatCombobox->addItem("Type and <>"); + ui->selectFormatCombobox->addItem("Only ()"); + ui->selectFormatCombobox->addItem("Only <>"); + ui->selectFormatCombobox->addItem("No extra Marks"); + applyModel(); } -// TODO! make sure that when a variable is added that the whole model is reloaded. -// TODO! Fix me. This function does not work as intended because it must process both varaible and contents, not just one. void VariableDialog::onVariablesTableUpdated() { if (_applyingModel){ @@ -504,7 +515,7 @@ void VariableDialog::onContainerContentsTableUpdated() if (ui->containerContentsTable->item(row, 0)) { newString = ui->containerContentsTable->item(row, 0)->text().toStdString(); - if (!newString.empty() && newString != "Add Item ..."){ + if (!newString.empty() && newString != "Add item ..."){ if (_model->getContainerListOrMap(containerRow)) { _model->addListItem(containerRow, newString); @@ -531,7 +542,7 @@ void VariableDialog::onContainerContentsTableUpdated() newString = ui->containerContentsTable->item(row, 0)->text().toStdString(); // But we can only create a new map item here. Ignore if this container is a list. - if (!newString.empty() && newString.substr(0, 10) != "Add Item ..."){ + if (!newString.empty() && newString.substr(0, 10) != "Add item ..."){ if (!_model->getContainerListOrMap(containerRow)) { auto ret = _model->addMapItem(containerRow, "", newString); _currentContainerItemCol1 = ret.first; @@ -1139,6 +1150,12 @@ void VariableDialog::onSwapKeysAndValuesButtonPressed() applyModel(); } +void VariableDialog::onSelectFormatComboboxSelectionChanged() +{ + _model->setTextMode(ui->selectFormatCombobox->currentIndex()); + applyModel(); +} + VariableDialog::~VariableDialog(){}; // NOLINT @@ -1665,13 +1682,15 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setItem(x, 0, item); } - if (ui->containerContentsTable->item(x, 1)){ - ui->containerContentsTable->item(x, 1)->setText(strings[x].c_str()); - ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); - } else { - QTableWidgetItem* item = new QTableWidgetItem(strings[x].c_str()); - item->setFlags(item->flags() | Qt::ItemIsEditable); - ui->containerContentsTable->setItem(x, 1, item); + if (x < static_cast(strings.size())){ + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(strings[x].c_str()); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(strings[x].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } } } @@ -1689,28 +1708,30 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setItem(x, 0, item); } - if (ui->containerContentsTable->item(x, 1)){ - ui->containerContentsTable->item(x, 1)->setText(std::to_string(numbers[x]).c_str()); - ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); - } else { - QTableWidgetItem* item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); - item->setFlags(item->flags() | Qt::ItemIsEditable); - ui->containerContentsTable->setItem(x, 1, item); + if (x < static_cast(numbers.size())){ + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(std::to_string(numbers[x]).c_str()); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } } } if (ui->containerContentsTable->item(x, 0)){ - ui->containerContentsTable->item(x, 0)->setText("Add key ..."); + ui->containerContentsTable->item(x, 0)->setText("Add item ..."); } else { - QTableWidgetItem* item = new QTableWidgetItem("Add key ..."); + QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); ui->containerContentsTable->setItem(x, 0, item); } if (ui->containerContentsTable->item(x, 1)){ - ui->containerContentsTable->item(x, 1)->setText("Add Value ..."); + ui->containerContentsTable->item(x, 1)->setText("Add item ..."); ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem("Add Value ..."); + QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } @@ -1752,7 +1773,7 @@ int VariableDialog::getCurrentContainerItemRow() // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { - if (item && item->column() == 0 && item->text().toStdString() != "Add Item ...") { + if (item && item->column() == 0 && item->text().toStdString() != "Add item ...") { return item->row(); } } diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index ade50f77958..65a1ac5b7a1 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -72,6 +72,7 @@ class VariableDialog : public QDialog { void onShiftItemUpButtonPressed(); void onShiftItemDownButtonPressed(); void onSwapKeysAndValuesButtonPressed(); + void onSelectFormatComboboxSelectionChanged(); int getCurrentVariableRow(); int getCurrentContainerRow(); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 20d19c58e92..0cc996e665f 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -573,6 +573,32 @@ false + + + + 550 + 140 + 81 + 20 + + + + Type Format + + + Qt::AlignCenter + + + + + + 540 + 160 + 101 + 24 + + + From b9b36d2a769af65789e3fc48f6378d81d0960785 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 5 May 2024 00:19:01 -0400 Subject: [PATCH 178/466] Fix Shift up and down buttons --- qtfred/src/ui/dialogs/VariableDialog.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 433d2466de9..1c968ce316e 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1571,11 +1571,11 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->item(x,0)->setSelected(true); // more than one item and not already at the top of the list. - if (x > 0 && x < static_cast(strings.size())){ + if (!(x > 0 && x < static_cast(strings.size()))){ ui->shiftItemUpButton->setEnabled(false); } - if (x > -1 && x < static_cast(strings.size()) - 1){ + if (!(x > -1 && x < static_cast(strings.size()) - 1)){ ui->shiftItemDownButton->setEnabled(false); } } From e4886db4d269128c4dbf65d964f6056eacccbbf2 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 5 May 2024 00:58:24 -0400 Subject: [PATCH 179/466] Fix shift up and down buttons --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 8 ++++---- qtfred/src/ui/dialogs/VariableDialog.cpp | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 280d0c90a59..1eb4f73aad7 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1466,8 +1466,8 @@ void VariableDialogModel::shiftListItemUp(int containerIndex, int itemIndex) } // handle itemIndex out of bounds - if ( (container->string && itemIndex <= static_cast(container->stringValues.size())) - || (!container->string && itemIndex <= static_cast(container->numberValues.size())) ){ + if ( (container->string && itemIndex >= static_cast(container->stringValues.size())) + || (!container->string && itemIndex >= static_cast(container->numberValues.size())) ){ return; } @@ -1490,8 +1490,8 @@ void VariableDialogModel::shiftListItemDown(int containerIndex, int itemIndex) } // handle itemIndex out of bounds. -1 is necessary. since the bottom item is cannot be moved down. - if ( (container->string && itemIndex <= static_cast(container->stringValues.size()) - 1) - || (!container->string && itemIndex <= static_cast(container->numberValues.size()) - 1) ){ + if ( (container->string && itemIndex >= static_cast(container->stringValues.size()) - 1) + || (!container->string && itemIndex >= static_cast(container->numberValues.size()) - 1) ){ return; } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 1c968ce316e..30d1a16d423 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -315,8 +315,9 @@ void VariableDialog::onVariablesTableUpdated() // marking a variable as deleted failed, resync UI apply = true; } else { + _model->changeVariableName(item->row(), itemText); // now that we know that the variable was deleted - updateVariableOptions(); + applyModel(); } // if the user is restoring a deleted variable by inserting a name.... @@ -326,8 +327,9 @@ void VariableDialog::onVariablesTableUpdated() // marking a variable as deleted failed, resync UI apply = true; } else { + _model->changeVariableName(item->row(), itemText); // now that we know that the variable was deleted - updateVariableOptions(); + applyModel(); } } else if (itemText != _currentVariable){ @@ -1133,7 +1135,7 @@ void VariableDialog::onShiftItemDownButtonPressed() return; } - _model->shiftListItemUp(containerRow, itemRow); + _model->shiftListItemDown(containerRow, itemRow); applyModel(); } From 9b29e754ce9d88cc086dfb8a046ce9c139a73c31 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 5 May 2024 11:13:03 -0400 Subject: [PATCH 180/466] Fix overflow return values Previously on windows it was returning 0 because std::stol has a poor windows implementation. --- .../mission/dialogs/VariableDialogModel.cpp | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 1eb4f73aad7..da83297e8a5 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1991,15 +1991,10 @@ SCP_string VariableDialogModel::trimIntegerString(SCP_string source) ret = "0"; } - return clampIntegerString(ret); -} - -// Helper function for trimIntegerString that makes sure we don't try to save a value that overflows or underflows -// I don't recommend using outside of there, as there can be data loss if the input string is not cleaned first. -SCP_string VariableDialogModel::clampIntegerString(SCP_string source) -{ + // here we deal with overflow values. try { - long test = std::stol(source); + // some OS's will deal with this properly. + long test = std::stol(ret); if (test > INT_MAX) { return "2147483647"; @@ -2007,14 +2002,25 @@ SCP_string VariableDialogModel::clampIntegerString(SCP_string source) return "-2147483648"; } - return source; + return ret; } - // most truly ludicrous cases should be caught before here in the calling function, so this should not cause much if any data loss + // Others will not, sadly. + // So down here, we can still return the right overflow values if stol derped out. Since we've already cleaned out non-digits, + // checking for length *really should* allow us to know if something overflowed catch (...){ - return "0"; + if (ret.size() > 9){ + if (ret[0] == '-'){ + return "-2147483648"; + } else { + return "2147483647"; + } + } + + return "0"; } } + } // dialogs } // fred } // fso From 621030a72016ec7859c14fedc751c346afe12f18 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 5 May 2024 13:30:24 -0400 Subject: [PATCH 181/466] Fix various issues --- .../mission/dialogs/VariableDialogModel.cpp | 14 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 155 +++++++++--------- 2 files changed, 81 insertions(+), 88 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index da83297e8a5..5ba9561fcff 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -472,11 +472,6 @@ SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName return ""; } - // no name means no variable - if (newName == "") { - variable->deleted = true; - } - // Truncate name if needed if (newName.length() >= TOKEN_LENGTH){ newName = newName.substr(0, TOKEN_LENGTH - 1); @@ -546,7 +541,7 @@ bool VariableDialogModel::removeVariable(int index, bool toDelete) } if (toDelete){ - if (_deleteWarningCount < 3){ + if (_deleteWarningCount < 2){ SCP_string question = "Are you sure you want to delete this variable? Any references to it will have to be changed."; SCP_string info = ""; @@ -1055,10 +1050,6 @@ SCP_string VariableDialogModel::copyContainer(int index) SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newName) { - if (newName == "") { - return ""; - } - auto container = lookupContainer(index); // nothing to change, or invalid entry @@ -1190,7 +1181,7 @@ std::pair VariableDialogModel::addMapItem(int index) } ret.first = newKey; - + container->keys.push_back(newKey); if (container->string){ ret.second = ""; @@ -2016,6 +2007,7 @@ SCP_string VariableDialogModel::trimIntegerString(SCP_string source) } } + // emergency return value return "0"; } } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 30d1a16d423..09a153f3aa0 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -311,27 +311,20 @@ void VariableDialog::onVariablesTableUpdated() // so if the user just removed the name, mark it as deleted if (itemText.empty() && !_currentVariable.empty()) { - if (!_model->removeVariable(item->row(), true) ) { - // marking a variable as deleted failed, resync UI - apply = true; - } else { - _model->changeVariableName(item->row(), itemText); - // now that we know that the variable was deleted - applyModel(); - } + + _model->removeVariable(item->row(), true); + // these things need to be done whether the deletion failed or not. + _currentVariable = _model->changeVariableName(item->row(), itemText); + apply = true; // if the user is restoring a deleted variable by inserting a name.... } else if (!itemText.empty() && _currentVariable.empty()){ - if (!_model->removeVariable(item->row(), true) ) { - // marking a variable as deleted failed, resync UI - apply = true; - } else { - _model->changeVariableName(item->row(), itemText); - // now that we know that the variable was deleted - applyModel(); - } - + _model->removeVariable(item->row(), false); + // these things need to be done whether the restoration failed or not. + _currentVariable =_model->changeVariableName(item->row(), itemText); + apply = true; + } else if (itemText != _currentVariable){ auto ret = _model->changeVariableName(item->row(), itemText); @@ -464,11 +457,18 @@ void VariableDialog::onContainersTableUpdated() // are they editing an existing container name? } else if (ui->containersTable->item(row, 0)){ - _currentContainer = ui->containersTable->item(row,0)->text().toStdString(); - - if (_currentContainer != _model->changeContainerName(row, ui->containersTable->item(row,0)->text().toStdString())){ - applyModel(); - } + SCP_string newName = ui->containersTable->item(row,0)->text().toStdString(); + + // Restoring a deleted container? + if (_currentContainer.empty()){ + _model->removeContainer(row, false); + // Removing a container? + } else if (newName.empty()) { + _model->removeContainer(row, true); + } + + _currentContainer = _model->changeContainerName(row, newName); + applyModel(); } } @@ -1262,7 +1262,7 @@ void VariableDialog::applyModel() } // check if this is the current variable. - if (selectedRow < 0 && !_currentContainer.empty() && containers[x][0] == _currentContainer){ + if (selectedRow < 0 && containers[x][0] == _currentContainer){ selectedRow = x; } @@ -1284,6 +1284,14 @@ void VariableDialog::applyModel() // do we need to switch the delete button to a restore button? if (selectedRow > -1 && ui->containersTable->item(selectedRow, 2) && ui->containersTable->item(selectedRow, 2)->text().toStdString() == "Deleted") { ui->deleteContainerButton->setText("Restore"); + + // We can't restore empty container names. + if (ui->containersTable->item(selectedRow, 0) && ui->containersTable->item(selectedRow, 0)->text().toStdString().empty()){ + ui->deleteContainerButton->setEnabled(false); + } else { + ui->deleteContainerButton->setEnabled(true); + } + } else { ui->deleteContainerButton->setText("Delete"); } @@ -1373,6 +1381,14 @@ void VariableDialog::updateVariableOptions() // do we need to switch the delete button to a restore button? if (ui->variablesTable->item(row, 2) && ui->variablesTable->item(row, 2)->text().toStdString() == "Deleted"){ ui->deleteVariableButton->setText("Restore"); + + // We can't restore empty variable names. + if (ui->variablesTable->item(row, 0) && ui->variablesTable->item(row, 0)->text().toStdString().empty()){ + ui->deleteVariableButton->setEnabled(false); + } else { + ui->deleteVariableButton->setEnabled(true); + } + } else { ui->deleteVariableButton->setText("Delete"); } @@ -1551,14 +1567,16 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); ui->swapKeysAndValuesButton->setEnabled(false); - + + int x; // with string contents if (_model->getContainerValueType(row)){ - auto strings = _model->getStringValues(row); + auto& strings = _model->getStringValues(row); + int containerItemsRow = -1; ui->containerContentsTable->setRowCount(static_cast(strings.size()) + 1); - int x; + for (x = 0; x < static_cast(strings.size()); ++x){ if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText(strings[x].c_str()); @@ -1568,7 +1586,7 @@ void VariableDialog::updateContainerDataOptions(bool list) } // set selected and enable shifting functions - if (strings[x] == _currentContainerItemCol1){ + if (containerItemsRow < 0 && strings[x] == _currentContainerItemCol1){ ui->containerContentsTable->clearSelection(); ui->containerContentsTable->item(x,0)->setSelected(true); @@ -1593,28 +1611,11 @@ void VariableDialog::updateContainerDataOptions(bool list) } } - if (ui->containerContentsTable->item(x, 0)){ - ui->containerContentsTable->item(x, 0)->setText("Add item ..."); - } else { - QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); - ui->containerContentsTable->setItem(x, 0, item); - } - - if (ui->containerContentsTable->item(x, 1)){ - ui->containerContentsTable->item(x, 1)->setText(""); - ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); - } else { - QTableWidgetItem* item = new QTableWidgetItem(""); - item->setFlags(item->flags() & ~Qt::ItemIsEditable); - ui->containerContentsTable->setItem(x, 1, item); - } - // list with number contents } else { - auto numbers = _model->getNumberValues(row); + auto& numbers = _model->getNumberValues(row); ui->containerContentsTable->setRowCount(static_cast(numbers.size()) + 1); - int x; for (x = 0; x < static_cast(numbers.size()); ++x){ if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText(std::to_string(numbers[x]).c_str()); @@ -1633,23 +1634,24 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setItem(x, 1, item); } } + } - if (ui->containerContentsTable->item(x, 0)){ - ui->containerContentsTable->item(x, 0)->setText("Add item ..."); - } else { - QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); - ui->containerContentsTable->setItem(x, 0, item); - } - - if (ui->containerContentsTable->item(x, 1)){ - ui->containerContentsTable->item(x, 1)->setText(""); - ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); - } else { - QTableWidgetItem* item = new QTableWidgetItem(""); - item->setFlags(item->flags() & ~Qt::ItemIsEditable); - ui->containerContentsTable->setItem(x, 1, item); - } + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText("Add item ..."); + ui->containerContentsTable->item(x, 0)->setFlags(ui->containerContentsTable->item(x, 0)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 0, item); + } + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(""); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); } // or it could be a map container @@ -1667,7 +1669,8 @@ void VariableDialog::updateContainerDataOptions(bool list) // keys I didn't bother to make separate. Should have done the same with values. auto& keys = _model->getMapKeys(row); - + + int x; // string valued map. if (_model->getContainerValueType(row)){ auto& strings = _model->getStringValues(row); @@ -1675,7 +1678,6 @@ void VariableDialog::updateContainerDataOptions(bool list) // use the map as the size because map containers are only as good as their keys anyway. ui->containerContentsTable->setRowCount(static_cast(keys.size()) + 1); - int x; for (x = 0; x < static_cast(keys.size()); ++x){ if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText(keys[x].c_str()); @@ -1701,7 +1703,6 @@ void VariableDialog::updateContainerDataOptions(bool list) auto& numbers = _model->getNumberValues(row); ui->containerContentsTable->setRowCount(static_cast(keys.size()) + 1); - int x; for (x = 0; x < static_cast(keys.size()); ++x){ if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText(keys[x].c_str()); @@ -1721,22 +1722,22 @@ void VariableDialog::updateContainerDataOptions(bool list) } } } + } - if (ui->containerContentsTable->item(x, 0)){ - ui->containerContentsTable->item(x, 0)->setText("Add item ..."); - } else { - QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); - ui->containerContentsTable->setItem(x, 0, item); - } + if (ui->containerContentsTable->item(x, 0)){ + ui->containerContentsTable->item(x, 0)->setText("Add item ..."); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + ui->containerContentsTable->setItem(x, 0, item); + } - if (ui->containerContentsTable->item(x, 1)){ - ui->containerContentsTable->item(x, 1)->setText("Add item ..."); - ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); - } else { - QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); - item->setFlags(item->flags() | Qt::ItemIsEditable); - ui->containerContentsTable->setItem(x, 1, item); - } + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText("Add item ..."); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); } } } From 2e5e0aa4f348c4abc5eec16dcd82b35635d01b55 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 8 May 2024 15:27:10 -0400 Subject: [PATCH 182/466] add Assault Gunboat Make the XWI importer aware of the new XG-1 Star Wing (Assault Gunboat) when importing missions. --- code/mission/import/xwingmissionparse.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 40fdb7c783d..39b23d0e371 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -245,7 +245,7 @@ const char *xwi_determine_base_ship_class(const XWMFlightGroup *fg) case XWMFlightGroupType::fg_TIE_Bomber: return "TIE/sa Bomber"; case XWMFlightGroupType::fg_Gunboat: - return nullptr; + return "XG-1 Star Wing"; case XWMFlightGroupType::fg_Transport: return "DX-9 Stormtrooper Transport"; case XWMFlightGroupType::fg_Shuttle: From bc5f986d7609211300cc70c697926c7bd8a51347 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 10 May 2024 22:18:54 -0400 Subject: [PATCH 183/466] Bug fixes from testing --- .../mission/dialogs/VariableDialogModel.cpp | 7 +++- qtfred/src/ui/dialogs/VariableDialog.cpp | 32 ++++++++++++++++++- qtfred/ui/VariableDialog.ui | 4 +-- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 5ba9561fcff..55a60ff3fb1 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -751,6 +751,8 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) ++current; } + container->stringKeys = string; + return container->stringKeys; } // filter out current keys @@ -759,6 +761,9 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) key = trimIntegerString(key); } + container->stringKeys = string; + return container->stringKeys; + // cancel the operation case QMessageBox::HelpRole: return !string; @@ -1259,7 +1264,7 @@ SCP_string VariableDialogModel::copyListItem(int containerIndex, int index) { auto container = lookupContainer(containerIndex); - if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ + if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (!container->string && index >= static_cast(container->numberValues.size()))){ return ""; } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 09a153f3aa0..66f01db9565 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1110,7 +1110,8 @@ void VariableDialog::onShiftItemUpButtonPressed() int itemRow = getCurrentContainerItemRow(); - if (itemRow < 0){ + // item row being 0 is bad here since we're shifting up. + if (itemRow < 1){ applyModel(); return; } @@ -1614,6 +1615,7 @@ void VariableDialog::updateContainerDataOptions(bool list) // list with number contents } else { auto& numbers = _model->getNumberValues(row); + int containerItemsRow = -1; ui->containerContentsTable->setRowCount(static_cast(numbers.size()) + 1); for (x = 0; x < static_cast(numbers.size()); ++x){ @@ -1624,6 +1626,32 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setItem(x, 0, item); } + // set selected and enable shifting functions + if (containerItemsRow < 0 ){ + + SCP_string temp; + + if (numbers[x] == 0){ + temp = "0"; + } else { + sprintf(temp, "%i", numbers[x]); + } + + if (temp == _currentContainerItemCol1){ + ui->containerContentsTable->clearSelection(); + ui->containerContentsTable->item(x,0)->setSelected(true); + + // more than one item and not already at the top of the list. + if (!(x > 0 && x < static_cast(numbers.size()))){ + ui->shiftItemUpButton->setEnabled(false); + } + + if (!(x > -1 && x < static_cast(numbers.size()) - 1)){ + ui->shiftItemDownButton->setEnabled(false); + } + } + } + // empty out the second column as it's not needed in list mode if (ui->containerContentsTable->item(x, 1)){ ui->containerContentsTable->item(x, 1)->setText(""); @@ -1726,8 +1754,10 @@ void VariableDialog::updateContainerDataOptions(bool list) if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText("Add item ..."); + ui->containerContentsTable->item(x, 0)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 0, item); } diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 0cc996e665f..1eaaa34c35b 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -235,7 +235,7 @@ 10 210 631 - 221 + 211 @@ -439,7 +439,7 @@ 650 210 191 - 221 + 211 From cd3464d331055b88b6001c471e1591c187ad00dd Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 10 May 2024 22:52:56 -0400 Subject: [PATCH 184/466] Make sure string items can be deleted --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 55a60ff3fb1..dbd275980d3 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1319,7 +1319,7 @@ bool VariableDialogModel::removeListItem(int containerIndex, int index) { auto container = lookupContainer(containerIndex); - if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (container->string && index >= static_cast(container->numberValues.size()))){ + if (!container || index < 0 || (container->string && index >= static_cast(container->stringValues.size())) || (!container->string && index >= static_cast(container->numberValues.size()))){ return false; } From e66803af09014321cf96af6a2eb99b182d85eb4b Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 10 May 2024 23:17:54 -0400 Subject: [PATCH 185/466] Further Bug fixes based on testing --- .../mission/dialogs/VariableDialogModel.cpp | 24 ++++++++----------- .../src/mission/dialogs/VariableDialogModel.h | 2 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 21 ++++++++-------- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index dbd275980d3..84937f5a6be 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1550,23 +1550,21 @@ bool VariableDialogModel::removeMapItem(int index, int itemIndex) return true; } -SCP_string VariableDialogModel::changeMapItemKey(int index, SCP_string oldKey, SCP_string newKey) +SCP_string VariableDialogModel::changeMapItemKey(int index, int keyRow, SCP_string newKey) { auto container = lookupContainer(index); - if (!container){ + if (!container || container->list){ return ""; } - for (auto& key : container->keys){ - if (key == oldKey) { - key = newKey; - return newKey; - } + if (container->stringKeys){ + container->keys[keyRow] = newKey; + } else { + container->keys[keyRow] = trimIntegerString(newKey); } - // Failure - return oldKey; + return container->keys[keyRow]; } SCP_string VariableDialogModel::changeMapItemStringValue(int index, int itemIndex, SCP_string newValue) @@ -2004,12 +2002,10 @@ SCP_string VariableDialogModel::trimIntegerString(SCP_string source) // So down here, we can still return the right overflow values if stol derped out. Since we've already cleaned out non-digits, // checking for length *really should* allow us to know if something overflowed catch (...){ - if (ret.size() > 9){ - if (ret[0] == '-'){ - return "-2147483648"; - } else { + if (ret.size() > 10 && ret[0] == '-'){ + return "-2147483648"; + } else if (ret.size() > 9) { return "2147483647"; - } } // emergency return value diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 50a2d1dea8d..95968e84252 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -113,7 +113,7 @@ class VariableDialogModel : public AbstractDialogModel { void shiftListItemUp(int containerIndex, int itemIndex); void shiftListItemDown(int containerIndex, int itemIndex); - SCP_string changeMapItemKey(int index, SCP_string oldKey, SCP_string newKey); + SCP_string changeMapItemKey(int index, int keyIndex, SCP_string newKey); SCP_string changeMapItemStringValue(int index, int itemIndex, SCP_string newValue); SCP_string changeMapItemNumberValue(int index, int itemIndex, int newValue); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 66f01db9565..8668786feb5 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -575,17 +575,7 @@ void VariableDialog::onContainerContentsTableUpdated() } } else if (newText != _currentContainerItemCol1){ - - if (!_model->getContainerKeyType(containerRow)){ - - if (!_model->getContainerKeyType(containerRow)){ - newText = _model->trimIntegerString(newText); - } - - // TODO! Write a key change function so you can put something here. - - } - + _model->changeMapItemKey(containerRow, row, newText); return; } } @@ -1748,6 +1738,15 @@ void VariableDialog::updateContainerDataOptions(bool list) item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } + } else { + if (ui->containerContentsTable->item(x, 1)){ + ui->containerContentsTable->item(x, 1)->setText(""); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + QTableWidgetItem* item = new QTableWidgetItem(""); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } } } } From d557a6eff4d8ded84c55ed2fe45c4214d72ef90c Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 11 May 2024 00:19:18 -0400 Subject: [PATCH 186/466] More Bug fixes --- .../mission/dialogs/VariableDialogModel.cpp | 11 +++++-- qtfred/src/ui/dialogs/VariableDialog.cpp | 29 +++++++++++++++---- 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 84937f5a6be..175f169cef5 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1210,7 +1210,7 @@ std::pair VariableDialogModel::addMapItem(int index, SCP return ret; } - bool conflict; + bool conflict = false; int count = 0; SCP_string newKey; @@ -1234,7 +1234,11 @@ std::pair VariableDialogModel::addMapItem(int index, SCP ++count; } while (conflict && count < 101); } else { - newKey = key; + if (container->stringKeys){ + newKey = key; + } else { + newKey = trimIntegerString(key); + } } if (conflict) { @@ -1248,7 +1252,8 @@ std::pair VariableDialogModel::addMapItem(int index, SCP ret.second = value; } else { try { - container->numberValues.push_back(std::stoi(value)); + ret.second = trimIntegerString(value); + container->numberValues.push_back(std::stoi(ret.second)); ret.second = value; } catch (...) { diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 8668786feb5..767389fe78f 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -521,13 +521,13 @@ void VariableDialog::onContainerContentsTableUpdated() if (_model->getContainerListOrMap(containerRow)) { _model->addListItem(containerRow, newString); - } else { - _model->addMapItem(containerRow, newString, ""); } - _currentContainer = newString; + _currentContainerItemCol1 = newString; + _currentContainerItemCol2 = ""; + applyModel(); return; } @@ -535,9 +535,24 @@ void VariableDialog::onContainerContentsTableUpdated() } if (!ui->containerContentsTable->item(row, 1)) { - // At this point there's nothing else we can do and something may be off, anyway. - applyModel(); - return; + newString = ui->containerContentsTable->item(row, 1)->text().toStdString(); + + if (!newString.empty() && newString != "Add item ..."){ + + // This should not be a list container. + if (_model->getContainerListOrMap(containerRow)) { + applyModel(); + return; + } else { + _model->addMapItem(containerRow, "", newString); + } + + _currentContainerItemCol1 = newString; + _currentContainerItemCol2 = ""; + + applyModel(); + return; + } } // if we got here, we know that the second cell is valid. @@ -571,11 +586,13 @@ void VariableDialog::onContainerContentsTableUpdated() // Finally change the list item _currentContainerItemCol1 = _model->changeListItem(containerRow, row, newText); + applyModel(); return; } } else if (newText != _currentContainerItemCol1){ _model->changeMapItemKey(containerRow, row, newText); + applyModel(); return; } } From 20f992fe54515ecefa0d4f83af79760592177710 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Sat, 11 May 2024 00:43:36 -0400 Subject: [PATCH 187/466] Fix Not being able to add map items by value --- qtfred/src/ui/dialogs/VariableDialog.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 767389fe78f..969195128bf 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -502,6 +502,8 @@ void VariableDialog::onContainerContentsTableUpdated() int containerRow = getCurrentContainerRow(); int row = getCurrentContainerItemRow(); + + // just in case something is goofy, return if (row < 0 || containerRow < 0){ applyModel(); @@ -1822,7 +1824,7 @@ int VariableDialog::getCurrentContainerItemRow() // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { - if (item && item->column() == 0 && item->text().toStdString() != "Add item ...") { + if (item && ((item->column() == 0 && item->text().toStdString() != "Add item ...") || (item->column() == 1 && item->text().toStdString() != "Add item ..."))) { return item->row(); } } From df77615473dbbf687080f649cf25b12d61699b7a Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 12 May 2024 01:42:36 -0400 Subject: [PATCH 188/466] Various Fixes Based on Testing --- .../mission/dialogs/VariableDialogModel.cpp | 51 +++++++++++-------- qtfred/src/ui/dialogs/VariableDialog.cpp | 39 ++++---------- qtfred/ui/VariableDialog.ui | 46 ++++++++--------- 3 files changed, 65 insertions(+), 71 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 175f169cef5..1e06687bef5 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -551,7 +551,7 @@ bool VariableDialogModel::removeVariable(int index, bool toDelete) return variable->deleted; } - // adjust to the user's actions. If they are deleting variable after variable, allow after a while. + // adjust to the user's actions. If they are deleting variable after variable, allow after a while. No one expects Cybog the Careless ++_deleteWarningCount; } @@ -719,7 +719,7 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) } catch (...) { quickConvert = false; - nprintf(("Cyborg", "This is Cyborg. Long story short, I don't need this variable, but c++ thinks I do. Last good number on conversion was: %i\n", test)); + nprintf(("Cyborg", "This is Cyborg. Long story short, I don't need this variable, but c++ and its linters think I do. So who knows, maybe you can use this. Last good number on conversion was: %i\n", test)); } } @@ -731,15 +731,16 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) // If we couldn't convert easily, then we need some input from the user // now ask about data - QMessageBox msgBoxListToMapRetainData; - msgBoxListToMapRetainData.setWindowTitle("Key Type Conversion"); - msgBoxListToMapRetainData.setText("Fred could not convert all string keys to numbers automatically. Would you like to use default keys, filter out integers from the current keys or cancel the operation?"); - msgBoxListToMapRetainData.setInformativeText("Current keys will be overwritten unless you cancel and cannot be restored. Filtering will keep *any* numerical digits and starting \"-\" in the string. Filtering also does not prevent duplicate keys."); - msgBoxListToMapRetainData.addButton("Use Default Keys", QMessageBox::ActionRole); // No, these categories don't make sense, but QT makes underlying assumptions about where each button will be - msgBoxListToMapRetainData.addButton("Filter Current Keys ", QMessageBox::RejectRole); - auto defaultButton = msgBoxListToMapRetainData.addButton("Cancel", QMessageBox::HelpRole); - msgBoxListToMapRetainData.setDefaultButton(defaultButton); - auto ret = msgBoxListToMapRetainData.exec(); + QMessageBox msgBoxContainerKeyTypeSwitch; + msgBoxContainerKeyTypeSwitch.setWindowTitle("Key Type Conversion"); + msgBoxContainerKeyTypeSwitch.setText("Fred could not convert all string keys to numbers automatically. Would you like to use default keys, filter out integers from the current keys or cancel the operation?"); + msgBoxContainerKeyTypeSwitch.setInformativeText("Current keys will be overwritten unless you cancel and cannot be restored. Filtering will keep *any* numerical digits and starting \"-\" in the string. Filtering also does not prevent duplicate keys."); + msgBoxContainerKeyTypeSwitch.addButton("Use Default Keys", QMessageBox::ActionRole); // No, these categories don't make sense, but QT makes underlying assumptions about where each button will be + msgBoxContainerKeyTypeSwitch.addButton("Filter Current Keys ", QMessageBox::RejectRole); + auto defaultButton = msgBoxContainerKeyTypeSwitch.addButton("Cancel", QMessageBox::HelpRole); + msgBoxContainerKeyTypeSwitch.setDefaultButton(defaultButton); + msgBoxContainerKeyTypeSwitch.exec(); + auto ret = msgBoxContainerKeyTypeSwitch.buttonRole(msgBoxContainerKeyTypeSwitch.clickedButton()); switch(ret){ // just use default keys @@ -846,7 +847,8 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) msgBoxListToMapRetainData.addButton("Purge", QMessageBox::ApplyRole); auto defaultButton = msgBoxListToMapRetainData.addButton("Cancel", QMessageBox::HelpRole); msgBoxListToMapRetainData.setDefaultButton(defaultButton); - ret = msgBoxListToMapRetainData.exec(); + msgBoxListToMapRetainData.exec(); + ret = msgBoxListToMapRetainData.buttonRole(msgBoxListToMapRetainData.clickedButton()); switch (ret) { case QMessageBox::RejectRole: @@ -869,8 +871,10 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) } container->numberValues.clear(); + container->numberValues.resize(container->keys.size(), 0); container->list = list; return container->list; + break; case QMessageBox::ActionRole: { @@ -896,12 +900,12 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) container->numberValues.resize(container->keys.size(), 0); } - } - else { + } else { // here currentSize must be greater than the key size, because we already dealt with equal size. // So let's add a few keys to make them level. + int keyIndex = 0; + while (currentSize > container->keys.size()) { - int keyIndex = 0; SCP_string newKey; if (container->stringKeys) { @@ -923,7 +927,9 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) container->list = list; return container->list; } - case QMessageBox::ApplyRole: + break; + + case QMessageBox::ApplyRole: container->list = list; container->stringValues.clear(); @@ -1190,12 +1196,13 @@ std::pair VariableDialogModel::addMapItem(int index) if (container->string){ ret.second = ""; - container->stringValues.push_back(""); - } else { + } else { ret.second = "0"; - container->numberValues.push_back(0); } + container->stringValues.push_back(""); + container->numberValues.push_back(0); + return ret; } @@ -1250,6 +1257,8 @@ std::pair VariableDialogModel::addMapItem(int index, SCP if (container->string) { ret.second = value; + container->stringValues.push_back(ret.second); + container->numberValues.push_back(0); } else { try { ret.second = trimIntegerString(value); @@ -1260,6 +1269,8 @@ std::pair VariableDialogModel::addMapItem(int index, SCP ret.second = "0"; container->numberValues.push_back(0); } + + container->stringValues.emplace_back(""); } return ret; @@ -1308,7 +1319,7 @@ SCP_string VariableDialogModel::changeListItem(int containerIndex, int index, SC } try{ - *listItem = std::stoi(newString); + *listItem = std::stoi(trimIntegerString(newString)); } catch(...){ SCP_string temp; diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 969195128bf..577a47feac8 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -217,16 +217,16 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) ui->variablesTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->variablesTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); - ui->variablesTable->setColumnWidth(0, 190); - ui->variablesTable->setColumnWidth(1, 190); - ui->variablesTable->setColumnWidth(2, 120); + ui->variablesTable->setColumnWidth(0, 200); + ui->variablesTable->setColumnWidth(1, 200); + ui->variablesTable->setColumnWidth(2, 130); ui->containersTable->setColumnCount(3); ui->containersTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Name")); ui->containersTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Types")); ui->containersTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); ui->containersTable->setColumnWidth(0, 190); - ui->containersTable->setColumnWidth(1, 190); + ui->containersTable->setColumnWidth(1, 220); ui->containersTable->setColumnWidth(2, 120); ui->containerContentsTable->setColumnCount(2); @@ -234,8 +234,8 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) // Default to list ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - ui->containerContentsTable->setColumnWidth(0, 225); - ui->containerContentsTable->setColumnWidth(1, 225); + ui->containerContentsTable->setColumnWidth(0, 245); + ui->containerContentsTable->setColumnWidth(1, 245); // set radio buttons to manually toggled, as some of these have the same parent widgets and some don't // and I don't mind just manually toggling them. @@ -513,7 +513,6 @@ void VariableDialog::onContainerContentsTableUpdated() // Are they adding a new item? if (row == ui->containerContentsTable->rowCount() - 1){ - bool newItemCreated = false; SCP_string newString; if (ui->containerContentsTable->item(row, 0)) { @@ -536,7 +535,7 @@ void VariableDialog::onContainerContentsTableUpdated() } - if (!ui->containerContentsTable->item(row, 1)) { + if (ui->containerContentsTable->item(row, 1)) { newString = ui->containerContentsTable->item(row, 1)->text().toStdString(); if (!newString.empty() && newString != "Add item ..."){ @@ -545,34 +544,18 @@ void VariableDialog::onContainerContentsTableUpdated() if (_model->getContainerListOrMap(containerRow)) { applyModel(); return; - } else { - _model->addMapItem(containerRow, "", newString); } + + auto ret = _model->addMapItem(containerRow, "", newString); - _currentContainerItemCol1 = newString; - _currentContainerItemCol2 = ""; + _currentContainerItemCol1 = ret.first; + _currentContainerItemCol2 = ret.second; applyModel(); return; } } - // if we got here, we know that the second cell is valid. - newString = ui->containerContentsTable->item(row, 0)->text().toStdString(); - - // But we can only create a new map item here. Ignore if this container is a list. - if (!newString.empty() && newString.substr(0, 10) != "Add item ..."){ - if (!_model->getContainerListOrMap(containerRow)) { - auto ret = _model->addMapItem(containerRow, "", newString); - _currentContainerItemCol1 = ret.first; - _currentContainerItemCol2 = ret.second; - } - } - - // nothing else to determine at this point. - applyModel(); - return; - // are they editing an existing container item column 1? } else if (ui->containerContentsTable->item(row, 0)){ SCP_string newText = ui->containerContentsTable->item(row, 0)->text().toStdString(); diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 1eaaa34c35b..2545a683016 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -42,7 +42,7 @@ - 550 + 580 120 81 101 @@ -83,7 +83,7 @@ 10 30 - 521 + 551 191 @@ -121,7 +121,7 @@ - 650 + 680 20 191 201 @@ -181,7 +181,7 @@ - 550 + 580 30 82 86 @@ -234,7 +234,7 @@ 10 210 - 631 + 661 211 @@ -246,7 +246,7 @@ 10 30 - 471 + 511 171 @@ -278,7 +278,7 @@ - 520 + 550 30 106 175 @@ -338,7 +338,7 @@ - 550 + 580 30 82 85 @@ -376,7 +376,7 @@ - 650 + 680 20 191 181 @@ -436,7 +436,7 @@ - 650 + 680 210 191 211 @@ -481,23 +481,23 @@ - + - Data Type + Key Type - + - + String - + Number @@ -506,23 +506,23 @@ - + - Key Type + Data Type - + - + String - + Number @@ -538,7 +538,7 @@ 10 30 - 521 + 551 171 @@ -576,7 +576,7 @@ - 550 + 580 140 81 20 @@ -592,7 +592,7 @@ - 540 + 570 160 101 24 From e855fc81e9cd646f8bf159f1faeefc5e84625336 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sun, 12 May 2024 01:50:45 -0400 Subject: [PATCH 189/466] Check for conversion availability with same algo --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 1e06687bef5..7c1f87bc8c3 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -712,15 +712,12 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) if (container->stringKeys) { // Ok, this is the complicated type. First check if all keys can just quickly be transferred to numbers. bool quickConvert = true; - int test; + for (auto& key : container->keys) { - try { - test = std::stoi(key); - } - catch (...) { - quickConvert = false; - nprintf(("Cyborg", "This is Cyborg. Long story short, I don't need this variable, but c++ and its linters think I do. So who knows, maybe you can use this. Last good number on conversion was: %i\n", test)); - } + if(key != trimIntegerString(key)){ + quickConvert = false; + break; + } } // Don't even notify the user. Switching back is exceedingly easy. From e502cdfe76bb81560eff5daa67d474e0a677b3d3 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 13 May 2024 09:48:40 -0400 Subject: [PATCH 190/466] Finish the validation function --- .../mission/dialogs/VariableDialogModel.cpp | 48 ++++++++++++++++++- qtfred/src/ui/dialogs/VariableDialog.cpp | 1 - 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 7c1f87bc8c3..fe0436659a4 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -27,9 +27,15 @@ bool VariableDialogModel::checkValidModel() std::unordered_set namesTaken; std::unordered_set duplicates; + int emptyVarNames = 0; + for (const auto& variable : _variableItems){ if (!namesTaken.insert(variable.name).second) { duplicates.insert(variable.name); + } + + if (variable.name.empty()){ + ++emptyVarNames; } } @@ -45,27 +51,43 @@ bool VariableDialogModel::checkValidModel() } } - sprintf(messageOut, "There are %zu duplicate variables:\n", duplicates.size()); + sprintf(messageOut, "There are %zu duplicate variable names:\n", duplicates.size()); messageOut += messageBuffer + "\n\n"; } duplicates.clear(); std::unordered_set namesTakenContainer; SCP_vector duplicateKeys; + int emptyContainerNames = 0; + int emptyKeys = 0; + int notNumberKeys = 0; for (const auto& container : _containerItems){ if (!namesTakenContainer.insert(container.name).second) { duplicates.insert(container.name); } + if (container.name.empty()){ + ++emptyContainerNames; + } + if (!container.list){ std::unordered_set keysTakenContainer; for (const auto& key : container.keys){ if (!keysTakenContainer.insert(key).second) { - SCP_string temp = key + "in map" + container.name + ", "; + SCP_string temp = "\"" + key + "\" in map \"" + container.name + "\", "; duplicateKeys.push_back(temp); } + + if (key.empty()){ + ++emptyKeys; + } else if (!container->stringKeys){ + if (key != trimNumberString(key)){ + ++notNumberKeys; + } + } + } } } } @@ -100,6 +122,28 @@ bool VariableDialogModel::checkValidModel() messageOut += messageBuffer + "\n"; } + if (emptyVarNames > 0){ + messageBuffer.clear(); + sprintf(messageBuffer, "There are %i empty variable names which must be populated.\n", emptyVarNames); + + messageOut += messageBuffer; + } + + if (emptyContainerNames > 0){ + messageBuffer.clear(); + sprintf(messageBuffer, "There are %i empty container names which must be populated.\n", emptyContainerNames); + + messageOut += messageBuffer; + } + + if (emptyKeys > 0){ + messageBuffer.clear(); + sprintf(messageBuffer, "There are %i empty keys which must be populated.\n", emptyKeys); + + messageOut += messageBuffer; + } + + if (messageOut.empty()){ return true; } else { diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 577a47feac8..0cc7de7fde3 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -491,7 +491,6 @@ void VariableDialog::onContainersSelectionChanged() applyModel(); } -// TODO, finish this function void VariableDialog::onContainerContentsTableUpdated() { if (_applyingModel){ From b053131e7124b0dccae8efbc3f19d81ce25c89a5 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 13 May 2024 10:18:29 -0400 Subject: [PATCH 191/466] Connect the x with prereject instead of reject. Because the x is not connected to the cancel button, it has to be connected to preReject first. --- qtfred/src/ui/dialogs/VariableDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 0cc7de7fde3..d7cf8e48f5d 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -26,7 +26,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) // Reject if the user wants to. connect(ui->OkCancelButtons, &QDialogButtonBox::rejected, this, &VariableDialog::preReject); connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::apply); - connect(this, &QDialog::rejected, _model.get(), &VariableDialogModel::reject); + connect(this, &QDialog::rejected, _model.get(), &VariableDialogModel::preReject); connect(ui->variablesTable, &QTableWidget::itemChanged, From 455d2c9681516e2f3da4959653a344a9618069c4 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 13 May 2024 20:57:39 -0400 Subject: [PATCH 192/466] Add to Apply() And start safeguarding changes that would not be safe to make without cleaning references. --- .../mission/dialogs/VariableDialogModel.cpp | 79 +++++++++++++++---- .../src/mission/dialogs/VariableDialogModel.h | 3 + qtfred/src/ui/dialogs/VariableDialog.cpp | 2 +- 3 files changed, 69 insertions(+), 15 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index fe0436659a4..45bfca5956e 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -83,10 +83,10 @@ bool VariableDialogModel::checkValidModel() if (key.empty()){ ++emptyKeys; } else if (!container->stringKeys){ - if (key != trimNumberString(key)){ - ++notNumberKeys; - } + if (key != trimNumberString(key)){ + ++notNumberKeys; } + } } } @@ -143,6 +143,10 @@ bool VariableDialogModel::checkValidModel() messageOut += messageBuffer; } + if (_variableItems.size() >= MAX_SEXP_VARIABLES){ + messageOut += "There are more than the max of 250 variables.\n" + } + if (messageOut.empty()){ return true; @@ -176,10 +180,7 @@ bool VariableDialogModel::apply() for (int i = 0; i < MAX_SEXP_VARIABLES; ++i) { if (!stricmp(Sexp_variables[i].variable_name, variable.originalName.c_str())){ if (variable.deleted) { - memset(Sexp_variables[i].variable_name, 0, NAME_LENGTH); - memset(Sexp_variables[i].text, 0, NAME_LENGTH); - Sexp_variables[i].type = 0; - + sexp_variable_delete(i); deletedVariables.insert(variable.originalName); } else { if (variable.name != variable.originalName) { @@ -202,10 +203,25 @@ bool VariableDialogModel::apply() break; } } + } else { + if (variable.string){ + variable.flags |= SEXP_VARIABLE_STRING; + sexp_add_variable(variable.stringValue.c_str(), variable.name.c_str(), variable.flags, -1); + } else { + variable.flags |= SEXP_VARIABLE_NUMBER; + sexp_add_variable(std::to_string(variable.numberValue).c_str(), variable.name.c_str(), variable.flags, -1); + } } + // just in case if (!found) { - // TODO! Lookup how FRED adds new variables. (look for an empty slot maybe?) + if (variable.string){ + variable.flags |= SEXP_VARIABLE_STRING; + sexp_add_variable(variable.stringValue.c_str(), variable.name.c_str(), variable.flags, -1); + } else { + variable.flags |= SEXP_VARIABLE_NUMBER; + sexp_add_variable(std::to_string(variable.numberValue).c_str(), variable.name.c_str(), variable.flags, -1); + } } } @@ -355,6 +371,10 @@ bool VariableDialogModel::setVariableType(int index, bool string) return !string; } + if (!safeToAlterVariable(index)){ + return variable->string; + } + // Here we change the variable type! // this variable is currently a string if (variable->string) { @@ -459,7 +479,11 @@ SCP_string VariableDialogModel::setVariableStringValue(int index, SCP_string val if (!variable || !variable->string){ return ""; } - + + if (!safeToAlterVariable(index)){ + return variable->stringValue; + } + variable->stringValue = value; return value; } @@ -473,6 +497,10 @@ int VariableDialogModel::setVariableNumberValue(int index, int value) return 0; } + if (!safeToAlterVariable(index)){ + return variable->numberValue; + } + variable->numberValue = value; return value; @@ -484,13 +512,16 @@ SCP_string VariableDialogModel::addNewVariable() int count = 1; SCP_string name; + if (atMaxVariables()){ + return ""; + } + do { name = ""; sprintf(name, "newVar%i", count); variable = lookupVariableByName(name); ++count; - } while (variable != nullptr && count < 51); - + } while (variable != nullptr && count < MAX_SEXP_VARIABLES); if (variable){ return ""; @@ -501,9 +532,14 @@ SCP_string VariableDialogModel::addNewVariable() return name; } -SCP_string VariableDialogModel::addNewVariable(SCP_string nameIn){ +SCP_string VariableDialogModel::addNewVariable(SCP_string nameIn) +{ + if (atMaxVariables()){ + return ""; + } + _variableItems.emplace_back(); - _variableItems.back().name = nameIn; + _variableItems.back().name.substr(0, TOKEN_LENGTH - 1) = nameIn; return _variableItems.back().name; } @@ -516,6 +552,10 @@ SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName return ""; } + if (!safeToAlterVariable(index)){ + return variable->name; + } + // Truncate name if needed if (newName.length() >= TOKEN_LENGTH){ newName = newName.substr(0, TOKEN_LENGTH - 1); @@ -535,6 +575,10 @@ SCP_string VariableDialogModel::copyVariable(int index) return ""; } + if (atMaxVariables()){ + return ""; + } + int count = 1; variableInfo* variableSearch; SCP_string newName; @@ -580,7 +624,7 @@ bool VariableDialogModel::removeVariable(int index, bool toDelete) return false; } - if (variable->deleted == toDelete){ + if (variable->deleted == toDelete || !safeToAlterVariable(index)){ return variable->deleted; } @@ -608,6 +652,9 @@ bool VariableDialogModel::removeVariable(int index, bool toDelete) } } +bool safeToAlterVariable(int /*index*/){ + return true; +} // Container Section @@ -1737,6 +1784,10 @@ void VariableDialogModel::swapKeyAndValues(int index) } } +bool safeToAlterContainer(int /*index*/){ + return true; +} + SCP_string VariableDialogModel::changeMapItemNumberValue(int index, int itemIndex, int newValue) { auto mapItem = lookupContainerNumberItem(index, itemIndex); diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 95968e84252..8caba47d53e 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -72,6 +72,7 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string copyVariable(int index); // returns whether it succeeded bool removeVariable(int index, bool toDelete); + bool safeToAlterVariable(int index); // Container Section @@ -122,6 +123,8 @@ class VariableDialogModel : public AbstractDialogModel { const SCP_vector& getNumberValues(int index); void swapKeyAndValues(int index); + + bool safeToAlterContainer(int index); const SCP_vector> getVariableValues(); const SCP_vector> getContainerNames(); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index d7cf8e48f5d..657d271824f 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -26,7 +26,7 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) // Reject if the user wants to. connect(ui->OkCancelButtons, &QDialogButtonBox::rejected, this, &VariableDialog::preReject); connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::apply); - connect(this, &QDialog::rejected, _model.get(), &VariableDialogModel::preReject); + connect(this, &QDialog::rejected, this, &VariableDialog::preReject); connect(ui->variablesTable, &QTableWidget::itemChanged, From e7f6fa3c9148bde628bf593ce00200804cdbc6cb Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Mon, 13 May 2024 22:31:30 -0400 Subject: [PATCH 193/466] Upgrade Container Copying --- .../mission/dialogs/VariableDialogModel.cpp | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 45bfca5956e..441f1763781 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -584,7 +584,7 @@ SCP_string VariableDialogModel::copyVariable(int index) SCP_string newName; do { - sprintf(newName, "%i_%s", count, variable->name.substr(0, TOKEN_LENGTH - 4).c_str()); + sprintf(newName, "%s_%i", variable->name.substr(0, TOKEN_LENGTH - 4).c_str(), count); variableSearch = lookupVariableByName(newName); // open slot found! @@ -609,7 +609,7 @@ SCP_string VariableDialogModel::copyVariable(int index) } ++count; - } while (variableSearch != nullptr && count < 100); + } while (variableSearch != nullptr && count < MAX_SEXP_VARIABLES); return ""; } @@ -716,7 +716,7 @@ bool VariableDialogModel::setContainerValueType(int index, bool type) return true; } - if (container->string == type){ + if (container->string == type || !safeToAlterContainer(index)){ return container->string; } @@ -796,7 +796,7 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) return false; } - if (container->stringKeys == string){ + if (container->stringKeys == string || !safeToAlterContainer(index)){ return container->stringKeys; } @@ -818,7 +818,7 @@ bool VariableDialogModel::setContainerKeyType(int index, bool string) } // If we couldn't convert easily, then we need some input from the user - // now ask about data + // now ask about data QMessageBox msgBoxContainerKeyTypeSwitch; msgBoxContainerKeyTypeSwitch.setWindowTitle("Key Type Conversion"); msgBoxContainerKeyTypeSwitch.setText("Fred could not convert all string keys to numbers automatically. Would you like to use default keys, filter out integers from the current keys or cancel the operation?"); @@ -879,8 +879,8 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) return !list; } - if (container->list == list){ - return list; + if (container->list == list || !safeToAlterContainer(index)){ + return container->list; } if (container->list) { @@ -1127,7 +1127,7 @@ SCP_string VariableDialogModel::addContainer() SCP_string VariableDialogModel::addContainer(SCP_string nameIn) { _containerItems.emplace_back(); - _containerItems.back().name = nameIn; + _containerItems.back().name = nameIn.substr(0, TOKEN_LENGTH - 1); return _containerItems.back().name; } @@ -1140,9 +1140,25 @@ SCP_string VariableDialogModel::copyContainer(int index) return ""; } - // K.I.S.S. We could guarantee the names be unique, but so can the user, and there will definitely be a lower number of containers + // searching for a duplicate is not that hard. _containerItems.push_back(*container); - _containerItems.back().name = "copy_" + _containerItems.back().name; + container = &_containerItems.back(); + + SCP_string newName; + int count = 0; + + do { + sprintf(newName, "%s_%i", container->name.substr(0, TOKEN_LENGTH - 4).c_str(), count); + auto containerSearch = lookupVariableByName(newName); + + // open slot found! + if (!containerSearch){ + break; + } + ++count; + } + + _containerItems.back().name = newName; _containerItems.back().name = _containerItems.back().name.substr(0, TOKEN_LENGTH - 1); return _containerItems.back().name; } @@ -1152,13 +1168,13 @@ SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newNam auto container = lookupContainer(index); // nothing to change, or invalid entry - if (!container){ + if (!container || !safeToAlterContainer(index)){ return ""; } - // We cannot have two containers with the same name, but we need to check this somewhere else (like on accept attempt). - container->name = newName; - return newName; + // We cannot have two containers with the same name, but we need to check that somewhere else (like on accept attempt). + container->name = newName.substring(0, TOKEN_LENGTH - 1); + return container->name; } bool VariableDialogModel::removeContainer(int index, bool toDelete) @@ -1169,7 +1185,7 @@ bool VariableDialogModel::removeContainer(int index, bool toDelete) return false; } - if (container->deleted == toDelete){ + if (container->deleted == toDelete || !safeToAlterContainer(index)){ return container->deleted; } @@ -1689,7 +1705,7 @@ void VariableDialogModel::swapKeyAndValues(int index) auto container = lookupContainer(index); // bogus cases - if (!container || container->list){ + if (!container || container->list || !safeToAlterContainer(index)){ return; } From c53e6b1487f283c6480b5c79021e8b585e9d788f Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 14 May 2024 00:14:48 -0400 Subject: [PATCH 194/466] Finish Safe to Alter Options in Model Update --- qtfred/src/ui/dialogs/VariableDialog.cpp | 77 +++++++++++++++--------- qtfred/src/ui/dialogs/VariableDialog.h | 6 +- 2 files changed, 50 insertions(+), 33 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 657d271824f..ece10c9981c 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -398,7 +398,7 @@ void VariableDialog::onVariablesSelectionChanged() int row = getCurrentVariableRow(); if (row < 0){ - updateVariableOptions(); + updateVariableOptions(false); return; } @@ -482,7 +482,7 @@ void VariableDialog::onContainersSelectionChanged() int row = getCurrentContainerRow(); if (row < 0) { - updateContainerOptions(); + updateContainerOptions(false); return; } @@ -1165,6 +1165,7 @@ void VariableDialog::applyModel() int x = 0, selectedRow = -1; ui->variablesTable->setRowCount(static_cast(variables.size()) + 1); + bool safeToAlter = false; for (x = 0; x < static_cast(variables.size()); ++x){ if (ui->variablesTable->item(x, 0)){ @@ -1180,6 +1181,10 @@ void VariableDialog::applyModel() // there's a deletion. if (selectedRow < 0 && !_currentVariable.empty() && variables[x][0] == _currentVariable){ selectedRow = x; + + if (_model->!safeToAlterVariable(selectedRow)){ + safeToAlter = true; + } } if (ui->variablesTable->item(x, 1)){ @@ -1237,7 +1242,7 @@ void VariableDialog::applyModel() } } - updateVariableOptions(); + updateVariableOptions(safeToAlter); auto containers = _model->getContainerNames(); ui->containersTable->setRowCount(static_cast(containers.size() + 1)); @@ -1314,21 +1319,25 @@ void VariableDialog::applyModel() ui->containersTable->setItem(x, 2, item); } + bool safeToAlterContainer = false; + if (selectedRow < 0 && ui->containersTable->rowCount() > 1) { if (ui->containersTable->item(0, 0)){ _currentContainer = ui->containersTable->item(0, 0)->text().toStdString(); ui->containersTable->clearSelection(); ui->containersTable->item(0, 0)->setSelected(true); } + } else if (selectedRow > -1){ + safeToAlterContainer = _model->safeToAlterContainer(selectedRow); } // this will update the list/map items. - updateContainerOptions(); + updateContainerOptions(safeToAlterContainer); _applyingModel = false; }; -void VariableDialog::updateVariableOptions() +void VariableDialog::updateVariableOptions(bool safeToAlter) { int row = getCurrentVariableRow(); @@ -1348,21 +1357,23 @@ void VariableDialog::updateVariableOptions() return; } + // options that are always safe ui->copyVariableButton->setEnabled(true); - ui->deleteVariableButton->setEnabled(true); - ui->setVariableAsStringRadio->setEnabled(true); - ui->setVariableAsNumberRadio->setEnabled(true); ui->doNotSaveVariableRadio->setEnabled(true); ui->saveVariableOnMissionCompletedRadio->setEnabled(true); ui->saveVariableOnMissionCloseRadio->setEnabled(true); ui->setVariableAsEternalcheckbox->setEnabled(true); ui->networkVariableCheckbox->setEnabled(true); - // if nothing is selected, but something could be selected, make it so. - if (row < 0 && ui->variablesTable->rowCount() > 1) { - row = 0; - ui->variablesTable->item(row, 0)->setSelected(true); - _currentVariable = ui->variablesTable->item(row, 0)->text().toStdString(); + // options that are only safe if there are no references + if (safeToAlter){ + ui->deleteVariableButton->setEnabled(true); + ui->setVariableAsStringRadio->setEnabled(true); + ui->setVariableAsNumberRadio->setEnabled(true); + } else { + ui->deleteVariableButton->setEnabled(false); + ui->setVariableAsStringRadio->setEnabled(false); + ui->setVariableAsNumberRadio->setEnabled(false); } // start populating values @@ -1372,7 +1383,7 @@ void VariableDialog::updateVariableOptions() // do we need to switch the delete button to a restore button? if (ui->variablesTable->item(row, 2) && ui->variablesTable->item(row, 2)->text().toStdString() == "Deleted"){ - ui->deleteVariableButton->setText("Restore"); + ui->deleteVariableButton->setText("Restore"); // We can't restore empty variable names. if (ui->variablesTable->item(row, 0) && ui->variablesTable->item(row, 0)->text().toStdString().empty()){ @@ -1406,7 +1417,7 @@ void VariableDialog::updateVariableOptions() } -void VariableDialog::updateContainerOptions() +void VariableDialog::updateContainerOptions(bool safeToAlter) { int row = getCurrentContainerRow(); @@ -1448,18 +1459,21 @@ void VariableDialog::updateContainerOptions() } else { auto items = ui->containersTable->selectedItems(); + // options that should always be turned on ui->copyContainerButton->setEnabled(true); - ui->deleteContainerButton->setEnabled(true); - ui->setContainerAsStringRadio->setEnabled(true); - ui->setContainerAsNumberRadio->setEnabled(true); ui->doNotSaveContainerRadio->setEnabled(true); ui->saveContainerOnMissionCompletedRadio->setEnabled(true); ui->saveContainerOnMissionCloseRadio->setEnabled(true); ui->setContainerAsEternalCheckbox->setEnabled(true); - ui->setContainerAsMapRadio->setEnabled(true); - ui->setContainerAsListRadio->setEnabled(true); ui->networkContainerCheckbox->setEnabled(true); + // options that require it be safe to alter because the container is not referenced + ui->deleteContainerButton->setEnabled(safeToAlter); + ui->setContainerAsStringRadio->setEnabled(safeToAlter); + ui->setContainerAsNumberRadio->setEnabled(safeToAlter); + ui->setContainerAsMapRadio->setEnabled(safeToAlter); + ui->setContainerAsListRadio->setEnabled(safeToAlter); + if (_model->getContainerValueType(row)){ ui->setContainerAsStringRadio->setChecked(true); ui->setContainerAsNumberRadio->setChecked(false); @@ -1480,15 +1494,15 @@ void VariableDialog::updateContainerOptions() ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); - updateContainerDataOptions(true); + updateContainerDataOptions(true, safeToAlter); } else { ui->setContainerAsListRadio->setChecked(false); ui->setContainerAsMapRadio->setChecked(true); // Enable Key Controls - ui->setContainerKeyAsStringRadio->setEnabled(true); - ui->setContainerKeyAsNumberRadio->setEnabled(true); + ui->setContainerKeyAsStringRadio->setEnabled(safeToAlter); + ui->setContainerKeyAsNumberRadio->setEnabled(safeToAlter); // string keys if (_model->getContainerKeyType(row)){ @@ -1504,7 +1518,7 @@ void VariableDialog::updateContainerOptions() // Don't forget to change headings ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); - updateContainerDataOptions(false); + updateContainerDataOptions(false, safeToAlter); } ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); @@ -1529,7 +1543,7 @@ void VariableDialog::updateContainerOptions() } } -void VariableDialog::updateContainerDataOptions(bool list) +void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) { int row = getCurrentContainerRow(); @@ -1553,13 +1567,14 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->addContainerItemButton->setEnabled(true); ui->copyContainerItemButton->setEnabled(true); ui->deleteContainerItemButton->setEnabled(true); - ui->containerContentsTable->setRowCount(0); ui->shiftItemDownButton->setEnabled(true); ui->shiftItemUpButton->setEnabled(true); ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Value")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("")); ui->swapKeysAndValuesButton->setEnabled(false); - + + ui->containerContentsTable->setRowCount(0); + int x; // with string contents @@ -1681,12 +1696,14 @@ void VariableDialog::updateContainerDataOptions(bool list) ui->containerContentsTable->setHorizontalHeaderItem(0, new QTableWidgetItem("Key")); ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); - // Enable shift up and down buttons are off in Map mode. + // Enable shift up and down buttons are off in Map mode, because order makes no difference ui->shiftItemUpButton->setEnabled(false); ui->shiftItemDownButton->setEnabled(false); - ui->swapKeysAndValuesButton->setEnabled(true); - // keys I didn't bother to make separate. Should have done the same with values. + // we can swap if it's safe or if the data types match. If the data types *don't* match, then we run into reference issues. + ui->swapKeysAndValuesButton->setEnabled(safeToAlter || _containerItems[row].stringKeys == _containerItems[row].string); + + // keys I didn't bother to make separate. Should have done the same with values, ah regrets. auto& keys = _model->getMapKeys(row); int x; diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 65a1ac5b7a1..66922b26b48 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -31,9 +31,9 @@ class VariableDialog : public QDialog { void checkValidModel(); // Helper functions for this - void updateVariableOptions(); - void updateContainerOptions(); - void updateContainerDataOptions(bool list); + void updateVariableOptions(bool safeToAlter); + void updateContainerOptions(bool safeToAlter); + void updateContainerDataOptions(bool list, bool safeToAlter); void onVariablesTableUpdated(); void onVariablesSelectionChanged(); From 492683db2a44ba1d62b33f7894892f9d20a56d34 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Tue, 14 May 2024 13:18:11 -0400 Subject: [PATCH 195/466] Add A check to safeToAlter functions And Add more notes to the notes column in the UI --- .../mission/dialogs/VariableDialogModel.cpp | 68 ++++++++++++++----- .../src/mission/dialogs/VariableDialogModel.h | 7 +- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 441f1763781..2daee3470dc 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -162,12 +162,9 @@ bool VariableDialogModel::checkValidModel() } } -// TODO! This function in general just needs a lot of work. bool VariableDialogModel::apply() { - // what did we delete from the original list? We need to check these references and clean them. - std::unordered_set deletedVariables; SCP_vector> nameChangedVariables; bool found; @@ -181,7 +178,6 @@ bool VariableDialogModel::apply() if (!stricmp(Sexp_variables[i].variable_name, variable.originalName.c_str())){ if (variable.deleted) { sexp_variable_delete(i); - deletedVariables.insert(variable.originalName); } else { if (variable.name != variable.originalName) { nameChangedVariables.emplace_back(i, variable.originalName); @@ -226,12 +222,9 @@ bool VariableDialogModel::apply() } - // TODO! containers + // TODO! containers need to be saved. std::unordered_set deletedContainers; - // TODO! Look for referenced variables and containers. - // Need a way to clean up references. I'm thinking making some pop ups to confirm replacements created in the editor. - return false; } @@ -652,10 +645,28 @@ bool VariableDialogModel::removeVariable(int index, bool toDelete) } } -bool safeToAlterVariable(int /*index*/){ +bool VariableDialogModel::safeToAlterVariable(int index) +{ + auto variable = lookupVariable(index); + if (!variable){ + return false; + } + + // FIXME! until we can actually count references (via a SEXP backend), this is the best way to go. + if (variable.orginalName != ""){ + return false; + } + return true; } +bool VariableDialogModel::safeToAlterVariable(const variableInfo& variableItem) +{ + // again, FIXME! Needs actally reference count. + return variableItem.originalName == ""; +} + + // Container Section // true on string, false on number @@ -1800,10 +1811,27 @@ void VariableDialogModel::swapKeyAndValues(int index) } } -bool safeToAlterContainer(int /*index*/){ +bool VariableDialogModel::safeToAlterContainer(int index) +{ auto container = lookupContainer(index); + + if (!container){ + return false; + } + + // FIXME! Until there's a sexp backend, we can only check if we just created the container. + if (container.originalName != ""){ + return false; + } + return true; } +bool VariableDialogModel::safeToAlterContainer(const containerInfo& containerItem) +{ + // again, FIXME! Needs actally reference count. + return containerItem.originalName == ""; +} + SCP_string VariableDialogModel::changeMapItemNumberValue(int index, int itemIndex, int newValue) { auto mapItem = lookupContainerNumberItem(index, itemIndex); @@ -1884,17 +1912,19 @@ const SCP_vector> VariableDialogModel::getVariableValu { SCP_vector> outStrings; - for (const auto& item : _variableItems) { + for (const auto& item : _variableItems){ SCP_string notes = ""; - if (item.deleted) { + if (!safeToAlterVariable(item)){ + notes = "Referenced"; + } else if (item.deleted){ notes = "Deleted"; - } else if (item.originalName == "") { + } else if (item.originalName == ""){ notes = "New"; } else if (item.name != item.originalName){ notes = "Renamed"; - } else if (item.string && item.stringValue == "") { - notes = "Defaulting to empty string"; + } else if (item.string && item.stringValue == "" || !item.string && item.numberValue == 0){ + notes = "At default"; } SCP_string temp; @@ -2040,12 +2070,18 @@ const SCP_vector> VariableDialogModel::getContainerNam } - if (item.deleted) { + if (!safeToAlterContainer(item)){ + notes = "Referenced" + } else if (item.deleted) { notes = "Deleted"; } else if (item.originalName == "") { notes = "New"; } else if (item.name != item.originalName){ notes = "Renamed"; + } else if (!item.list && item.keys.empty()){ + notes = "Empty Map"; + } else if (item.list && ((item.string && item.stringValues.empty()) || (!item.string && item.numberValues.empty()))){ + notes = "Empty List"; } outStrings.push_back(std::array{item.name, type, notes}); diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 8caba47d53e..485e894a528 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -32,7 +32,9 @@ struct containerInfo { SCP_string originalName = ""; // I found out that keys could be strictly typed as numbers *after* finishing the majority of the model.... - // So I am just going to store numerical keys as strings and use a bool to differentiate. + // So I am just going to store numerical keys as strings and use a bool to differentiate. + // Additionally the reason why these are separate and not in a map is to allow duplicates that the user can fix. + // Less friction than a popup telling them they did it wrong. SCP_vector keys; SCP_vector numberValues; SCP_vector stringValues; @@ -145,7 +147,8 @@ class VariableDialogModel : public AbstractDialogModel { int _deleteWarningCount; - static SCP_string clampIntegerString(SCP_string source); + void sortMap(int index); + SCP_string clampIntegerString(SCP_string source); variableInfo* lookupVariable(int index){ if(index > -1 && index < static_cast(_variableItems.size()) ){ From 62a43512fd9cdc989951b67d7c2a72281737c872 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 15 May 2024 12:31:13 -0400 Subject: [PATCH 196/466] Finish safe to alter variable functions And sorting function. --- .../mission/dialogs/VariableDialogModel.cpp | 55 +++++++++++++++++++ .../src/mission/dialogs/VariableDialogModel.h | 2 + qtfred/src/ui/dialogs/VariableDialog.cpp | 2 - 3 files changed, 57 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 2daee3470dc..eab37f8c510 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -2092,6 +2092,61 @@ const SCP_vector> VariableDialogModel::getContainerNam void VariableDialogModel::setTextMode(int modeIn) { _textMode = modeIn;} +void VariableDialogModel::sortMap(int index) +{ + auto container = lookupContainer(index); + + // No sorting of non maps, and no point to sort if size is less than 2 + if (container.list || static_cast(container.keys.size() < 2)){ + return; + } + + // Yes, a little inefficient, but I didn't realize this was done in the original dialog when I designed the model. + SCP_vector keyCopy = container->keys; + SCP_vector sortedStringValues; + SCP_vector sortedNumberValues; + + // code borrowed from jg18, but going to try simple sorting first. Just need to see what it does with numbers. +// if (container->string) { + std::sort(container->keys.begin(), container->keys.end()); +/* } else { + std::sort(container->keys.begin(), + container->keys.end(), + [](const SCP_string &str1, const SCP_string &str2) -> bool { + try{ + return std::atoi(str1.c_str()) < std::atoi(str2.c_str()); + } + catch(...) + } + ); +*/ + + int y = 0; + + for (int x = 0; static_cast(container.keys.size()); ++x){ + // look for the first match in the temporary copy. + for (; y < static_cast(keyCopy.size()); ++y){ + // copy the values over. + if (container->keys[x] == keyCopy[y]){ + sortedStringValues.push_back(container->stringValues[y]); + sortedNumberValues.push_back(container->numberValues[y]); + break; + } + } + + // only reset y if we *dont* have a duplicate key coming up next. The first part of this check is simply a bound check. + // If the last item is a duplicate, that was checked on the previous iteration. + if ((x >= static_cast(container.keys.size()) - 1) || container.keys[x] != container.keys[x + 1]){ + y = 0; + } + } + + // TODO! Switch to Assertion after testing. + Verification(container->keys.size() == sortedStringValues.size(), "Keys size %uz and values %uz have a size mismatch after sorting. Please report to the SCP.", container->keys.size(), sortedStringValues.size()); + container->stringValues = std::move(sortedStringValues); + container->numberValues = std::move(sortedNumberValues); +} + // This function is for cleaning up input strings that should be numbers. We could use std::stoi, // but this helps to not erase the entire string if user ends up mistyping just one digit. // If we ever allowed float types in sexp variables ... *shudder* ... we would definitely need a float diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 485e894a528..807a9d78e26 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -75,6 +75,7 @@ class VariableDialogModel : public AbstractDialogModel { // returns whether it succeeded bool removeVariable(int index, bool toDelete); bool safeToAlterVariable(int index); + bool safeToAlterVariable(const VariableInfo& variableItem); // Container Section @@ -127,6 +128,7 @@ class VariableDialogModel : public AbstractDialogModel { void swapKeyAndValues(int index); bool safeToAlterContainer(int index); + bool safeToAlterContainer(const containerInfo& containerItem); const SCP_vector> getVariableValues(); const SCP_vector> getContainerNames(); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index ece10c9981c..b4136eeffa8 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1457,8 +1457,6 @@ void VariableDialog::updateContainerOptions(bool safeToAlter) } else { - auto items = ui->containersTable->selectedItems(); - // options that should always be turned on ui->copyContainerButton->setEnabled(true); ui->doNotSaveContainerRadio->setEnabled(true); From 7da79c174b8e68bb28f23ca7fd3bb81f9602d838 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 15 May 2024 13:01:13 -0400 Subject: [PATCH 197/466] Add sorting to other function calls --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index eab37f8c510..c222bb6f126 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1318,6 +1318,8 @@ std::pair VariableDialogModel::addMapItem(int index) container->stringValues.push_back(""); container->numberValues.push_back(0); + sortMap(index); + return ret; } @@ -1388,6 +1390,7 @@ std::pair VariableDialogModel::addMapItem(int index, SCP container->stringValues.emplace_back(""); } + sortMap(index); return ret; } @@ -1531,7 +1534,9 @@ std::pair VariableDialogModel::copyMapItem(int index, in container->keys.push_back(newKey); container->stringValues.push_back(copyValue); + container->numberValues.push_back(0); + sortMap(index); return std::make_pair(newKey, copyValue); } else { @@ -1572,10 +1577,12 @@ std::pair VariableDialogModel::copyMapItem(int index, in container->keys.push_back(newKey); container->numberValues.push_back(copyValue); + container->stringValues.push_back(""); SCP_string temp; sprintf(temp, "%i", copyValue); - + sortMap(index); + return std::make_pair(newKey, temp); } @@ -1695,6 +1702,7 @@ SCP_string VariableDialogModel::changeMapItemKey(int index, int keyRow, SCP_stri container->keys[keyRow] = trimIntegerString(newKey); } + sortMap(index); return container->keys[keyRow]; } @@ -1807,8 +1815,9 @@ void VariableDialogModel::swapKeyAndValues(int index) container->string = true; container->stringKeys = false; } - } + + sortMap(index); } bool VariableDialogModel::safeToAlterContainer(int index) From 3be4d0b15ad081a7f969e7d18888661d4ec8d6ca Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Thu, 16 May 2024 00:47:46 -0400 Subject: [PATCH 198/466] Fixes based on testing and linters --- .../mission/dialogs/VariableDialogModel.cpp | 84 ++++++++++++------- .../src/mission/dialogs/VariableDialogModel.h | 3 +- qtfred/src/ui/dialogs/VariableDialog.cpp | 19 +---- qtfred/src/ui/dialogs/VariableDialog.h | 13 ++- 4 files changed, 72 insertions(+), 47 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index c222bb6f126..95fe37e7167 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -82,8 +82,8 @@ bool VariableDialogModel::checkValidModel() if (key.empty()){ ++emptyKeys; - } else if (!container->stringKeys){ - if (key != trimNumberString(key)){ + } else if (!container.stringKeys){ + if (key != trimIntegerString(key)){ ++notNumberKeys; } @@ -119,7 +119,7 @@ bool VariableDialogModel::checkValidModel() SCP_string temp; sprintf(temp, "There are %zu duplicate map keys:\n\n", duplicateKeys.size()); - messageOut += messageBuffer + "\n"; + messageOut += temp + messageBuffer + "\n"; } if (emptyVarNames > 0){ @@ -144,7 +144,7 @@ bool VariableDialogModel::checkValidModel() } if (_variableItems.size() >= MAX_SEXP_VARIABLES){ - messageOut += "There are more than the max of 250 variables.\n" + messageOut += "There are more than the max of 250 variables.\n"; } @@ -169,7 +169,7 @@ bool VariableDialogModel::apply() bool found; // first we have to edit known variables. - for (const auto& variable : _variableItems){ + for (auto& variable : _variableItems){ found = false; // set of instructions for updating variables @@ -653,7 +653,7 @@ bool VariableDialogModel::safeToAlterVariable(int index) } // FIXME! until we can actually count references (via a SEXP backend), this is the best way to go. - if (variable.orginalName != ""){ + if (variable->originalName != ""){ return false; } @@ -983,21 +983,23 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) // to adjust keys. if (currentSize == container->keys.size()) { container->list = list; + + // we need all key related vectors to be size synced + if (container->string){ + container->numberValues.resize(container->keys.size(), 0); + } else { + container->stringValues.resize(container->keys.size(), ""); + } + return container->list; } // not enough data items. if (currentSize < container->keys.size()) { // just put the default value in them. Any string I specify for string values will - // be inconvenient to someone. - if (container->string) { - SCP_string newValue = ""; - container->stringValues.resize(container->keys.size(), newValue); - } - else { - // But differentiating numbers by having zero be the default is a good idea and does - container->numberValues.resize(container->keys.size(), 0); - } + // be inconvenient to someone. Zero is a good default, too. + container->stringValues.resize(container->keys.size(), ""); + container->numberValues.resize(container->keys.size(), 0); } else { // here currentSize must be greater than the key size, because we already dealt with equal size. @@ -1158,16 +1160,16 @@ SCP_string VariableDialogModel::copyContainer(int index) SCP_string newName; int count = 0; - do { + while(true) { sprintf(newName, "%s_%i", container->name.substr(0, TOKEN_LENGTH - 4).c_str(), count); - auto containerSearch = lookupVariableByName(newName); + auto containerSearch = lookupContainerByName(newName); // open slot found! if (!containerSearch){ break; } ++count; - } + } _containerItems.back().name = newName; _containerItems.back().name = _containerItems.back().name.substr(0, TOKEN_LENGTH - 1); @@ -1184,7 +1186,7 @@ SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newNam } // We cannot have two containers with the same name, but we need to check that somewhere else (like on accept attempt). - container->name = newName.substring(0, TOKEN_LENGTH - 1); + container->name = newName.substr(0, TOKEN_LENGTH - 1); return container->name; } @@ -1828,7 +1830,7 @@ bool VariableDialogModel::safeToAlterContainer(int index) } // FIXME! Until there's a sexp backend, we can only check if we just created the container. - if (container.originalName != ""){ + if (container->originalName != ""){ return false; } @@ -2080,7 +2082,7 @@ const SCP_vector> VariableDialogModel::getContainerNam if (!safeToAlterContainer(item)){ - notes = "Referenced" + notes = "Referenced"; } else if (item.deleted) { notes = "Deleted"; } else if (item.originalName == "") { @@ -2106,7 +2108,7 @@ void VariableDialogModel::sortMap(int index) auto container = lookupContainer(index); // No sorting of non maps, and no point to sort if size is less than 2 - if (container.list || static_cast(container.keys.size() < 2)){ + if (container->list || static_cast(container->keys.size() < 2)){ return; } @@ -2116,23 +2118,26 @@ void VariableDialogModel::sortMap(int index) SCP_vector sortedNumberValues; // code borrowed from jg18, but going to try simple sorting first. Just need to see what it does with numbers. -// if (container->string) { - std::sort(container->keys.begin(), container->keys.end()); -/* } else { + if (container->string) { + std::sort(container->keys.begin(), container->keys.end()); + } else { std::sort(container->keys.begin(), container->keys.end(), [](const SCP_string &str1, const SCP_string &str2) -> bool { try{ return std::atoi(str1.c_str()) < std::atoi(str2.c_str()); } - catch(...) + catch(...){ + // we're up the creek if this happens anyway. + return true; + } } ); -*/ + } int y = 0; - for (int x = 0; static_cast(container.keys.size()); ++x){ + for (int x = 0; x < static_cast(container->keys.size()); ++x){ // look for the first match in the temporary copy. for (; y < static_cast(keyCopy.size()); ++y){ // copy the values over. @@ -2145,7 +2150,7 @@ void VariableDialogModel::sortMap(int index) // only reset y if we *dont* have a duplicate key coming up next. The first part of this check is simply a bound check. // If the last item is a duplicate, that was checked on the previous iteration. - if ((x >= static_cast(container.keys.size()) - 1) || container.keys[x] != container.keys[x + 1]){ + if ((x >= static_cast(container->keys.size()) - 1) || container->keys[x] != container->keys[x + 1]){ y = 0; } } @@ -2156,6 +2161,27 @@ void VariableDialogModel::sortMap(int index) container->numberValues = std::move(sortedNumberValues); } +bool VariableDialogModel::atMaxVariables() +{ + if (_variableItems.size() < MAX_SEXP_VARIABLES){ + return false; + } + + int count = 0; + + for (const auto& item : _variableItems){ + if (!item.deleted){ + ++count; + } + } + + if (count < MAX_SEXP_VARIABLES){ + return false; + } else { + return true; + } +} + // This function is for cleaning up input strings that should be numbers. We could use std::stoi, // but this helps to not erase the entire string if user ends up mistyping just one digit. // If we ever allowed float types in sexp variables ... *shudder* ... we would definitely need a float diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 807a9d78e26..0f2422d884e 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -75,7 +75,7 @@ class VariableDialogModel : public AbstractDialogModel { // returns whether it succeeded bool removeVariable(int index, bool toDelete); bool safeToAlterVariable(int index); - bool safeToAlterVariable(const VariableInfo& variableItem); + bool safeToAlterVariable(const variableInfo& variableItem); // Container Section @@ -150,6 +150,7 @@ class VariableDialogModel : public AbstractDialogModel { int _deleteWarningCount; void sortMap(int index); + bool atMaxVariables(); SCP_string clampIntegerString(SCP_string source); variableInfo* lookupVariable(int index){ diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index b4136eeffa8..c58f4cb825a 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -24,9 +24,8 @@ VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) // Here we need to check that there are no issues with variable names or container names, or with maps having duplicate keys. connect(ui->OkCancelButtons, &QDialogButtonBox::accepted, this, &VariableDialog::checkValidModel); // Reject if the user wants to. - connect(ui->OkCancelButtons, &QDialogButtonBox::rejected, this, &VariableDialog::preReject); + connect(ui->OkCancelButtons, &QDialogButtonBox::rejected, this, &VariableDialog::reject); connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::apply); - connect(this, &QDialog::rejected, this, &VariableDialog::preReject); connect(ui->variablesTable, &QTableWidget::itemChanged, @@ -1182,7 +1181,7 @@ void VariableDialog::applyModel() if (selectedRow < 0 && !_currentVariable.empty() && variables[x][0] == _currentVariable){ selectedRow = x; - if (_model->!safeToAlterVariable(selectedRow)){ + if (!_model->safeToAlterVariable(selectedRow)){ safeToAlter = true; } } @@ -1699,7 +1698,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->shiftItemDownButton->setEnabled(false); // we can swap if it's safe or if the data types match. If the data types *don't* match, then we run into reference issues. - ui->swapKeysAndValuesButton->setEnabled(safeToAlter || _containerItems[row].stringKeys == _containerItems[row].string); + ui->swapKeysAndValuesButton->setEnabled(safeToAlter || _model->getContainerKeyType(row) == _model->getContainerValueType(row)); // keys I didn't bother to make separate. Should have done the same with values, ah regrets. auto& keys = _model->getMapKeys(row); @@ -1829,18 +1828,6 @@ int VariableDialog::getCurrentContainerItemRow() return -1; } -void VariableDialog::preReject() -{ - QMessageBox msgBox; - msgBox.setText("Are you sure you want to discard your changes?"); - msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); - int ret = msgBox.exec(); - - if (ret == QMessageBox::Yes) { - reject(); - } -} - void VariableDialog::checkValidModel() { if (ui->OkCancelButtons->button(QDialogButtonBox::Ok)->hasFocus() && _model->checkValidModel()){ diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 66922b26b48..14b870bda19 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -27,7 +27,6 @@ class VariableDialog : public QDialog { // basically UpdateUI, but called when there is an inconsistency between model and UI void applyModel(); - void preReject(); void checkValidModel(); // Helper functions for this @@ -84,6 +83,18 @@ class VariableDialog : public QDialog { SCP_string _currentContainer = ""; SCP_string _currentContainerItemCol1 = ""; SCP_string _currentContainerItemCol2 = ""; + + void VariableDialog::reject() + { + QMessageBox msgBox; + msgBox.setText("Are you sure you want to discard your changes?"); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + int ret = msgBox.exec(); + + if (ret == QMessageBox::Yes) { + QDialog::reject(); + } + } }; From c51bc08d1d47dea40ac6f9dd166a66efa2c17449 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 16 May 2024 11:11:07 -0400 Subject: [PATCH 199/466] linter fix --- qtfred/src/ui/dialogs/VariableDialog.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 14b870bda19..7c973366bd6 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -83,8 +83,8 @@ class VariableDialog : public QDialog { SCP_string _currentContainer = ""; SCP_string _currentContainerItemCol1 = ""; SCP_string _currentContainerItemCol2 = ""; - - void VariableDialog::reject() + + void reject() { QMessageBox msgBox; msgBox.setText("Are you sure you want to discard your changes?"); From d432f9b1e9095b10e847c594d765bb1ea31e0acd Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 16 May 2024 11:21:14 -0400 Subject: [PATCH 200/466] Make to loop over the right constraint --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 95fe37e7167..e3d232152bd 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -233,7 +233,7 @@ void VariableDialogModel::initializeData() _variableItems.clear(); _containerItems.clear(); - for (int i = 0; i < static_cast(_variableItems.size()); ++i){ + for (int i = 0; i < MAX_SEXP_VARIABLES; ++i){ if (strlen(Sexp_variables[i].text)) { _variableItems.emplace_back(); auto& item = _variableItems.back(); From 039866d526f4bc49061bfff1fd180e0068b071ad Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 16 May 2024 11:24:03 -0400 Subject: [PATCH 201/466] Linter Fixes --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index e3d232152bd..89ebe65d586 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1934,8 +1934,8 @@ const SCP_vector> VariableDialogModel::getVariableValu notes = "New"; } else if (item.name != item.originalName){ notes = "Renamed"; - } else if (item.string && item.stringValue == "" || !item.string && item.numberValue == 0){ - notes = "At default"; + } else if ((item.string && item.stringValue == "") || (!item.string && item.numberValue == 0)){ + notes = "Default Value"; } SCP_string temp; @@ -2156,7 +2156,7 @@ void VariableDialogModel::sortMap(int index) } // TODO! Switch to Assertion after testing. - Verification(container->keys.size() == sortedStringValues.size(), "Keys size %uz and values %uz have a size mismatch after sorting. Please report to the SCP.", container->keys.size(), sortedStringValues.size()); + Verification(container->keys.size() == sortedStringValues.size(), "Keys size %zu and values %zu have a size mismatch after sorting. Please report to the SCP.", container->keys.size(), sortedStringValues.size()); container->stringValues = std::move(sortedStringValues); container->numberValues = std::move(sortedNumberValues); } From e5e2729157a55e98ee6827702626296e35202b66 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 16 May 2024 11:38:01 -0400 Subject: [PATCH 202/466] Add non-numeric keys to warning messages --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 89ebe65d586..c14211c7396 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -143,6 +143,13 @@ bool VariableDialogModel::checkValidModel() messageOut += messageBuffer; } + if (notNumberKeys > 0){ + messageBuffer.clear(); + sprintf(messageBuffer, "There are %i numeric keys that are not numbers.\n", notNumberKeys); + + messageOut += messageBuffer; + } + if (_variableItems.size() >= MAX_SEXP_VARIABLES){ messageOut += "There are more than the max of 250 variables.\n"; } From ad7e7136929d3f61702e1cf73e2e4063f66cde94 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 16 May 2024 14:40:38 -0400 Subject: [PATCH 203/466] Override the virtual reject function --- qtfred/src/ui/dialogs/VariableDialog.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 7c973366bd6..9f96ed6ee97 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -84,7 +84,7 @@ class VariableDialog : public QDialog { SCP_string _currentContainerItemCol1 = ""; SCP_string _currentContainerItemCol2 = ""; - void reject() + void override reject() { QMessageBox msgBox; msgBox.setText("Are you sure you want to discard your changes?"); From 87367d6b84a281fc6ea0802e53699533945daa0e Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 16 May 2024 15:19:03 -0400 Subject: [PATCH 204/466] Make sure Keys and Items are proper length --- .../mission/dialogs/VariableDialogModel.cpp | 45 +++++++++++-------- qtfred/src/ui/dialogs/VariableDialog.h | 2 +- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index c14211c7396..403eb077829 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1179,7 +1179,6 @@ SCP_string VariableDialogModel::copyContainer(int index) } _containerItems.back().name = newName; - _containerItems.back().name = _containerItems.back().name.substr(0, TOKEN_LENGTH - 1); return _containerItems.back().name; } @@ -1193,6 +1192,7 @@ SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newNam } // We cannot have two containers with the same name, but we need to check that somewhere else (like on accept attempt). + // Otherwise editing variables and containers becomes super annoying. container->name = newName.substr(0, TOKEN_LENGTH - 1); return container->name; } @@ -1258,7 +1258,7 @@ SCP_string VariableDialogModel::addListItem(int index, SCP_string item) } if (container->string) { - container->stringValues.push_back(item); + container->stringValues.push_back(item.substr(0, TOKEN_LENGTH - 1)); return container->stringValues.back(); } else { auto temp = trimIntegerString(item); @@ -1332,6 +1332,7 @@ std::pair VariableDialogModel::addMapItem(int index) return ret; } +// Overload for specified key and/or Value std::pair VariableDialogModel::addMapItem(int index, SCP_string key, SCP_string value) { auto container = lookupContainer(index); @@ -1368,7 +1369,7 @@ std::pair VariableDialogModel::addMapItem(int index, SCP } while (conflict && count < 101); } else { if (container->stringKeys){ - newKey = key; + newKey = key.substr(0, TOKEN_LENGTH - 1); } else { newKey = trimIntegerString(key); } @@ -1382,7 +1383,7 @@ std::pair VariableDialogModel::addMapItem(int index, SCP container->keys.push_back(ret.first); if (container->string) { - ret.second = value; + ret.second = value.substr(0, TOKEN_LENGTH - 1); container->stringValues.push_back(ret.second); container->numberValues.push_back(0); } else { @@ -1436,7 +1437,7 @@ SCP_string VariableDialogModel::changeListItem(int containerIndex, int index, SC return ""; } - *listItem = newString; + *listItem = newString.substr(0, TOKEN_LENGTH - 1); } else { auto listItem = lookupContainerNumberItem(containerIndex, index); @@ -1500,7 +1501,7 @@ std::pair VariableDialogModel::copyMapItem(int index, in auto key = lookupContainerKey(index, mapIndex); - if (!key) { + if (key == nullptr) { return std::make_pair("", ""); } @@ -1509,13 +1510,14 @@ std::pair VariableDialogModel::copyMapItem(int index, in if (container->string){ auto value = lookupContainerStringItem(index, mapIndex); - // no valid value. - if (!value){ + // not a valid value. + if (value == nullptr){ return std::make_pair("", ""); } SCP_string copyValue = *value; - SCP_string newKey = *key + "0"; + SCP_string baseNewKey = key->substr(0, TOKEN_LENGTH - 4); + SCP_string newKey = baseNewKey + "0"; int count = 0; bool found; @@ -1531,10 +1533,10 @@ std::pair VariableDialogModel::copyMapItem(int index, in // attempt did not work, try next number if (found) { - sprintf(newKey, "%s%i", key->c_str(), ++count); + sprintf(newKey, "%s%i", baseNewKey.c_str(), ++count); } - } while (found && count < 100); + } while (found && count < 999); // we could not generate a new key .... somehow. if (found){ @@ -1557,7 +1559,8 @@ std::pair VariableDialogModel::copyMapItem(int index, in } int copyValue = *value; - SCP_string newKey = *key + "0"; + SCP_string baseNewKey = key->substr(0, TOKEN_LENGTH - 4); + SCP_string newKey = baseNewKey + "0"; int count = 0; bool found; @@ -1573,7 +1576,7 @@ std::pair VariableDialogModel::copyMapItem(int index, in // attempt did not work, try next number if (found) { - sprintf(newKey, "%s%i", key->c_str(), ++count); + sprintf(newKey, "%s%i", baseNewKey.c_str(), ++count); } } while (found && count < 100); @@ -1706,7 +1709,7 @@ SCP_string VariableDialogModel::changeMapItemKey(int index, int keyRow, SCP_stri } if (container->stringKeys){ - container->keys[keyRow] = newKey; + container->keys[keyRow] = newKey.substr(0, TOKEN_LENGTH - 1); } else { container->keys[keyRow] = trimIntegerString(newKey); } @@ -1723,7 +1726,7 @@ SCP_string VariableDialogModel::changeMapItemStringValue(int index, int itemInde return ""; } - *item = newValue; + *item = newValue.substr(0, TKEN_LENGTH - 1); return *item; } @@ -1936,13 +1939,15 @@ const SCP_vector> VariableDialogModel::getVariableValu if (!safeToAlterVariable(item)){ notes = "Referenced"; } else if (item.deleted){ - notes = "Deleted"; + notes = "To Be Deleted"; } else if (item.originalName == ""){ notes = "New"; - } else if (item.name != item.originalName){ - notes = "Renamed"; } else if ((item.string && item.stringValue == "") || (!item.string && item.numberValue == 0)){ notes = "Default Value"; + } else if (item.name != item.originalName){ + notes = "Renamed"; + } else { + notes = "Unreferenced"; } SCP_string temp; @@ -2091,7 +2096,7 @@ const SCP_vector> VariableDialogModel::getContainerNam if (!safeToAlterContainer(item)){ notes = "Referenced"; } else if (item.deleted) { - notes = "Deleted"; + notes = "To Be Deleted"; } else if (item.originalName == "") { notes = "New"; } else if (item.name != item.originalName){ @@ -2100,6 +2105,8 @@ const SCP_vector> VariableDialogModel::getContainerNam notes = "Empty Map"; } else if (item.list && ((item.string && item.stringValues.empty()) || (!item.string && item.numberValues.empty()))){ notes = "Empty List"; + } else { + notes = "Unreferenced"; } outStrings.push_back(std::array{item.name, type, notes}); diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 9f96ed6ee97..6d37610faee 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -84,7 +84,7 @@ class VariableDialog : public QDialog { SCP_string _currentContainerItemCol1 = ""; SCP_string _currentContainerItemCol2 = ""; - void override reject() + void reject() override { QMessageBox msgBox; msgBox.setText("Are you sure you want to discard your changes?"); From 07a389a5e8a26bf9154eac07c2035c412cb078bb Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 16 May 2024 15:45:53 -0400 Subject: [PATCH 205/466] Typo --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 403eb077829..ae08d5a7681 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1726,7 +1726,7 @@ SCP_string VariableDialogModel::changeMapItemStringValue(int index, int itemInde return ""; } - *item = newValue.substr(0, TKEN_LENGTH - 1); + *item = newValue.substr(0, TOKEN_LENGTH - 1); return *item; } From 860e28fbdb23e6d4c2745acbe3d3cf8ab6e41b9e Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 17 May 2024 15:33:11 -0400 Subject: [PATCH 206/466] Finish applying data from model to mission --- .../mission/dialogs/VariableDialogModel.cpp | 81 ++++++++++++++++++- .../src/mission/dialogs/VariableDialogModel.h | 3 +- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index ae08d5a7681..bef3bcf1ca8 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -169,6 +169,71 @@ bool VariableDialogModel::checkValidModel() } } +sexp_container VariableDialogModel::createContainerFromModel(const containerInfo& infoIn) +{ + sexp_container containerOut; + + containerOut.container_name = infoIn.name; + + // handle type info, which defaults to List + if (!infoIn.list) { + contianerOut.type &= ~ContainerType::LIST; + containerOut.type |= ContainerType::MAP; + + // Map Key type. This is not set by default, so we have explicity set it. + if (infoIn.stringKeys){ + containerOut.type |= ContainerType::STRING_KEYS; + } else { + containerOut.type |= ContainerType::NUMBER_KEYS; + } + } + + // New Containers also default to string data + if (!infoIn.String){ + containerOut.type &= ~ContainerType::STRING_DATA; + containerOut.type |= ContainerType::NUMBER_DATA; + } + + // Now flags + if (infoIn.flags & SEXP_VARIABLE_NETWORK){ + containerOut.type |= ContainerType::NETWORK; + } + + + if (infoIn.flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE){ + containerOut.type |= ContainerType::SAVE_TO_PLAYER_FILE; + } + + // No persistence means No flag, which is the default, but if anything else is true, then this has to be + if (infoIn.flags & SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE){ + containerOut.type |= ContainerType::SAVE_ON_MISSION_CLOSE; + } else if (infoIn.flags & SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS){ + containerOut.type |= ContainerType::SAVE_ON_MISSION_PROGRESS; + } else { + containerOut.type &= ~ContainerType::SAVE_TO_PLAYER_FILE; + } + + + // Handle contained data + if (infoIn.list){ + if (infoIn.string){ + containerOut.list_data = infoIn.stringValues; + } else { + for (const auto& number : infoIn.numberValues){ + containerOut.list_data.push_back(std::to_string(number)); + } + } + } else { + for (int x = 0; x < infoIn.keys; ++x){ + if (infoIn.string){ + map_data[infoIn.keys[x]] = infoIn.stringValues[x]; + } else { + map_data[infoIn.keys[x]] = std::to_string(infoIn.numberValues[x]); + } + } + } +} + bool VariableDialogModel::apply() { // what did we delete from the original list? We need to check these references and clean them. @@ -229,10 +294,20 @@ bool VariableDialogModel::apply() } - // TODO! containers need to be saved. - std::unordered_set deletedContainers; + SCP_vector newContainers; + SCP_unordered_map renamedContainers; + + for (const auto& container : _containerItems){ + newContainers.push_back(createContainerFromModel(container)); + + if (container.name != container.originalName){ + renamedContainers[container.originalName] = container.name; + } + } + + update_sexp_containers(newContainers, renamed_containers); - return false; + return true; } void VariableDialogModel::initializeData() diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 0f2422d884e..7f91c7a5abb 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -151,7 +151,8 @@ class VariableDialogModel : public AbstractDialogModel { void sortMap(int index); bool atMaxVariables(); - SCP_string clampIntegerString(SCP_string source); + + sexp_container& createContainerFromModel(const containerInfo& infoIn); variableInfo* lookupVariable(int index){ if(index > -1 && index < static_cast(_variableItems.size()) ){ From d1829a9f10c94c92d169d448590e3716cf7ca94a Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 17 May 2024 17:29:40 -0400 Subject: [PATCH 207/466] Fix Eternal flags being set with no persistence --- .../mission/dialogs/VariableDialogModel.cpp | 14 +++++---- qtfred/src/ui/dialogs/VariableDialog.cpp | 29 ++++++++++++++----- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index bef3bcf1ca8..c9bfec1db59 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -200,15 +200,19 @@ sexp_container VariableDialogModel::createContainerFromModel(const containerInfo } - if (infoIn.flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE){ - containerOut.type |= ContainerType::SAVE_TO_PLAYER_FILE; - } - // No persistence means No flag, which is the default, but if anything else is true, then this has to be if (infoIn.flags & SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE){ containerOut.type |= ContainerType::SAVE_ON_MISSION_CLOSE; + + if (infoIn.flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE){ + containerOut.type |= ContainerType::SAVE_TO_PLAYER_FILE; + } } else if (infoIn.flags & SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS){ containerOut.type |= ContainerType::SAVE_ON_MISSION_PROGRESS; + + if (infoIn.flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE){ + containerOut.type |= ContainerType::SAVE_TO_PLAYER_FILE; + } } else { containerOut.type &= ~ContainerType::SAVE_TO_PLAYER_FILE; } @@ -300,7 +304,7 @@ bool VariableDialogModel::apply() for (const auto& container : _containerItems){ newContainers.push_back(createContainerFromModel(container)); - if (container.name != container.originalName){ + if (container.originalName != "" && container.name != container.originalName){ renamedContainers[container.originalName] = container.name; } } diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index c58f4cb825a..73b12ea4b40 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1361,7 +1361,6 @@ void VariableDialog::updateVariableOptions(bool safeToAlter) ui->doNotSaveVariableRadio->setEnabled(true); ui->saveVariableOnMissionCompletedRadio->setEnabled(true); ui->saveVariableOnMissionCloseRadio->setEnabled(true); - ui->setVariableAsEternalcheckbox->setEnabled(true); ui->networkVariableCheckbox->setEnabled(true); // options that are only safe if there are no references @@ -1400,20 +1399,28 @@ void VariableDialog::updateVariableOptions(bool safeToAlter) if (ret == 0){ ui->doNotSaveVariableRadio->setChecked(true); ui->saveVariableOnMissionCompletedRadio->setChecked(false); - ui->saveVariableOnMissionCloseRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setChecked(false); + ui->setVariableAsEternalcheckbox->setEnabled(false); } else if (ret == 1) { ui->doNotSaveVariableRadio->setChecked(false); ui->saveVariableOnMissionCompletedRadio->setChecked(true); ui->saveVariableOnMissionCloseRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); } else { + ui->setVariableAsEternalcheckbox->setEnabled(true); ui->doNotSaveVariableRadio->setChecked(false); ui->saveVariableOnMissionCompletedRadio->setChecked(false); ui->saveVariableOnMissionCloseRadio->setChecked(true); + + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); } ui->networkVariableCheckbox->setChecked(_model->getVariableNetworkStatus(row)); - ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); - } void VariableDialog::updateContainerOptions(bool safeToAlter) @@ -1461,7 +1468,6 @@ void VariableDialog::updateContainerOptions(bool safeToAlter) ui->doNotSaveContainerRadio->setEnabled(true); ui->saveContainerOnMissionCompletedRadio->setEnabled(true); ui->saveContainerOnMissionCloseRadio->setEnabled(true); - ui->setContainerAsEternalCheckbox->setEnabled(true); ui->networkContainerCheckbox->setEnabled(true); // options that require it be safe to alter because the container is not referenced @@ -1518,7 +1524,6 @@ void VariableDialog::updateContainerOptions(bool safeToAlter) updateContainerDataOptions(false, safeToAlter); } - ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); ui->networkContainerCheckbox->setChecked(_model->getContainerNetworkStatus(row)); int ret = _model->getContainerOnMissionCloseOrCompleteFlag(row); @@ -1526,15 +1531,25 @@ void VariableDialog::updateContainerOptions(bool safeToAlter) if (ret == 0){ ui->doNotSaveContainerRadio->setChecked(true); ui->saveContainerOnMissionCompletedRadio->setChecked(false); - ui->saveContainerOnMissionCloseRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(false); + + ui->setContainerAsEternalCheckbox->setChecked(false); + ui->setContainerAsEternalCheckbox->setEnabled(false); + } else if (ret == 1) { ui->doNotSaveContainerRadio->setChecked(false); ui->saveContainerOnMissionCompletedRadio->setChecked(true); ui->saveContainerOnMissionCloseRadio->setChecked(false); + + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); } else { ui->doNotSaveContainerRadio->setChecked(false); ui->saveContainerOnMissionCompletedRadio->setChecked(false); ui->saveContainerOnMissionCloseRadio->setChecked(true); + + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); } } From 768f88b135d99ce04e1d335fe560fef2ac39666b Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 17 May 2024 17:44:38 -0400 Subject: [PATCH 208/466] Linter Fixes --- .../mission/dialogs/VariableDialogModel.cpp | 33 +++++++++++-------- .../src/mission/dialogs/VariableDialogModel.h | 2 +- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index c9bfec1db59..7e97e0d50a0 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -177,7 +177,7 @@ sexp_container VariableDialogModel::createContainerFromModel(const containerInfo // handle type info, which defaults to List if (!infoIn.list) { - contianerOut.type &= ~ContainerType::LIST; + containerOut.type &= ~ContainerType::LIST; containerOut.type |= ContainerType::MAP; // Map Key type. This is not set by default, so we have explicity set it. @@ -189,7 +189,7 @@ sexp_container VariableDialogModel::createContainerFromModel(const containerInfo } // New Containers also default to string data - if (!infoIn.String){ + if (!infoIn.string){ containerOut.type &= ~ContainerType::STRING_DATA; containerOut.type |= ContainerType::NUMBER_DATA; } @@ -220,19 +220,24 @@ sexp_container VariableDialogModel::createContainerFromModel(const containerInfo // Handle contained data if (infoIn.list){ - if (infoIn.string){ - containerOut.list_data = infoIn.stringValues; - } else { - for (const auto& number : infoIn.numberValues){ - containerOut.list_data.push_back(std::to_string(number)); - } - } + + if (infoIn.string){ + for (const auto& string : infoIn.stringValues){ + containerOut.list_data.push_back(string); + } + } else { + for (const auto& number : infoIn.numberValues){ + + containerOut.list_data.push_back(std::to_string(number)); + + } + } } else { - for (int x = 0; x < infoIn.keys; ++x){ + for (int x = 0; x < static_cast(infoIn.keys.size()); ++x){ if (infoIn.string){ - map_data[infoIn.keys[x]] = infoIn.stringValues[x]; + containerOut.map_data[infoIn.keys[x]] = infoIn.stringValues[x]; } else { - map_data[infoIn.keys[x]] = std::to_string(infoIn.numberValues[x]); + containerOut.map_data[infoIn.keys[x]] = std::to_string(infoIn.numberValues[x]); } } } @@ -298,7 +303,7 @@ bool VariableDialogModel::apply() } - SCP_vector newContainers; + SCP_vector newContainers; SCP_unordered_map renamedContainers; for (const auto& container : _containerItems){ @@ -309,7 +314,7 @@ bool VariableDialogModel::apply() } } - update_sexp_containers(newContainers, renamed_containers); + update_sexp_containers(newContainers, renamedContainers); return true; } diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 7f91c7a5abb..067dc7dd310 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -152,7 +152,7 @@ class VariableDialogModel : public AbstractDialogModel { void sortMap(int index); bool atMaxVariables(); - sexp_container& createContainerFromModel(const containerInfo& infoIn); + sexp_container createContainerFromModel(const containerInfo& infoIn); variableInfo* lookupVariable(int index){ if(index > -1 && index < static_cast(_variableItems.size()) ){ From 2cf24cb27b26f0ed56986ce376b3cd39ef503b78 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Fri, 17 May 2024 17:58:48 -0400 Subject: [PATCH 209/466] Trying to fix this mystifying error --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 4 +++- qtfred/src/mission/dialogs/VariableDialogModel.h | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 7e97e0d50a0..40455bf0634 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -169,7 +169,7 @@ bool VariableDialogModel::checkValidModel() } } -sexp_container VariableDialogModel::createContainerFromModel(const containerInfo& infoIn) +sexp_container VariableDialogModel::createContainer(const containerInfo& infoIn) { sexp_container containerOut; @@ -241,6 +241,8 @@ sexp_container VariableDialogModel::createContainerFromModel(const containerInfo } } } + + return containerOut; } bool VariableDialogModel::apply() diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 067dc7dd310..6a01ef29cca 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -149,11 +149,11 @@ class VariableDialogModel : public AbstractDialogModel { int _deleteWarningCount; + sexp_container createContainer(const containerInfo& infoIn); + void sortMap(int index); bool atMaxVariables(); - sexp_container createContainerFromModel(const containerInfo& infoIn); - variableInfo* lookupVariable(int index){ if(index > -1 && index < static_cast(_variableItems.size()) ){ return &_variableItems[index]; From d8c7a63af75fb69a9cd1711023310dbbe0e18c11 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 17 May 2024 18:04:08 -0400 Subject: [PATCH 210/466] This should actually fix it. --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 3 +-- qtfred/src/mission/dialogs/VariableDialogModel.h | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 40455bf0634..ddf6546ea43 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1,6 +1,5 @@ #include "VariableDialogModel.h" #include "parse/sexp.h" -#include "parse/sexp_container.h" #include #include @@ -309,7 +308,7 @@ bool VariableDialogModel::apply() SCP_unordered_map renamedContainers; for (const auto& container : _containerItems){ - newContainers.push_back(createContainerFromModel(container)); + newContainers.push_back(createContainer(container)); if (container.originalName != "" && container.name != container.originalName){ renamedContainers[container.originalName] = container.name; diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 6a01ef29cca..9dac7b37b17 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -3,6 +3,7 @@ #include "globalincs/pstypes.h" #include "AbstractDialogModel.h" +#include "parse/sexp_container.h" #include namespace fso { From 7229fdc4239c32cd58751647a0ba5db01e2caf98 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 18 May 2024 15:58:19 -0400 Subject: [PATCH 211/466] Fix variable read and write --- .../mission/dialogs/VariableDialogModel.cpp | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index ddf6546ea43..dd3a25dffbc 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -281,18 +281,19 @@ bool VariableDialogModel::apply() break; } } - } else { - if (variable.string){ - variable.flags |= SEXP_VARIABLE_STRING; - sexp_add_variable(variable.stringValue.c_str(), variable.name.c_str(), variable.flags, -1); - } else { - variable.flags |= SEXP_VARIABLE_NUMBER; - sexp_add_variable(std::to_string(variable.numberValue).c_str(), variable.name.c_str(), variable.flags, -1); - } - } - // just in case - if (!found) { + // just in case + if (!found) { + if (variable.string){ + variable.flags |= SEXP_VARIABLE_STRING; + sexp_add_variable(variable.stringValue.c_str(), variable.name.c_str(), variable.flags, -1); + } else { + variable.flags |= SEXP_VARIABLE_NUMBER; + sexp_add_variable(std::to_string(variable.numberValue).c_str(), variable.name.c_str(), variable.flags, -1); + } + } + + } else { if (variable.string){ variable.flags |= SEXP_VARIABLE_STRING; sexp_add_variable(variable.stringValue.c_str(), variable.name.c_str(), variable.flags, -1); @@ -326,7 +327,7 @@ void VariableDialogModel::initializeData() _containerItems.clear(); for (int i = 0; i < MAX_SEXP_VARIABLES; ++i){ - if (strlen(Sexp_variables[i].text)) { + if (!(Sexp_variables[i].type & SEXP_VARIABLE_NOT_USED)) { _variableItems.emplace_back(); auto& item = _variableItems.back(); item.name = Sexp_variables[i].variable_name; From 77062e25714cfba3e6c048194b7d2e93b1b0b007 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 18 May 2024 16:17:32 -0400 Subject: [PATCH 212/466] Make sure check eternal boxes are properly enabled --- qtfred/src/ui/dialogs/VariableDialog.cpp | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 73b12ea4b40..a2928726b7a 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -733,6 +733,9 @@ void VariableDialog::onDoNotSaveVariableRadioSelected() } else { ui->saveVariableOnMissionCompletedRadio->setChecked(false); ui->saveVariableOnMissionCloseRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setChecked(false); + ui->setVariableAsEternalcheckbox->setEnabled(false); } } @@ -752,6 +755,9 @@ void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() } else { ui->doNotSaveVariableRadio->setChecked(false); ui->saveVariableOnMissionCloseRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); } } @@ -772,6 +778,9 @@ void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() } else { ui->doNotSaveVariableRadio->setChecked(false); ui->saveVariableOnMissionCompletedRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); } } @@ -784,7 +793,7 @@ void VariableDialog::onSaveVariableAsEternalCheckboxClicked() } // If the model returns the old status, then the change failed and we're out of sync. - if (ui->setVariableAsEternalcheckbox->isChecked() == _model->setVariableEternalFlag(row, ui->setVariableAsEternalcheckbox->isChecked())) { + if (ui->setVariableAsEternalcheckbox->isChecked() != _model->setVariableEternalFlag(row, ui->setVariableAsEternalcheckbox->isChecked())) { applyModel(); } else { ui->setVariableAsEternalcheckbox->setChecked(!ui->setVariableAsEternalcheckbox->isChecked()); @@ -800,7 +809,7 @@ void VariableDialog::onNetworkVariableCheckboxClicked() } // If the model returns the old status, then the change failed and we're out of sync. - if (ui->networkVariableCheckbox->isChecked() == _model->setVariableNetworkStatus(row, ui->networkVariableCheckbox->isChecked())) { + if (ui->networkVariableCheckbox->isChecked() != _model->setVariableNetworkStatus(row, ui->networkVariableCheckbox->isChecked())) { applyModel(); } else { ui->networkVariableCheckbox->setChecked(!ui->networkVariableCheckbox->isChecked()); @@ -954,6 +963,9 @@ void VariableDialog::onDoNotSaveContainerRadioSelected() ui->doNotSaveContainerRadio->setChecked(true); ui->saveContainerOnMissionCloseRadio->setChecked(false); ui->saveContainerOnMissionCompletedRadio->setChecked(false); + + ui->setContainerAsEternalCheckbox->setChecked(false); + ui->setContainerAsEternalCheckbox->setEnabled(false); } } @@ -972,6 +984,9 @@ void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() ui->doNotSaveContainerRadio->setChecked(false); ui->saveContainerOnMissionCloseRadio->setChecked(false); ui->saveContainerOnMissionCompletedRadio->setChecked(true); + + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); } } @@ -990,6 +1005,9 @@ void VariableDialog::onSaveContainerOnMissionCloseRadioSelected() ui->doNotSaveContainerRadio->setChecked(false); ui->saveContainerOnMissionCloseRadio->setChecked(true); ui->saveContainerOnMissionCompletedRadio->setChecked(false); + + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); } } @@ -1181,7 +1199,7 @@ void VariableDialog::applyModel() if (selectedRow < 0 && !_currentVariable.empty() && variables[x][0] == _currentVariable){ selectedRow = x; - if (!_model->safeToAlterVariable(selectedRow)){ + if (_model->safeToAlterVariable(selectedRow)){ safeToAlter = true; } } From ab7aa635692f2c883965a4fdcce4baa8f2dc11f9 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 18 May 2024 16:31:09 -0400 Subject: [PATCH 213/466] Whoops --- qtfred/src/ui/dialogs/VariableDialog.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index a2928726b7a..6defd288a4f 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -793,7 +793,7 @@ void VariableDialog::onSaveVariableAsEternalCheckboxClicked() } // If the model returns the old status, then the change failed and we're out of sync. - if (ui->setVariableAsEternalcheckbox->isChecked() != _model->setVariableEternalFlag(row, ui->setVariableAsEternalcheckbox->isChecked())) { + if (ui->setVariableAsEternalcheckbox->isChecked() == _model->setVariableEternalFlag(row, ui->setVariableAsEternalcheckbox->isChecked())) { applyModel(); } else { ui->setVariableAsEternalcheckbox->setChecked(!ui->setVariableAsEternalcheckbox->isChecked()); @@ -809,7 +809,7 @@ void VariableDialog::onNetworkVariableCheckboxClicked() } // If the model returns the old status, then the change failed and we're out of sync. - if (ui->networkVariableCheckbox->isChecked() != _model->setVariableNetworkStatus(row, ui->networkVariableCheckbox->isChecked())) { + if (ui->networkVariableCheckbox->isChecked() == _model->setVariableNetworkStatus(row, ui->networkVariableCheckbox->isChecked())) { applyModel(); } else { ui->networkVariableCheckbox->setChecked(!ui->networkVariableCheckbox->isChecked()); From 29fa8c4f9ee16bb026054a8d6e3a490a03812036 Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Sat, 18 May 2024 23:26:21 -0400 Subject: [PATCH 214/466] Final Fixes Implements container contents reading, as well --- .../mission/dialogs/VariableDialogModel.cpp | 49 +++++++++++++++++-- qtfred/src/ui/dialogs/VariableDialog.cpp | 4 +- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index dd3a25dffbc..94d824eff69 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -386,10 +386,47 @@ void VariableDialogModel::initializeData() } newContainer.list = container.is_list(); - } -} + if (any(container.type & ContainerType::LIST)) { + for (const auto& item : container.list_data){ + if (any(container.type & ContainerType::STRING_DATA)){ + newContainer.stringValues.push_back(item); + } else { + try { + newContainer.numberValues.push_back(std::stoi(item)); + } + catch (...){ + newContainer.numberValues.push_back(0); + } + } + } + } else { + if (any(container.type & ContainerType::STRING_KEYS)){ + newContainer.stringKeys = true; + } else { + newContainer.stringKeys = false; + } + for (const auto& item : container.map_data){ + newContainer.keys.push_back(item.first); + + if (any(container.type & ContainerType::STRING_DATA)){ + newContainer.stringValues.push_back(item.second); + newContainer.numberValues.push_back(0); + } else { + newContainer.stringValues.push_back(""); + + try{ + newContainer.numberValues.push_back(std::stoi(item.second)); + } + catch (...){ + newContainer.numberValues.push_back(0); + } + } + } + } + } +} // true on string, false on number bool VariableDialogModel::getVariableType(int index) @@ -747,7 +784,7 @@ bool VariableDialogModel::safeToAlterVariable(int index) // FIXME! until we can actually count references (via a SEXP backend), this is the best way to go. if (variable->originalName != ""){ - return false; + return true; } return true; @@ -1927,7 +1964,7 @@ bool VariableDialogModel::safeToAlterContainer(int index) // FIXME! Until there's a sexp backend, we can only check if we just created the container. if (container->originalName != ""){ - return false; + return true; } return true; @@ -2252,7 +2289,9 @@ void VariableDialogModel::sortMap(int index) // If the last item is a duplicate, that was checked on the previous iteration. if ((x >= static_cast(container->keys.size()) - 1) || container->keys[x] != container->keys[x + 1]){ y = 0; - } + } else { + ++y; + } } // TODO! Switch to Assertion after testing. diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 6defd288a4f..ef73dbd6682 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1296,7 +1296,7 @@ void VariableDialog::applyModel() } // do we need to switch the delete button to a restore button? - if (selectedRow > -1 && ui->containersTable->item(selectedRow, 2) && ui->containersTable->item(selectedRow, 2)->text().toStdString() == "Deleted") { + if (selectedRow > -1 && ui->containersTable->item(selectedRow, 2) && ui->containersTable->item(selectedRow, 2)->text().toStdString() == "To Be Deleted") { ui->deleteContainerButton->setText("Restore"); // We can't restore empty container names. @@ -1398,7 +1398,7 @@ void VariableDialog::updateVariableOptions(bool safeToAlter) ui->setVariableAsNumberRadio->setChecked(!string); // do we need to switch the delete button to a restore button? - if (ui->variablesTable->item(row, 2) && ui->variablesTable->item(row, 2)->text().toStdString() == "Deleted"){ + if (ui->variablesTable->item(row, 2) && ui->variablesTable->item(row, 2)->text().toStdString() == "To Be Deleted"){ ui->deleteVariableButton->setText("Restore"); // We can't restore empty variable names. From 23254c993eaa0d502760f68c8dc6a8450f7a221a Mon Sep 17 00:00:00 2001 From: JohnAFernandez Date: Wed, 22 May 2024 01:09:14 -0400 Subject: [PATCH 215/466] Allow the loadout dialog to open varaible dialog --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 3 +-- qtfred/src/ui/FredView.h | 4 +++- qtfred/src/ui/dialogs/LoadoutDialog.cpp | 3 +-- qtfred/ui/LoadoutDialog.ui | 3 +++ qtfred/ui/VariableDialog.ui | 3 +++ 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 94d824eff69..4d269d07ab2 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -2294,8 +2294,7 @@ void VariableDialogModel::sortMap(int index) } } - // TODO! Switch to Assertion after testing. - Verification(container->keys.size() == sortedStringValues.size(), "Keys size %zu and values %zu have a size mismatch after sorting. Please report to the SCP.", container->keys.size(), sortedStringValues.size()); + Assertion(container->keys.size() == sortedStringValues.size(), "Keys size %zu and values %zu have a size mismatch after sorting. Please report to the SCP.", container->keys.size(), sortedStringValues.size()); container->stringValues = std::move(sortedStringValues); container->numberValues = std::move(sortedNumberValues); } diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index d63095f02dd..a5b39b5dba2 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -43,6 +43,9 @@ class FredView: public QMainWindow, public IDialogProvider { void newMission(); + // this can be triggered by the loadout dialog and so needs to be public + void on_actionVariables_triggered(bool); + private slots: void on_actionSave_As_triggered(bool); void on_actionSave_triggered(bool); @@ -91,7 +94,6 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionCommand_Briefing_triggered(bool); void on_actionReinforcements_triggered(bool); void on_actionLoadout_triggered(bool); - void on_actionVariables_triggered(bool); void on_actionSelectionLock_triggered(bool enabled); diff --git a/qtfred/src/ui/dialogs/LoadoutDialog.cpp b/qtfred/src/ui/dialogs/LoadoutDialog.cpp index 83678409d45..6e9b2a2945b 100644 --- a/qtfred/src/ui/dialogs/LoadoutDialog.cpp +++ b/qtfred/src/ui/dialogs/LoadoutDialog.cpp @@ -516,8 +516,7 @@ void LoadoutDialog::onClearAllUsedWeaponsPressed() void LoadoutDialog::openEditVariablePressed() { - //TODO! FIX ME! - //viewport->on_actionVariables_triggered(); + reinterpret_cast(parent())->on_actionVariables_triggered(true); } void LoadoutDialog::onSelectionRequiredPressed() diff --git a/qtfred/ui/LoadoutDialog.ui b/qtfred/ui/LoadoutDialog.ui index a454de8ee09..1e8477915e2 100644 --- a/qtfred/ui/LoadoutDialog.ui +++ b/qtfred/ui/LoadoutDialog.ui @@ -2,6 +2,9 @@ fso::fred::dialogs::LoadoutDialog + + Qt::WindowModal + true diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui index 2545a683016..6551fb54423 100644 --- a/qtfred/ui/VariableDialog.ui +++ b/qtfred/ui/VariableDialog.ui @@ -2,6 +2,9 @@ fso::fred::dialogs::VariableEditorDialog + + Qt::WindowModal + 0 From 4cfcb70381f8ef9d4614adeca0b94d8449783d14 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 9 Mar 2025 20:33:50 -0400 Subject: [PATCH 216/466] fix compilation Fix compilation due to upstream changes. --- code/mission/import/xwingmissionparse.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 39b23d0e371..194aef6c943 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -466,18 +466,18 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh wingp->arrival_cue = arrival_cue; wingp->arrival_delay = fg->arrivalDelay; - wingp->arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; + wingp->arrival_location = fg->arriveByHyperspace ? ArrivalLocation::AT_LOCATION : ArrivalLocation::FROM_DOCK_BAY; wingp->arrival_anchor = xwi_determine_anchor(xwim, fg); wingp->departure_cue = Locked_sexp_false; - wingp->departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; + wingp->departure_location = fg->departByHyperspace ? DepartureLocation::AT_LOCATION : DepartureLocation::TO_DOCK_BAY; wingp->departure_anchor = wingp->arrival_anchor; // if a wing doesn't have an anchor, make sure it is at-location // (flight groups present at mission start will have arriveByHyperspace set to false) if (wingp->arrival_anchor < 0) - wingp->arrival_location = ARRIVE_AT_LOCATION; + wingp->arrival_location = ArrivalLocation::AT_LOCATION; if (wingp->departure_anchor < 0) - wingp->departure_location = DEPART_AT_LOCATION; + wingp->departure_location = DepartureLocation::AT_LOCATION; wingp->wave_count = number_in_wave; } @@ -557,18 +557,18 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh pobj.arrival_cue = arrival_cue; pobj.arrival_delay = fg->arrivalDelay; - pobj.arrival_location = fg->arriveByHyperspace ? ARRIVE_AT_LOCATION : ARRIVE_FROM_DOCK_BAY; + pobj.arrival_location = fg->arriveByHyperspace ? ArrivalLocation::AT_LOCATION : ArrivalLocation::FROM_DOCK_BAY; pobj.arrival_anchor = xwi_determine_anchor(xwim, fg); pobj.departure_cue = Locked_sexp_false; - pobj.departure_location = fg->departByHyperspace ? DEPART_AT_LOCATION : DEPART_AT_DOCK_BAY; + pobj.departure_location = fg->departByHyperspace ? DepartureLocation::AT_LOCATION : DepartureLocation::TO_DOCK_BAY; pobj.departure_anchor = pobj.arrival_anchor; // if a ship doesn't have an anchor, make sure it is at-location // (flight groups present at mission start will have arriveByHyperspace set to false) if (pobj.arrival_anchor < 0) - pobj.arrival_location = ARRIVE_AT_LOCATION; + pobj.arrival_location = ArrivalLocation::AT_LOCATION; if (pobj.departure_anchor < 0) - pobj.departure_location = DEPART_AT_LOCATION; + pobj.departure_location = DepartureLocation::AT_LOCATION; } pobj.ship_class = ship_class; @@ -601,7 +601,7 @@ void parse_xwi_flightgroup(mission *pm, const XWingMission *xwim, const XWMFligh // undo any previously set player if (Player_starts > 0) { - auto prev_player_pobjp = mission_parse_get_parse_object(Player_start_shipname); + auto prev_player_pobjp = mission_parse_find_parse_object(Player_start_shipname); if (prev_player_pobjp) { Warning(LOCATION, "This mission specifies multiple player starting ships. Skipping %s.", Player_start_shipname); @@ -850,9 +850,9 @@ void parse_xwi_objectgroup(mission *pm, const XWingMission *xwim, const XWMObjec pobj.ship_class = ship_class; pobj.arrival_cue = Locked_sexp_true; - pobj.arrival_location = ARRIVE_AT_LOCATION; + pobj.arrival_location = ArrivalLocation::AT_LOCATION; pobj.departure_cue = Locked_sexp_false; - pobj.departure_location = DEPART_AT_LOCATION; + pobj.departure_location = DepartureLocation::AT_LOCATION; pobj.ai_class = sip->ai_class; pobj.warpin_params_index = sip->warpin_params_index; From de8368be71d28b294cf3b6c121d1f8b1ea7f77d0 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Thu, 10 Apr 2025 10:56:17 -0400 Subject: [PATCH 217/466] sync XWI names to FotG master names --- code/mission/import/xwingmissionparse.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp index 194aef6c943..0ac24dd566e 100644 --- a/code/mission/import/xwingmissionparse.cpp +++ b/code/mission/import/xwingmissionparse.cpp @@ -249,25 +249,25 @@ const char *xwi_determine_base_ship_class(const XWMFlightGroup *fg) case XWMFlightGroupType::fg_Transport: return "DX-9 Stormtrooper Transport"; case XWMFlightGroupType::fg_Shuttle: - return "Lambda-class T-4a Shuttle"; + return "Lambda T-4a Shuttle"; case XWMFlightGroupType::fg_Tug: - return "DV-3 Cargo Freighter"; + return "DV-3 Freighter"; case XWMFlightGroupType::fg_Container: return "BFF-1 Container"; case XWMFlightGroupType::fg_Freighter: - return "BFF-1 Bulk Freighter"; + return "BFF-1 Freighter"; case XWMFlightGroupType::fg_Calamari_Cruiser: return "Liberty Type Star Cruiser"; case XWMFlightGroupType::fg_Nebulon_B_Frigate: - return "EF76 Nebulon-B Escort Frigate"; + return "Nebulon-B Frigate"; case XWMFlightGroupType::fg_Corellian_Corvette: - return "CR90 Corvette"; + return "CR90 Corvette#Reb"; case XWMFlightGroupType::fg_Imperial_Star_Destroyer: return "Imperial Star Destroyer"; case XWMFlightGroupType::fg_TIE_Advanced: return nullptr; case XWMFlightGroupType::fg_B_Wing: - return "B-wing Starfighter"; + return "ASF-01 B-wing"; default: break; } From 3a380a05c9f47f8895b09066851784283a4b16f2 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 1 Jul 2025 23:17:55 -0400 Subject: [PATCH 218/466] change this WarningEx to a Warning I just finished helping a modder debug his mod, and it would have saved him a lot of time and trouble if this WarningEx had been a Warning. --- code/graphics/software/font.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/graphics/software/font.cpp b/code/graphics/software/font.cpp index 5b4b5e5ec15..ca8b0d9b6c7 100644 --- a/code/graphics/software/font.cpp +++ b/code/graphics/software/font.cpp @@ -466,7 +466,7 @@ namespace { case VFNT_FONT: if (Unicode_text_mode) { - WarningEx(LOCATION, "Bitmap fonts are not supported in Unicode text mode! Font %s will be ignored.", fontName.c_str()); + Warning(LOCATION, "Bitmap fonts are not supported in Unicode text mode! Font %s will be ignored.", fontName.c_str()); skip_to_start_of_string_one_of({"$TrueType:", "$Font:", "#End"}); } else { parse_vfnt_font(fontName); From 87021dd1d2ce1b85dc94e18e96b0d566a246b897 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 2 Jul 2025 01:39:51 -0400 Subject: [PATCH 219/466] make script-eval-num and script-eval-bool multiline These sexp operators can be enhanced to allow multiline scripts while retaining backwards compatibility, so do so. --- code/parse/sexp.cpp | 58 ++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 5c069ade1e5..909ee74f813 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -364,8 +364,8 @@ SCP_vector Operators = { { "map-has-data-item", OP_MAP_HAS_DATA_ITEM, 2, 3, SEXP_INTEGER_OPERATOR, }, // Karajorma //Other Sub-Category - { "script-eval-bool", OP_SCRIPT_EVAL_BOOL, 1, 1, SEXP_BOOLEAN_OPERATOR, }, - { "script-eval-num", OP_SCRIPT_EVAL_NUM, 1, 1, SEXP_INTEGER_OPERATOR, }, + { "script-eval-bool", OP_SCRIPT_EVAL_BOOL, 1, INT_MAX, SEXP_BOOLEAN_OPERATOR, }, + { "script-eval-num", OP_SCRIPT_EVAL_NUM, 1, INT_MAX, SEXP_INTEGER_OPERATOR, }, //Time Category { "time-ship-destroyed", OP_TIME_SHIP_DESTROYED, 1, 1, SEXP_INTEGER_OPERATOR, }, @@ -26764,27 +26764,35 @@ int sexp_script_eval(int node, int return_type, bool concat_args = false) switch(return_type) { case OPR_BOOL: - { - auto s = CTEXT(n); - bool r = false; - bool success = Script_system.EvalStringWithReturn(s, "|b", &r); + { + SCP_string script_cmd; + for (; n != -1; n = CDR(n)) + script_cmd.append(CTEXT(n)); - if(!success) - Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", s); + bool r = false; + bool success = Script_system.EvalStringWithReturn(script_cmd.c_str(), "|b", &r); + + if (!success) + Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", script_cmd.c_str()); + + return r ? SEXP_TRUE : SEXP_FALSE; + } - return r ? SEXP_TRUE : SEXP_FALSE; - } case OPR_NUMBER: - { - auto s = CTEXT(n); - int r = -1; - bool success = Script_system.EvalStringWithReturn(s, "|i", &r); + { + SCP_string script_cmd; + for (; n != -1; n = CDR(n)) + script_cmd.append(CTEXT(n)); - if(!success) - Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", s); + int r = -1; + bool success = Script_system.EvalStringWithReturn(script_cmd.c_str(), "|i", &r); + + if (!success) + Warning(LOCATION, "sexp-script-eval failed to evaluate string \"%s\"; check your syntax", script_cmd.c_str()); + + return r; + } - return r; - } case OPR_STRING: { const char* ret = nullptr; @@ -26827,7 +26835,7 @@ int sexp_script_eval(int node, int return_type, bool concat_args = false) if (concat_args) { - script_cmd.append(CTEXT(n)); + script_cmd.append(s); } else { @@ -41939,15 +41947,15 @@ SCP_vector Sexp_help = { }, {OP_SCRIPT_EVAL_BOOL, "script-eval-bool\r\n" - "\tEvaluates script to return a boolean" - "Takes 1 argument...\r\n" - "\t1:\tScript\r\n" + "\tEvaluates the concatenation of all arguments as a single script that returns a boolean" + "Takes at least 1 argument...\r\n" + "\tAll:\tScript\r\n" }, {OP_SCRIPT_EVAL_NUM, "script-eval-num\r\n" - "\tEvaluates script to return a number" - "Takes 1 argument...\r\n" - "\t1:\tScript\r\n" + "\tEvaluates the concatenation of all arguments as a single script that returns a number" + "Takes at least 1 argument...\r\n" + "\tAll:\tScript\r\n" }, { OP_DISABLE_ETS, "disable-ets\r\n" From 1b75fea06c7cd95a2af5fd1714c98a0113dcdd22 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 5 Jul 2025 13:15:43 -0400 Subject: [PATCH 220/466] subsystem scripting enhancements Add several API features to improve scripting support for subsystems. 1. A `CanonicalName` virtvar to provide a clear way to get subsystem names for indexing and referencing 2. `Submodel` and `SubmodelInstance` virtvars to get the submodel information associated with the subsystem, if it exists 3. A `getSubsystemList()` function to provide a subsystem iterator --- code/scripting/api/objs/model.cpp | 28 ++++++++++++++ code/scripting/api/objs/ship.cpp | 30 +++++++++++++++ code/scripting/api/objs/subsystem.cpp | 54 ++++++++++++++++++++++++++- code/ship/ship.cpp | 5 +++ code/ship/ship.h | 1 + 5 files changed, 116 insertions(+), 2 deletions(-) diff --git a/code/scripting/api/objs/model.cpp b/code/scripting/api/objs/model.cpp index 67d3f2e747b..da7910953dc 100644 --- a/code/scripting/api/objs/model.cpp +++ b/code/scripting/api/objs/model.cpp @@ -63,6 +63,34 @@ bool submodel_h::isValid() const } +ADE_FUNC(__eq, l_Model, "model, model", "Checks if two model handles refer to the same model", "boolean", "True if models are equal") +{ + model_h* mdl1; + model_h* mdl2; + + if (!ade_get_args(L, "oo", l_Model.GetPtr(&mdl1), l_Model.GetPtr(&mdl2))) + return ADE_RETURN_NIL; + + if (mdl1->GetID() == mdl2->GetID()) + return ADE_RETURN_TRUE; + + return ADE_RETURN_FALSE; +} + +ADE_FUNC(__eq, l_Submodel, "submodel, submodel", "Checks if two submodel handles refer to the same submodel", "boolean", "True if submodels are equal") +{ + submodel_h* smh1; + submodel_h* smh2; + + if (!ade_get_args(L, "oo", l_Submodel.GetPtr(&smh1), l_Submodel.GetPtr(&smh2))) + return ADE_RETURN_NIL; + + if (smh1->GetModelID() == smh2->GetModelID() && smh1->GetSubmodelIndex() == smh2->GetSubmodelIndex()) + return ADE_RETURN_TRUE; + + return ADE_RETURN_FALSE; +} + ADE_VIRTVAR(Submodels, l_Model, nullptr, "Model submodels", "submodels", "Model submodels, or an invalid submodels handle if the model handle is invalid") { model_h *mdl = nullptr; diff --git a/code/scripting/api/objs/ship.cpp b/code/scripting/api/objs/ship.cpp index 285a04abda6..17d588ad124 100644 --- a/code/scripting/api/objs/ship.cpp +++ b/code/scripting/api/objs/ship.cpp @@ -94,6 +94,36 @@ ADE_FUNC(__len, l_Ship, NULL, "Number of subsystems on ship", "number", "Subsyst return ade_set_args(L, "i", ship_get_num_subsys(&Ships[objh->objp()->instance])); } +ADE_FUNC(getSubsystemList, + l_Ship, + nullptr, + "Get the list of subsystems on this ship", + "iterator", + "An iterator across all subsystems on the ship. Can be used in a for .. in loop. Is not valid for more than one frame.") +{ + object_h* objh; + if (!ade_get_args(L, "o", l_Ship.GetPtr(&objh))) + return ADE_RETURN_NIL; + + if (!objh->isValid()) + return ADE_RETURN_NIL; + + ship* shipp = &Ships[objh->objp()->instance]; + ship_subsys* ss = &shipp->subsys_list; + + return ade_set_args(L, "u", luacpp::LuaFunction::createFromStdFunction(L, [shipp, ss](lua_State* LInner, const luacpp::LuaValueList& /*params*/) mutable -> luacpp::LuaValueList { + //Since the first element of a list is the next element from the head, and we start this function with the captured "ss" object being the head, this GET_NEXT will return the first element on first call of this lambda. + //Similarly, an empty list is defined by the head's next element being itself, hence an empty list will immediately return nil just fine + ss = GET_NEXT(ss); + + if (ss == END_OF_LIST(&shipp->subsys_list) || ss == nullptr) { + return luacpp::LuaValueList{ luacpp::LuaValue::createNil(LInner) }; + } + + return luacpp::LuaValueList{ luacpp::LuaValue::createValue(LInner, l_Subsystem.Set(ship_subsys_h(&Objects[shipp->objnum], ss))) }; + })); +} + ADE_FUNC(setFlag, l_Ship, "boolean set_it, string flag_name", "Sets or clears one or more flags - this function can accept an arbitrary number of flag arguments. The flag names can be any string that the alter-ship-flag SEXP operator supports.", nullptr, "Returns nothing") { object_h *objh; diff --git a/code/scripting/api/objs/subsystem.cpp b/code/scripting/api/objs/subsystem.cpp index 74f02145a11..02bacbe6083 100644 --- a/code/scripting/api/objs/subsystem.cpp +++ b/code/scripting/api/objs/subsystem.cpp @@ -2,6 +2,7 @@ // #include "subsystem.h" +#include "model.h" #include "model_path.h" #include "object.h" #include "ship.h" @@ -9,6 +10,7 @@ #include "vecmath.h" #include "hud/hudtarget.h" #include "ship/shiphit.h" +#include "modelinstance.h" #include "network/multi.h" #include "network/multimsgs.h" @@ -121,6 +123,38 @@ ADE_VIRTVAR(AWACSRadius, l_Subsystem, "number", "Subsystem AWACS radius", "numbe return ade_set_args(L, "f", sso->ss->awacs_radius); } +ADE_VIRTVAR(Submodel, l_Subsystem, "submodel", "The submodel corresponding to this subsystem, if one exists", "submodel", "Submodel handle, or invalid submodel handle if this subsystem does not have a submodel, or if the subsystem handle is invalid") +{ + ship_subsys_h *sso; + if (!ade_get_args(L, "o", l_Subsystem.GetPtr(&sso))) + return ade_set_error(L, "o", l_Submodel.Set(submodel_h())); + + if (!sso->isValid()) + return ade_set_error(L, "o", l_Submodel.Set(submodel_h())); + + if (ADE_SETTING_VAR) + LuaError(L, "Setting the Submodel is not allowed!"); + + return ade_set_args(L, "o", l_Submodel.Set(submodel_h(sso->ss->system_info->model_num, sso->ss->system_info->subobj_num))); +} + +ADE_VIRTVAR(SubmodelInstance, l_Subsystem, "submodel_instance", "The submodel instance corresponding to this subsystem, if one exists", "submodel_instance", "Submodel instance handle, or invalid submodel instance handle if this subsystem does not have a submodel instance, or if the subsystem handle is invalid") +{ + ship_subsys_h *sso; + if (!ade_get_args(L, "o", l_Subsystem.GetPtr(&sso))) + return ade_set_error(L, "o", l_SubmodelInstance.Set(submodelinstance_h())); + + if (!sso->isValid()) + return ade_set_error(L, "o", l_SubmodelInstance.Set(submodelinstance_h())); + + if (ADE_SETTING_VAR) + LuaError(L, "Setting the SubmodelInstance is not allowed!"); + + auto shipp = &Ships[sso->objh.objp()->instance]; + auto pmi = model_get_instance(shipp->model_instance_num); + return ade_set_args(L, "o", l_SubmodelInstance.Set(submodelinstance_h(pmi, sso->ss->system_info->subobj_num))); +} + ADE_VIRTVAR(Orientation, l_Subsystem, "orientation", "Orientation of subobject or turret base", "orientation", "Subsystem orientation, or identity orientation if handle is invalid") { ship_subsys_h *sso; @@ -345,6 +379,22 @@ ADE_VIRTVAR(NameOnHUD, l_Subsystem, "string", "Subsystem name as it would be dis return ade_set_args(L, "s", ship_subsys_get_name_on_hud(sso->ss)); } +ADE_VIRTVAR(CanonicalName, l_Subsystem, "string", "Canonical subsystem name that can be used to reference this subsystem in a SEXP or script", "string", "Canonical subsystem name, or an empty string if handle is invalid") +{ + ship_subsys_h *sso; + + if (!ade_get_args(L, "o", l_Subsystem.GetPtr(&sso))) + return ade_set_error(L, "s", ""); + + if (!sso->isValid()) + return ade_set_error(L, "s", ""); + + if (ADE_SETTING_VAR) + LuaError(L, "Setting the CanonicalName is not allowed!"); + + return ade_set_args(L, "s", ship_subsys_get_canonical_name(sso->ss)); +} + ADE_VIRTVAR(NumFirePoints, l_Subsystem, "number", "Number of firepoints", "number", "Number of fire points, or 0 if handle is invalid") { ship_subsys_h* sso; @@ -384,7 +434,7 @@ ADE_VIRTVAR(FireRateMultiplier, l_Subsystem, "number", "Factor by which turret's return ade_set_args(L, "f", sso->ss->rof_scaler); } -ADE_FUNC(getModelName, l_Subsystem, NULL, "Returns the original name of the subsystem in the model file", "string", "name or empty string on error") +ADE_FUNC(getModelName, l_Subsystem, nullptr, "Returns the original name of the subsystem as defined in the ship class, which could possibly correspond to a submodel in the model file. This is the same as CanonicalName.", "string", "name or empty string on error") { ship_subsys_h *sso; if(!ade_get_args(L, "o", l_Subsystem.GetPtr(&sso))) @@ -393,7 +443,7 @@ ADE_FUNC(getModelName, l_Subsystem, NULL, "Returns the original name of the subs if (!sso->isValid()) return ade_set_error(L, "s", ""); - return ade_set_args(L, "s", sso->ss->system_info->subobj_name); + return ade_set_args(L, "s", ship_subsys_get_canonical_name(sso->ss)); } ADE_VIRTVAR(PrimaryBanks, l_Subsystem, "weaponbanktype", "Array of primary weapon banks", "weaponbanktype", "Primary banks, or invalid weaponbanktype handle if subsystem handle is invalid") diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index f5ff68c9c85..d3d451ce839 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -17189,6 +17189,11 @@ const char *ship_subsys_get_name_on_hud(const ship_subsys *ss) return ship_subsys_get_name(ss); } +const char *ship_subsys_get_canonical_name(const ship_subsys *ss) +{ + return ss->system_info->subobj_name; +} + /** * Return the shield strength of the specified quadrant on hit_objp * diff --git a/code/ship/ship.h b/code/ship/ship.h index db458ee83b9..aece66879a3 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1803,6 +1803,7 @@ bool ship_subsys_has_instance_name(const ship_subsys *ss); void ship_subsys_set_name(ship_subsys* ss, const char* n_name); const char *ship_subsys_get_name_on_hud(const ship_subsys *ss); +const char *ship_subsys_get_canonical_name(const ship_subsys *ss); // subsys disruption extern int ship_subsys_disrupted(const ship_subsys *ss); From 8dc03b0fffb61edabd65ced9424bfa6258e94080 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 5 Jul 2025 14:11:42 -0400 Subject: [PATCH 221/466] additional submodel scripting enhancements 4. `__eq` functions for models and submodels 5. Store model and submodel IDs, not pointers --- code/model/model.h | 1 + code/model/modelread.cpp | 5 ++ code/scripting/api/objs/model.cpp | 6 +- code/scripting/api/objs/modelinstance.cpp | 103 ++++++++++++++++------ code/scripting/api/objs/modelinstance.h | 24 +++-- 5 files changed, 102 insertions(+), 37 deletions(-) diff --git a/code/model/model.h b/code/model/model.h index fb5d0efe99f..846b3f656a3 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -1012,6 +1012,7 @@ SCP_set model_get_textures_used(const polymodel* pm, int submodel); // Returns a pointer to the polymodel structure for model 'n' polymodel *model_get(int model_num); +int num_model_instances(); polymodel_instance* model_get_instance(int model_instance_num); // routine to copy subsystems. Must be called when subsystems sets are the same -- see ship.cpp diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index f16aa16461d..61deb09a7f3 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -3895,6 +3895,11 @@ polymodel * model_get(int model_num) return Polygon_models[num]; } +int num_model_instances() +{ + return static_cast(Polygon_model_instances.size()); +} + polymodel_instance* model_get_instance(int model_instance_num) { Assert( model_instance_num >= 0 ); diff --git a/code/scripting/api/objs/model.cpp b/code/scripting/api/objs/model.cpp index da7910953dc..e192610667d 100644 --- a/code/scripting/api/objs/model.cpp +++ b/code/scripting/api/objs/model.cpp @@ -23,7 +23,7 @@ int model_h::GetID() const } bool model_h::isValid() const { - return (model_num >= 0) && (model_get(model_num) != nullptr); + return (model_num >= 0) && (model_get(model_num) != nullptr); // note: the model ID can exceed MAX_POLYGON_MODELS because the modulo is taken } model_h::model_h(int n_modelnum) : model_num(n_modelnum) @@ -56,8 +56,8 @@ bool submodel_h::isValid() const if (model_num >= 0 && submodel_num >= 0) { auto model = model_get(model_num); - if (model != nullptr) - return submodel_num < model->n_models; + if (model != nullptr && submodel_num < model->n_models) + return true; } return false; } diff --git a/code/scripting/api/objs/modelinstance.cpp b/code/scripting/api/objs/modelinstance.cpp index 7722caf2fc1..49fa843a0a3 100644 --- a/code/scripting/api/objs/modelinstance.cpp +++ b/code/scripting/api/objs/modelinstance.cpp @@ -111,65 +111,118 @@ ADE_OBJ(l_ModelInstance, modelinstance_h, "model_instance", "Model instance hand modelinstance_h::modelinstance_h(int pmi_id) { - _pmi = model_get_instance(pmi_id); + _pmi_id = pmi_id; } modelinstance_h::modelinstance_h(polymodel_instance *pmi) - : _pmi(pmi) + : _pmi_id(pmi ? pmi->id : -1) {} modelinstance_h::modelinstance_h() - : _pmi(nullptr) + : _pmi_id(-1) {} -polymodel_instance *modelinstance_h::Get() +polymodel_instance *modelinstance_h::Get() const { - return _pmi; + return isValid() ? model_get_instance(_pmi_id) : nullptr; +} +int modelinstance_h::GetID() const +{ + return isValid() ? _pmi_id : -1; +} +polymodel *modelinstance_h::GetModel() const +{ + return isValid() ? model_get(model_get_instance(_pmi_id)->model_num) : nullptr; +} +int modelinstance_h::GetModelID() const +{ + return isValid() ? model_get_instance(_pmi_id)->model_num : -1; } bool modelinstance_h::isValid() const { - return (_pmi != nullptr); + return (_pmi_id >= 0) && (_pmi_id < num_model_instances()) && (model_get_instance(_pmi_id) != nullptr); } ADE_OBJ(l_SubmodelInstance, submodelinstance_h, "submodel_instance", "Submodel instance handle"); submodelinstance_h::submodelinstance_h(int pmi_id, int submodel_num) - : _submodel_num(submodel_num) + : _pmi_id(pmi_id), _submodel_num(submodel_num) +{} +submodelinstance_h::submodelinstance_h(polymodel_instance *pmi, int submodel_num) + : _pmi_id(pmi ? pmi->id : -1), _submodel_num(submodel_num) +{} +submodelinstance_h::submodelinstance_h() + : _pmi_id(-1), _submodel_num(-1) +{} +polymodel_instance *submodelinstance_h::GetModelInstance() const { - _pmi = model_get_instance(pmi_id); - _pm = _pmi ? model_get(_pmi->model_num) : nullptr; + return isValid() ? model_get_instance(_pmi_id) : nullptr; } -submodelinstance_h::submodelinstance_h(polymodel_instance *pmi, int submodel_num) - : _pmi(pmi), _submodel_num(submodel_num) +int submodelinstance_h::GetModelInstanceID() const { - _pm = pmi ? model_get(pmi->model_num) : nullptr; + return isValid() ? _pmi_id : -1; } -submodelinstance_h::submodelinstance_h() - : _pmi(nullptr), _pm(nullptr), _submodel_num(-1) -{} -polymodel_instance *submodelinstance_h::GetModelInstance() +submodel_instance *submodelinstance_h::Get() const { - return isValid() ? _pmi : nullptr; + return isValid() ? &model_get_instance(_pmi_id)->submodel[_submodel_num] : nullptr; } -submodel_instance *submodelinstance_h::Get() +polymodel *submodelinstance_h::GetModel() const { - return isValid() ? &_pmi->submodel[_submodel_num] : nullptr; + return isValid() ? model_get(model_get_instance(_pmi_id)->model_num) : nullptr; } -polymodel *submodelinstance_h::GetModel() +int submodelinstance_h::GetModelID() const { - return isValid() ? _pm : nullptr; + return isValid() ? model_get_instance(_pmi_id)->model_num : -1; } -bsp_info *submodelinstance_h::GetSubmodel() +bsp_info *submodelinstance_h::GetSubmodel() const { - return isValid() ? &_pm->submodel[_submodel_num] : nullptr; + return isValid() ? &model_get(model_get_instance(_pmi_id)->model_num)->submodel[_submodel_num] : nullptr; } -int submodelinstance_h::GetSubmodelIndex() +int submodelinstance_h::GetSubmodelIndex() const { return isValid() ? _submodel_num : -1; } bool submodelinstance_h::isValid() const { - return _pmi != nullptr && _pm != nullptr && _submodel_num >= 0 && _submodel_num < _pm->n_models; + if (_pmi_id >= 0 && _submodel_num >= 0 && _pmi_id < num_model_instances()) + { + auto pmi = model_get_instance(_pmi_id); + if (pmi != nullptr && pmi->model_num >= 0) + { + auto pm = model_get(pmi->model_num); + if (pm != nullptr && _submodel_num < pm->n_models) + return true; + } + } + return false; +} + + +ADE_FUNC(__eq, l_ModelInstance, "model_instance, model_instance", "Checks if two model instance handles refer to the same model instance", "boolean", "True if model instances are equal") +{ + modelinstance_h* mih1; + modelinstance_h* mih2; + + if (!ade_get_args(L, "oo", l_ModelInstance.GetPtr(&mih1), l_ModelInstance.GetPtr(&mih2))) + return ADE_RETURN_NIL; + + if (mih1->GetID() == mih2->GetID()) + return ADE_RETURN_TRUE; + + return ADE_RETURN_FALSE; } +ADE_FUNC(__eq, l_SubmodelInstance, "submodel_instance, submodel_instance", "Checks if two submodel instance handles refer to the same submodel instance", "boolean", "True if submodel instances are equal") +{ + submodelinstance_h* smih1; + submodelinstance_h* smih2; + + if (!ade_get_args(L, "oo", l_SubmodelInstance.GetPtr(&smih1), l_SubmodelInstance.GetPtr(&smih2))) + return ADE_RETURN_NIL; + + if (smih1->GetModelInstanceID() == smih2->GetModelInstanceID() && smih1->GetSubmodelIndex() == smih2->GetSubmodelIndex()) + return ADE_RETURN_TRUE; + + return ADE_RETURN_FALSE; +} ADE_FUNC(getModel, l_ModelInstance, nullptr, "Returns the model used by this instance", "model", "A model") { diff --git a/code/scripting/api/objs/modelinstance.h b/code/scripting/api/objs/modelinstance.h index a8a2d6a92ca..d44d26813ba 100644 --- a/code/scripting/api/objs/modelinstance.h +++ b/code/scripting/api/objs/modelinstance.h @@ -10,14 +10,18 @@ namespace api { class modelinstance_h { protected: - polymodel_instance *_pmi; + int _pmi_id; public: explicit modelinstance_h(int pmi_id); explicit modelinstance_h(polymodel_instance *pmi); modelinstance_h(); - polymodel_instance *Get(); + polymodel_instance *Get() const; + int GetID() const; + + polymodel *GetModel() const; + int GetModelID() const; bool isValid() const; }; @@ -27,8 +31,7 @@ DECLARE_ADE_OBJ(l_ModelInstance, modelinstance_h); class submodelinstance_h { protected: - polymodel_instance *_pmi; - polymodel *_pm; + int _pmi_id; int _submodel_num; public: @@ -36,12 +39,15 @@ class submodelinstance_h explicit submodelinstance_h(polymodel_instance *pmi, int submodel_num); submodelinstance_h(); - polymodel_instance *GetModelInstance(); - submodel_instance *Get(); + polymodel_instance *GetModelInstance() const; + int GetModelInstanceID() const; + + polymodel *GetModel() const; + int GetModelID() const; - polymodel *GetModel(); - bsp_info *GetSubmodel(); - int GetSubmodelIndex(); + submodel_instance *Get() const; + bsp_info *GetSubmodel() const; + int GetSubmodelIndex() const; bool isValid() const; }; From df1126923dead82875e7264ec8c972d3825f9bf2 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Mon, 30 Jun 2025 22:30:47 -0400 Subject: [PATCH 222/466] alphabetize accelerator keys, sort editor menu, sort ship dialog --- fred2/fred.rc | 197 +++++++++++++++++++------------------ fred2/shipeditordlg.cpp | 2 +- qtfred/src/ui/FredView.cpp | 4 +- qtfred/src/ui/FredView.h | 4 +- qtfred/ui/FredView.ui | 30 +++--- 5 files changed, 123 insertions(+), 114 deletions(-) diff --git a/fred2/fred.rc b/fred2/fred.rc index 6d8bfdeb3a8..2930fa9856d 100644 --- a/fred2/fred.rc +++ b/fred2/fred.rc @@ -324,24 +324,29 @@ BEGIN BEGIN MENUITEM "&Ships\tShift+S", 32799 MENUITEM "&Wings\tShift+W", 32955 - MENUITEM "Objects\tShift+O", 32973 + MENUITEM "&Objects\tShift+O", 32973 MENUITEM "Waypoint Paths\tShift+Y", 32979 - MENUITEM "Mission &Objectives\tShift+G", 32800 - MENUITEM "&Events\tShift+E", 32974 - MENUITEM "Team Loadout\tShift+P", 32972 - MENUITEM "Background\tShift+I", 32976 - MENUITEM "Reinforcements\tShift+R", 32977 - MENUITEM "Asteroid Field\tShift+A", 32984 + MENUITEM "Jump Nodes\tShift+J", ID_EDITORS_JUMPNODE + MENUITEM SEPARATOR MENUITEM "&Mission Specs\tShift+N", 32771 + MENUITEM "Mission &Goals\tShift+G", 32800 + MENUITEM "Mission &Events\tShift+E", 32974 + MENUITEM "Mission Cutscenes", ID_EDITORS_CUTSCENES + MENUITEM "&Voice Acting Manager", ID_EDITORS_VOICE + MENUITEM SEPARATOR + MENUITEM "&Fiction Viewer\tShift+F", ID_EDITORS_FICTION + MENUITEM "&Command Briefing\tShift+C", 33054 + MENUITEM "Team &Loadout\tShift+P", 32972 MENUITEM "&Briefing\tShift+B", 33006 MENUITEM "&Debriefing\tShift+D", 33007 - MENUITEM "Command Briefing\tShift+C", 33054 - MENUITEM "Fiction Viewer\tShift+F", ID_EDITORS_FICTION + MENUITEM SEPARATOR + MENUITEM "Background\tShift+I", 32976 + MENUITEM "Asteroid Field\tShift+A", 32984 MENUITEM "Volumetric Nebula", ID_EDITORS_VOLUMETRICS - MENUITEM "Mission Cutscenes", ID_EDITORS_CUTSCENES + MENUITEM SEPARATOR + MENUITEM "Reinforcements\tShift+R", 32977 MENUITEM "Shield System", 33033 MENUITEM "Set Global Ship Flags", 33073 - MENUITEM "Voice Acting Manager", ID_EDITORS_VOICE MENUITEM SEPARATOR MENUITEM "Campaign", 32986 END @@ -601,40 +606,57 @@ END IDR_MAINFRAME ACCELERATORS BEGIN - "L", ID_ALIGN_OBJ, VIRTKEY, CONTROL, NOINVERT "A", ID_ASTEROID_EDITOR, VIRTKEY, SHIFT, NOINVERT - "K", ID_CANCEL_SUBSYS, VIRTKEY, ALT, NOINVERT - VK_F1, ID_CONTEXT_HELP, VIRTKEY, SHIFT, NOINVERT - "T", ID_CONTROL_OBJ, VIRTKEY, NOINVERT - "D", ID_DISBAND_WING, VIRTKEY, CONTROL, NOINVERT - "D", ID_DUMP_STATS, VIRTKEY, SHIFT, CONTROL, NOINVERT - VK_DELETE, ID_EDIT_DELETE, VIRTKEY, NOINVERT - VK_DELETE, ID_EDIT_DELETE_WING, VIRTKEY, CONTROL, NOINVERT - "3", ID_EDIT_POPUP_SHOW_COMPASS, VIRTKEY, SHIFT, ALT, NOINVERT - "I", ID_EDIT_POPUP_SHOW_SHIP_ICONS, VIRTKEY, SHIFT, ALT, NOINVERT - "M", ID_EDIT_POPUP_SHOW_SHIP_MODELS, VIRTKEY, SHIFT, ALT, NOINVERT - "Z", ID_EDIT_UNDO, VIRTKEY, CONTROL, NOINVERT - "I", ID_EDITORS_BG_BITMAPS, VIRTKEY, SHIFT, NOINVERT "B", ID_EDITORS_BRIEFING, VIRTKEY, SHIFT, NOINVERT + "B", ID_SHOW_STARFRIELD, VIRTKEY, SHIFT, ALT, NOINVERT "C", ID_EDITORS_CMD_BRIEF, VIRTKEY, SHIFT, NOINVERT + "C", ID_SHOW_COORDINATES, VIRTKEY, SHIFT, ALT, NOINVERT "D", ID_EDITORS_DEBRIEFING, VIRTKEY, SHIFT, NOINVERT + "D", ID_SHOW_DISTANCES, VIRTKEY, NOINVERT + "D", ID_DISBAND_WING, VIRTKEY, CONTROL, NOINVERT + "D", ID_DUMP_STATS, VIRTKEY, SHIFT, CONTROL, NOINVERT "E", ID_EDITORS_EVENTS, VIRTKEY, SHIFT, NOINVERT "F", ID_EDITORS_FICTION, VIRTKEY, SHIFT, NOINVERT "G", ID_EDITORS_GOALS, VIRTKEY, SHIFT, NOINVERT - "O", ID_EDITORS_ORIENT, VIRTKEY, SHIFT, NOINVERT - "P", ID_EDITORS_PLAYER, VIRTKEY, SHIFT, NOINVERT - "R", ID_EDITORS_REINFORCEMENT, VIRTKEY, SHIFT, NOINVERT - "S", ID_EDITORS_SHIPS, VIRTKEY, SHIFT, NOINVERT - "Y", ID_EDITORS_WAYPOINT, VIRTKEY, SHIFT, NOINVERT - "J", ID_EDITORS_JUMPNODE, VIRTKEY, SHIFT, NOINVERT - "W", ID_EDITORS_WING, VIRTKEY, SHIFT, NOINVERT + "G", ID_VIEW_GRID, VIRTKEY, SHIFT, ALT, NOINVERT "H", ID_ERROR_CHECKER, VIRTKEY, SHIFT, NOINVERT - "N", ID_FILE_MISSIONNOTES, VIRTKEY, SHIFT, NOINVERT + "H", ID_SELECT_LIST, VIRTKEY, NOINVERT + "H", ID_SHOW_HORIZON, VIRTKEY, SHIFT, ALT, NOINVERT + "I", ID_EDIT_POPUP_SHOW_SHIP_ICONS, VIRTKEY, SHIFT, ALT, NOINVERT + "I", ID_EDITORS_BG_BITMAPS, VIRTKEY, SHIFT, NOINVERT + "J", ID_EDITORS_JUMPNODE, VIRTKEY, SHIFT, NOINVERT + "K", ID_CANCEL_SUBSYS, VIRTKEY, ALT, NOINVERT + "K", ID_NEXT_SUBSYS, VIRTKEY, NOINVERT + "K", ID_PREV_SUBSYS, VIRTKEY, SHIFT, NOINVERT + "L", ID_ALIGN_OBJ, VIRTKEY, CONTROL, NOINVERT + "L", ID_LEVEL_OBJ, VIRTKEY, NOINVERT + "M", ID_EDIT_POPUP_SHOW_SHIP_MODELS, VIRTKEY, SHIFT, ALT, NOINVERT + "M", ID_SELECT_AND_MOVE, VIRTKEY, NOINVERT "N", ID_FILE_NEW, VIRTKEY, CONTROL, NOINVERT + "N", ID_FILE_MISSIONNOTES, VIRTKEY, SHIFT, NOINVERT "O", ID_FILE_OPEN, VIRTKEY, CONTROL, NOINVERT + "O", ID_EDITORS_ORIENT, VIRTKEY, SHIFT, NOINVERT + "O", ID_VIEW_OUTLINES, VIRTKEY, SHIFT, ALT, NOINVERT + "P", ID_EDITORS_PLAYER, VIRTKEY, SHIFT, NOINVERT + "P", ID_SAVE_CAMERA, VIRTKEY, CONTROL, NOINVERT + "P", ID_SHOW_GRID_POSITIONS, VIRTKEY, SHIFT, ALT, NOINVERT + "R", ID_EDITORS_REINFORCEMENT, VIRTKEY, SHIFT, NOINVERT + "R", ID_LOOKAT_OBJ, VIRTKEY, ALT, NOINVERT + "R", ID_RESTORE_CAMERA, VIRTKEY, CONTROL, NOINVERT "S", ID_FILE_SAVE, VIRTKEY, CONTROL, NOINVERT "S", ID_FILE_SAVE_AS, VIRTKEY, SHIFT, CONTROL, NOINVERT + "S", ID_EDITORS_SHIPS, VIRTKEY, SHIFT, NOINVERT + "S", ID_SELECT, VIRTKEY, NOINVERT + "T", ID_CONTROL_OBJ, VIRTKEY, NOINVERT + "V", ID_TOGGLE_VIEWPOINT, VIRTKEY, SHIFT, NOINVERT + "W", ID_EDITORS_WING, VIRTKEY, SHIFT, NOINVERT "W", ID_FORM_WING, VIRTKEY, CONTROL, NOINVERT + "W", ID_MARK_WING, VIRTKEY, NOINVERT + "X", ID_ROTATE_LOCALLY, VIRTKEY, NOINVERT + "Y", ID_EDITORS_WAYPOINT, VIRTKEY, SHIFT, NOINVERT + "Z", ID_EDIT_UNDO, VIRTKEY, CONTROL, NOINVERT + "Z", ID_ZOOM_EXTENTS, VIRTKEY, SHIFT, NOINVERT + "Z", ID_ZOOM_SELECTED, VIRTKEY, ALT, NOINVERT "1", ID_GROUP1, VIRTKEY, CONTROL, NOINVERT "2", ID_GROUP2, VIRTKEY, CONTROL, NOINVERT "3", ID_GROUP3, VIRTKEY, CONTROL, NOINVERT @@ -644,45 +666,28 @@ BEGIN "7", ID_GROUP7, VIRTKEY, CONTROL, NOINVERT "8", ID_GROUP8, VIRTKEY, CONTROL, NOINVERT "9", ID_GROUP9, VIRTKEY, CONTROL, NOINVERT - "L", ID_LEVEL_OBJ, VIRTKEY, NOINVERT - "R", ID_LOOKAT_OBJ, VIRTKEY, ALT, NOINVERT - "W", ID_MARK_WING, VIRTKEY, NOINVERT - VK_SPACE, ID_MISC_STATISTICS, VIRTKEY, ALT, NOINVERT - VK_TAB, ID_NEXT_OBJ, VIRTKEY, NOINVERT - VK_F6, ID_NEXT_PANE, VIRTKEY, NOINVERT - "K", ID_NEXT_SUBSYS, VIRTKEY, NOINVERT - VK_TAB, ID_PREV_OBJ, VIRTKEY, CONTROL, NOINVERT - VK_F6, ID_PREV_PANE, VIRTKEY, SHIFT, NOINVERT - "K", ID_PREV_SUBSYS, VIRTKEY, SHIFT, NOINVERT - "R", ID_RESTORE_CAMERA, VIRTKEY, CONTROL, NOINVERT "1", ID_ROT1, VIRTKEY, SHIFT, NOINVERT "2", ID_ROT2, VIRTKEY, SHIFT, NOINVERT "3", ID_ROT3, VIRTKEY, SHIFT, NOINVERT "4", ID_ROT4, VIRTKEY, SHIFT, NOINVERT "5", ID_ROT5, VIRTKEY, SHIFT, NOINVERT - "X", ID_ROTATE_LOCALLY, VIRTKEY, NOINVERT - "P", ID_SAVE_CAMERA, VIRTKEY, CONTROL, NOINVERT - "S", ID_SELECT, VIRTKEY, NOINVERT - "M", ID_SELECT_AND_MOVE, VIRTKEY, NOINVERT - "H", ID_SELECT_LIST, VIRTKEY, NOINVERT - "C", ID_SHOW_COORDINATES, VIRTKEY, SHIFT, ALT, NOINVERT - "D", ID_SHOW_DISTANCES, VIRTKEY, NOINVERT - "P", ID_SHOW_GRID_POSITIONS, VIRTKEY, SHIFT, ALT, NOINVERT - "H", ID_SHOW_HORIZON, VIRTKEY, SHIFT, ALT, NOINVERT - "B", ID_SHOW_STARFRIELD, VIRTKEY, SHIFT, ALT, NOINVERT "1", ID_SPEED1, VIRTKEY, NOINVERT - "6", ID_SPEED10, VIRTKEY, NOINVERT - "8", ID_SPEED100, VIRTKEY, NOINVERT "2", ID_SPEED2, VIRTKEY, NOINVERT "3", ID_SPEED3, VIRTKEY, NOINVERT "4", ID_SPEED5, VIRTKEY, NOINVERT - "7", ID_SPEED50, VIRTKEY, NOINVERT "5", ID_SPEED8, VIRTKEY, NOINVERT - "V", ID_TOGGLE_VIEWPOINT, VIRTKEY, SHIFT, NOINVERT - "G", ID_VIEW_GRID, VIRTKEY, SHIFT, ALT, NOINVERT - "O", ID_VIEW_OUTLINES, VIRTKEY, SHIFT, ALT, NOINVERT - "Z", ID_ZOOM_EXTENTS, VIRTKEY, SHIFT, NOINVERT - "Z", ID_ZOOM_SELECTED, VIRTKEY, ALT, NOINVERT + "6", ID_SPEED10, VIRTKEY, NOINVERT + "7", ID_SPEED50, VIRTKEY, NOINVERT + "8", ID_SPEED100, VIRTKEY, NOINVERT + "3", ID_EDIT_POPUP_SHOW_COMPASS, VIRTKEY, SHIFT, ALT, NOINVERT + VK_DELETE, ID_EDIT_DELETE, VIRTKEY, NOINVERT + VK_DELETE, ID_EDIT_DELETE_WING, VIRTKEY, CONTROL, NOINVERT + VK_SPACE, ID_MISC_STATISTICS, VIRTKEY, ALT, NOINVERT + VK_TAB, ID_NEXT_OBJ, VIRTKEY, NOINVERT + VK_TAB, ID_PREV_OBJ, VIRTKEY, CONTROL, NOINVERT + VK_F1, ID_CONTEXT_HELP, VIRTKEY, SHIFT, NOINVERT + VK_F6, ID_NEXT_PANE, VIRTKEY, NOINVERT + VK_F6, ID_PREV_PANE, VIRTKEY, SHIFT, NOINVERT END IDR_ACC_CAMPAIGN ACCELERATORS @@ -1000,83 +1005,83 @@ FONT 8, "MS Sans Serif", 0, 0, 0x1 BEGIN PUSHBUTTON "Prev",IDC_PREV,264,7,24,14,0,WS_EX_STATICEDGE PUSHBUTTON "Next",IDC_NEXT,291,7,24,14,0,WS_EX_STATICEDGE + LTEXT "Ship Name",IDC_STATIC,7,10,36,8 EDITTEXT IDC_SHIP_NAME,47,7,94,14,ES_AUTOHSCROLL + LTEXT "Ship Class",IDC_STATIC,9,25,34,8 COMBOBOX IDC_SHIP_CLASS,47,23,94,207,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "AI Class",IDC_STATIC,17,40,26,8 COMBOBOX IDC_AI_CLASS,47,37,94,185,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Team",IDC_STATIC,24,54,19,8 COMBOBOX IDC_SHIP_TEAM,47,51,94,196,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Cargo",IDC_STATIC,23,68,20,8 COMBOBOX IDC_SHIP_CARGO1,47,65,94,258,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP + LTEXT "Alt Name",IDC_STATIC,14,82,30,8 COMBOBOX IDC_SHIP_ALT,47,79,94,125,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP + LTEXT "Callsign",IDC_STATIC,18,95,25,8 COMBOBOX IDC_SHIP_CALLSIGN,47,93,94,125,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP PUSHBUTTON "Texture Replacement",IDC_TEXTURES,47,110,94,14,0,WS_EX_STATICEDGE + RTEXT "Wing:",IDC_STATIC,165,13,20,8 + LTEXT "Static",IDC_WING,189,13,67,8 + LTEXT "Hotkey",IDC_STATIC,161,26,24,8 COMBOBOX IDC_HOTKEY,189,24,68,122,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Persona",IDC_STATIC,158,42,27,8 COMBOBOX IDC_SHIP_PERSONA,189,40,68,129,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Kill Score",IDC_STATIC,155,58,30,8 EDITTEXT IDC_SCORE,189,55,68,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Assist %",IDC_STATIC,158,75,27,8 + EDITTEXT IDC_ASSIST_SCORE,189,72,68,14,ES_AUTOHSCROLL | ES_NUMBER CONTROL "Player Ship",IDC_PLAYER_SHIP,"Button",BS_3STATE | WS_TABSTOP,189,88,51,10 PUSHBUTTON "Set As Player Ship",IDC_SET_AS_PLAYER_SHIP,188,98,69,14,0,WS_EX_STATICEDGE - PUSHBUTTON "Misc",IDC_FLAGS,47,132,50,14,0,WS_EX_STATICEDGE - PUSHBUTTON "Initial Status",IDC_INITIAL_STATUS,101,132,50,14,0,WS_EX_STATICEDGE - PUSHBUTTON "Initial Orders",IDC_GOALS,156,132,50,14,0,WS_EX_STATICEDGE - PUSHBUTTON "TBL Info",IDC_SHIP_TBL,210,132,50,14,0,WS_EX_STATICEDGE + PUSHBUTTON "Alt Ship Class",IDC_ALT_SHIP_CLASS,187,114,69,14,0,WS_EX_STATICEDGE PUSHBUTTON "Delete",IDC_DELETE_SHIP,265,23,50,14,0,WS_EX_STATICEDGE PUSHBUTTON "Reset",IDC_SHIP_RESET,265,39,50,14,BS_CENTER | BS_MULTILINE,WS_EX_STATICEDGE PUSHBUTTON "Weapons",IDC_WEAPONS,265,55,50,14,0,WS_EX_STATICEDGE PUSHBUTTON "Player Orders",IDC_IGNORE_ORDERS,265,71,50,14,0,WS_EX_STATICEDGE PUSHBUTTON "Special Exp",IDC_SPECIAL_EXP,265,87,50,14,0,WS_EX_STATICEDGE PUSHBUTTON "Special Hits",IDC_SPECIAL_HITPOINTS,265,103,50,14,0,WS_EX_STATICEDGE + PUSHBUTTON "Misc",IDC_FLAGS,47,132,50,14,0,WS_EX_STATICEDGE + PUSHBUTTON "Initial Status",IDC_INITIAL_STATUS,101,132,50,14,0,WS_EX_STATICEDGE + PUSHBUTTON "Initial Orders",IDC_GOALS,156,132,50,14,0,WS_EX_STATICEDGE + PUSHBUTTON "TBL Info",IDC_SHIP_TBL,210,132,50,14,0,WS_EX_STATICEDGE CONTROL "Hide Cues",IDC_HIDE_CUES,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,266,133,49,12 + GROUPBOX "Arrival",IDC_CUE_FRAME,7,149,150,240 + LTEXT "Location",IDC_STATIC,15,162,28,8 COMBOBOX IDC_ARRIVAL_LOCATION,46,162,103,127,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Target",IDC_STATIC,15,178,22,8 COMBOBOX IDC_ARRIVAL_TARGET,46,176,103,262,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Distance",IDC_STATIC,15,194,29,8 EDITTEXT IDC_ARRIVAL_DISTANCE,46,190,40,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Delay",IDC_STATIC,15,210,19,8 EDITTEXT IDC_ARRIVAL_DELAY,46,206,40,14,ES_AUTOHSCROLL | ES_NUMBER CONTROL "Spin1",IDC_ARRIVAL_DELAY_SPIN,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS,86,206,11,14 LTEXT "Seconds",IDC_STATIC,103,208,45,8 + PUSHBUTTON "Restrict Arrival Paths",IDC_RESTRICT_ARRIVAL,15,225,134,14,0,WS_EX_STATICEDGE + PUSHBUTTON "Custom Warp-in Parameters",IDC_CUSTOM_WARPIN_PARAMS,15,243,134,14,0,WS_EX_STATICEDGE CONTROL "Update Cue",IDC_UPDATE_ARRIVAL,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,94,263,54,10 + LTEXT "Cue:",IDC_STATIC,15,263,16,8 CONTROL "Tree1",IDC_ARRIVAL_TREE,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_EDITLABELS | WS_BORDER | WS_HSCROLL | WS_TABSTOP,15,273,133,91,WS_EX_CLIENTEDGE CONTROL "No Warp Effect",IDC_NO_ARRIVAL_WARP,"Button",BS_3STATE | WS_TABSTOP,15,366,65,10 CONTROL "Don't Adjust Warp When Docked",IDC_SAME_ARRIVAL_WARP_WHEN_DOCKED, "Button",BS_3STATE | WS_TABSTOP,15,376,120,10 + GROUPBOX "Departure",IDC_STATIC,165,149,150,240 + LTEXT "Location",IDC_STATIC,172,162,28,8 COMBOBOX IDC_DEPARTURE_LOCATION,202,162,103,127,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Target",IDC_STATIC,172,178,22,8 COMBOBOX IDC_DEPARTURE_TARGET,202,176,103,262,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Delay",IDC_STATIC,172,210,19,8 EDITTEXT IDC_DEPARTURE_DELAY,201,206,40,14,ES_AUTOHSCROLL | ES_NUMBER CONTROL "Spin3",IDC_DEPARTURE_DELAY_SPIN,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS,240,206,11,14 LTEXT "Seconds",IDC_STATIC,255,209,45,8 + PUSHBUTTON "Restrict Departure Paths",IDC_RESTRICT_DEPARTURE,171,225,134,14,0,WS_EX_STATICEDGE + PUSHBUTTON "Custom Warp-out Parameters",IDC_CUSTOM_WARPOUT_PARAMS,171,243,134,14,0,WS_EX_STATICEDGE CONTROL "Update Cue",IDC_UPDATE_DEPARTURE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,251,263,54,10 + LTEXT "Cue:",IDC_STATIC,172,263,16,8 CONTROL "Tree1",IDC_DEPARTURE_TREE,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_EDITLABELS | WS_BORDER | WS_HSCROLL | WS_TABSTOP,172,273,133,91,WS_EX_CLIENTEDGE CONTROL "No Warp Effect",IDC_NO_DEPARTURE_WARP,"Button",BS_3STATE | WS_TABSTOP,172,366,65,10 CONTROL "Don't Adjust Warp When Docked",IDC_SAME_DEPARTURE_WARP_WHEN_DOCKED, "Button",BS_3STATE | WS_TABSTOP,172,376,120,10 - EDITTEXT IDC_HELP_BOX,7,405,308,78,ES_MULTILINE | ES_READONLY,WS_EX_TRANSPARENT - LTEXT "Ship Name",IDC_STATIC,7,10,36,8 - LTEXT "Ship Class",IDC_STATIC,9,25,34,8 - LTEXT "Cargo",IDC_STATIC,23,68,20,8 - RTEXT "Wing:",IDC_STATIC,165,13,20,8 - LTEXT "Team",IDC_STATIC,24,54,19,8 - LTEXT "Location",IDC_STATIC,15,162,28,8 - LTEXT "Cue:",IDC_STATIC,15,263,16,8 - GROUPBOX "Arrival",IDC_CUE_FRAME,7,149,150,240 - LTEXT "Location",IDC_STATIC,172,162,28,8 - LTEXT "Cue:",IDC_STATIC,172,263,16,8 - GROUPBOX "Departure",IDC_STATIC,165,149,150,240 - LTEXT "AI Class",IDC_STATIC,17,40,26,8 - LTEXT "Delay",IDC_STATIC,15,210,19,8 - LTEXT "Delay",IDC_STATIC,172,210,19,8 - LTEXT "Static",IDC_WING,189,13,67,8 - LTEXT "Hotkey",IDC_STATIC,161,26,24,8 - LTEXT "Kill Score",IDC_STATIC,155,58,30,8 - LTEXT "Target",IDC_STATIC,15,178,22,8 - LTEXT "Distance",IDC_STATIC,15,194,29,8 - LTEXT "Target",IDC_STATIC,172,178,22,8 - LTEXT "Persona",IDC_STATIC,158,42,27,8 - LTEXT "Alt Name",IDC_STATIC,14,82,30,8 - PUSHBUTTON "Restrict Arrival Paths",IDC_RESTRICT_ARRIVAL,15,225,134,14,0,WS_EX_STATICEDGE - PUSHBUTTON "Restrict Departure Paths",IDC_RESTRICT_DEPARTURE,171,225,134,14,0,WS_EX_STATICEDGE - EDITTEXT IDC_ASSIST_SCORE,189,72,68,14,ES_AUTOHSCROLL | ES_NUMBER - LTEXT "Assist %",IDC_STATIC,158,75,27,8 - LTEXT "Callsign",IDC_STATIC,18,95,25,8 - PUSHBUTTON "Alt Ship Class",IDC_ALT_SHIP_CLASS,187,114,69,14,0,WS_EX_STATICEDGE EDITTEXT IDC_MINI_HELP_BOX,7,392,308,14,ES_MULTILINE | ES_READONLY,WS_EX_DLGMODALFRAME | WS_EX_TRANSPARENT - PUSHBUTTON "Custom Warp-in Parameters",IDC_CUSTOM_WARPIN_PARAMS,15,243,134,14,0,WS_EX_STATICEDGE - PUSHBUTTON "Custom Warp-out Parameters",IDC_CUSTOM_WARPOUT_PARAMS,171,243,134,14,0,WS_EX_STATICEDGE + EDITTEXT IDC_HELP_BOX,7,405,308,78,ES_MULTILINE | ES_READONLY,WS_EX_TRANSPARENT END IDD_WEAPON_EDITOR DIALOGEX 0, 0, 564, 79 diff --git a/fred2/shipeditordlg.cpp b/fred2/shipeditordlg.cpp index 718e91feac6..be4e88c425f 100644 --- a/fred2/shipeditordlg.cpp +++ b/fred2/shipeditordlg.cpp @@ -1923,7 +1923,7 @@ void CShipEditorDlg::calc_cue_height() CRect cue; GetDlgItem(IDC_CUE_FRAME)->GetWindowRect(cue); - cue_height = (cue.bottom - cue.top) + 10; + cue_height = (cue.bottom - cue.top) + 1; } void CShipEditorDlg::show_hide_sexp_help() diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 851a71faa1f..9ad78f12458 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -702,7 +702,7 @@ void FredView::keyReleaseEvent(QKeyEvent* event) { _inKeyReleaseHandler = false; } -void FredView::on_actionEvents_triggered(bool) { +void FredView::on_actionMission_Events_triggered(bool) { auto eventEditor = new dialogs::EventEditorDialog(this, _viewport); eventEditor->setAttribute(Qt::WA_DeleteOnClose); eventEditor->show(); @@ -1154,7 +1154,7 @@ void FredView::on_actionVoice_Acting_Manager_triggered(bool) { dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } -void FredView::on_actionMission_Objectives_triggered(bool) { +void FredView::on_actionMission_Goals_triggered(bool) { auto dialog = new dialogs::MissionGoalsDialog(this, _viewport); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 7958dc9ae66..25678fe85a9 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -83,7 +83,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionCamera_triggered(bool enabled); void on_actionCurrent_Ship_triggered(bool enabled); - void on_actionEvents_triggered(bool); + void on_actionMission_Events_triggered(bool); void on_actionAsteroid_Field_triggered(bool); void on_actionBriefing_triggered(bool); void on_actionMission_Specs_triggered(bool); @@ -137,7 +137,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionShield_System_triggered(bool); void on_actionVoice_Acting_Manager_triggered(bool); void on_actionFiction_Viewer_triggered(bool); - void on_actionMission_Objectives_triggered(bool); + void on_actionMission_Goals_triggered(bool); signals: /** * @brief Special version of FredApplication::onIdle which is limited to the lifetime of this object diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index b578a59a5e2..806c8cfe99e 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -168,20 +168,24 @@ - - - - - - + + + + + + + + - - + + + + + - @@ -1124,9 +1128,9 @@ Shift+Y - + - &Events + Mission &Events Shift+E @@ -1489,9 +1493,9 @@ &Empty - + - Mission Objectives + Mission Goals Shift+G From fe1540c8a4b5a2c6d2c9685374bc07b3f53e983a Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 1 Jul 2025 01:33:45 -0400 Subject: [PATCH 223/466] add 'Always save display names' option --- fred2/fred.cpp | 2 ++ fred2/fred.rc | 3 +++ fred2/fredrender.cpp | 1 + fred2/fredrender.h | 1 + fred2/fredview.cpp | 14 ++++++++++++++ fred2/fredview.h | 2 ++ fred2/missionsave.cpp | 16 ++++++++++------ fred2/resource.h | 1 + qtfred/src/mission/EditorViewport.h | 1 + qtfred/src/mission/missionsave.cpp | 16 ++++++++++------ qtfred/src/ui/FredView.cpp | 4 ++++ qtfred/src/ui/FredView.h | 1 + qtfred/ui/FredView.ui | 9 +++++++++ 13 files changed, 59 insertions(+), 12 deletions(-) diff --git a/fred2/fred.cpp b/fred2/fred.cpp index cb5de31e48a..46b1dbb3a38 100644 --- a/fred2/fred.cpp +++ b/fred2/fred.cpp @@ -244,6 +244,7 @@ BOOL CFREDApp::InitInstance() { Draw_outlines_on_selected_ships = GetProfileInt("Preferences", "Draw outlines on selected ships", 1) != 0; Point_using_uvec = GetProfileInt("Preferences", "Point using uvec", Point_using_uvec); Draw_outline_at_warpin_position = GetProfileInt("Preferences", "Draw outline at warpin position", 0) != 0; + Always_save_display_names = GetProfileInt("Preferences", "Always save display names", 0) != 0; Error_checker_checks_potential_issues = GetProfileInt("Preferences", "Error checker checks potential issues", 1) != 0; read_window("Main window", &Main_wnd_data); @@ -536,6 +537,7 @@ void CFREDApp::write_ini_file(int degree) { WriteProfileInt("Preferences", "Draw outlines on selected ships", Draw_outlines_on_selected_ships ? 1 : 0); WriteProfileInt("Preferences", "Point using uvec", Point_using_uvec); WriteProfileInt("Preferences", "Draw outline at warpin position", Draw_outline_at_warpin_position ? 1 : 0); + WriteProfileInt("Preferences", "Always save display names", Always_save_display_names ? 1 : 0); WriteProfileInt("Preferences", "Error checker checks potential issues", Error_checker_checks_potential_issues ? 1 : 0); if (!degree) { diff --git a/fred2/fred.rc b/fred2/fred.rc index 2930fa9856d..49339b17248 100644 --- a/fred2/fred.rc +++ b/fred2/fred.rc @@ -394,6 +394,7 @@ BEGIN MENUITEM "Mission Statistics\tCtrl+Shift+D", 33067 MENUITEM "Music Player", ID_MUSIC_PLAYER MENUITEM SEPARATOR + MENUITEM "Always save Display Names", ID_ALWAYS_SAVE_DISPLAY_NAMES MENUITEM "Error checker checks for potential issues", ID_ERROR_CHECKER_CHECKS_POTENTIAL_ISSUES, CHECKED MENUITEM "Error checker\tShift+H", 32978 END @@ -3684,6 +3685,8 @@ END STRINGTABLE BEGIN + ID_ALWAYS_SAVE_DISPLAY_NAMES "When saving a mission, always write display names to the mission file even if the display name is not set" + ID_ERROR_CHECKER_CHECKS_POTENTIAL_ISSUES "If checked, error checker will check for things that are not necessarily errors but may cause unexpected behavior" ID_ERROR_CHECKER "Checks mission for FRED-detectable errors" END diff --git a/fred2/fredrender.cpp b/fred2/fredrender.cpp index c744042fdd4..2c91d00d849 100644 --- a/fred2/fredrender.cpp +++ b/fred2/fredrender.cpp @@ -102,6 +102,7 @@ int Show_horizon = 0; int Show_outlines = 0; bool Draw_outlines_on_selected_ships = true; bool Draw_outline_at_warpin_position = false; +bool Always_save_display_names = false; bool Error_checker_checks_potential_issues = true; bool Error_checker_checks_potential_issues_once = false; int Show_stars = 1; diff --git a/fred2/fredrender.h b/fred2/fredrender.h index c5c5d99f1e3..cb69edf8ebf 100644 --- a/fred2/fredrender.h +++ b/fred2/fredrender.h @@ -22,6 +22,7 @@ extern int Show_coordinates; //!< Bool. If nonzero, draw the coordinates extern int Show_outlines; //!< Bool. If nonzero, draw each object's mesh. If models are shown, highlight them in white. extern bool Draw_outlines_on_selected_ships; // If a ship is selected, draw mesh lines extern bool Draw_outline_at_warpin_position; // Project an outline at the place where the ship will arrive after warping in +extern bool Always_save_display_names; // When saving a mission, always write display names to the mission file even if the display name is not set extern bool Error_checker_checks_potential_issues; // Error checker checks not only outright errors but also potential issues extern bool Error_checker_checks_potential_issues_once; // Same as above, but only once, and independent of the selected option extern int Show_stars; //!< Bool. If nonzero, draw the starfield, nebulas, and suns. Might also handle skyboxes diff --git a/fred2/fredview.cpp b/fred2/fredview.cpp index c521ea5b521..c3ca517c733 100644 --- a/fred2/fredview.cpp +++ b/fred2/fredview.cpp @@ -269,6 +269,8 @@ BEGIN_MESSAGE_MAP(CFREDView, CView) ON_UPDATE_COMMAND_UI(ID_VIEW_OUTLINES_ON_SELECTED, OnUpdateViewOutlinesOnSelected) ON_COMMAND(ID_VIEW_OUTLINE_AT_WARPIN, OnViewOutlineAtWarpin) ON_UPDATE_COMMAND_UI(ID_VIEW_OUTLINE_AT_WARPIN, OnUpdateViewOutlineAtWarpin) + ON_COMMAND(ID_ALWAYS_SAVE_DISPLAY_NAMES, OnAlwaysSaveDisplayNames) + ON_UPDATE_COMMAND_UI(ID_ALWAYS_SAVE_DISPLAY_NAMES, OnUpdateAlwaysSaveDisplayNames) ON_COMMAND(ID_ERROR_CHECKER_CHECKS_POTENTIAL_ISSUES, OnErrorCheckerChecksPotentialIssues) ON_UPDATE_COMMAND_UI(ID_ERROR_CHECKER_CHECKS_POTENTIAL_ISSUES, OnUpdateErrorCheckerChecksPotentialIssues) ON_UPDATE_COMMAND_UI(ID_NEW_SHIP_TYPE, OnUpdateNewShipType) @@ -3829,6 +3831,18 @@ void CFREDView::OnUpdateViewOutlineAtWarpin(CCmdUI* pCmdUI) pCmdUI->SetCheck(Draw_outline_at_warpin_position); } +void CFREDView::OnAlwaysSaveDisplayNames() +{ + Always_save_display_names = !Always_save_display_names; + theApp.write_ini_file(); + Update_window = 1; +} + +void CFREDView::OnUpdateAlwaysSaveDisplayNames(CCmdUI* pCmdUI) +{ + pCmdUI->SetCheck(Always_save_display_names); +} + void CFREDView::OnErrorCheckerChecksPotentialIssues() { Error_checker_checks_potential_issues = !Error_checker_checks_potential_issues; diff --git a/fred2/fredview.h b/fred2/fredview.h index dd5275ac8d4..d351a00dc46 100644 --- a/fred2/fredview.h +++ b/fred2/fredview.h @@ -225,6 +225,8 @@ class CFREDView : public CView afx_msg void OnUpdateViewOutlinesOnSelected(CCmdUI* pCmdUI); afx_msg void OnViewOutlineAtWarpin(); afx_msg void OnUpdateViewOutlineAtWarpin(CCmdUI* pCmdUI); + afx_msg void OnAlwaysSaveDisplayNames(); + afx_msg void OnUpdateAlwaysSaveDisplayNames(CCmdUI* pCmdUI); afx_msg void OnErrorCheckerChecksPotentialIssues(); afx_msg void OnUpdateErrorCheckerChecksPotentialIssues(CCmdUI* pCmdUI); afx_msg void OnUpdateNewShipType(CCmdUI* pCmdUI); diff --git a/fred2/missionsave.cpp b/fred2/missionsave.cpp index 1afcbd6fd96..2e65c47438a 100644 --- a/fred2/missionsave.cpp +++ b/fred2/missionsave.cpp @@ -3549,15 +3549,19 @@ int CFred_mission_save::save_objects() // Display name // The display name is only written if there was one at the start to avoid introducing inconsistencies - if (Mission_save_format != FSO_FORMAT_RETAIL && shipp->has_display_name()) { + if (Mission_save_format != FSO_FORMAT_RETAIL && (Always_save_display_names || shipp->has_display_name())) { char truncated_name[NAME_LENGTH]; strcpy_s(truncated_name, shipp->ship_name); end_string_at_first_hash_symbol(truncated_name); // Also, the display name is not written if it's just the truncation of the name at the hash - if (strcmp(shipp->get_display_name(), truncated_name) != 0) { - fout("\n$Display name:"); - fout_ext(" ", "%s", shipp->display_name.c_str()); + if (Always_save_display_names || strcmp(shipp->get_display_name(), truncated_name) != 0) { + if (optional_string_fred("$Display name:", "$Class:")) { + parse_comments(); + } else { + fout("\n$Display name:"); + } + fout_ext(" ", "%s", shipp->get_display_name()); } } @@ -4844,13 +4848,13 @@ int CFred_mission_save::save_waypoints() if (Mission_save_format != FSO_FORMAT_RETAIL) { // The display name is only written if there was one at the start to avoid introducing inconsistencies - if (jnp->HasDisplayName()) { + if (Always_save_display_names || jnp->HasDisplayName()) { char truncated_name[NAME_LENGTH]; strcpy_s(truncated_name, jnp->GetName()); end_string_at_first_hash_symbol(truncated_name); // Also, the display name is not written if it's just the truncation of the name at the hash - if (strcmp(jnp->GetDisplayName(), truncated_name) != 0) { + if (Always_save_display_names || strcmp(jnp->GetDisplayName(), truncated_name) != 0) { if (optional_string_fred("+Display Name:", "$Jump Node:")) { parse_comments(); } else { diff --git a/fred2/resource.h b/fred2/resource.h index 5edfb7ac085..4105c291c2a 100644 --- a/fred2/resource.h +++ b/fred2/resource.h @@ -1462,6 +1462,7 @@ #define ID_CPGN_FILE_SAVE_AS 32998 #define ID_SHOW_STARFRIELD 32999 #define ID_REVERT 33000 +#define ID_ALWAYS_SAVE_DISPLAY_NAMES 33001 #define ID_HIDE_MARKED_OBJECTS 33002 #define ID_SHOW_HIDDEN_OBJECTS 33003 #define ID_GROUP_SET 33004 diff --git a/qtfred/src/mission/EditorViewport.h b/qtfred/src/mission/EditorViewport.h index 18d48e5edba..cc02875e68f 100644 --- a/qtfred/src/mission/EditorViewport.h +++ b/qtfred/src/mission/EditorViewport.h @@ -163,6 +163,7 @@ class EditorViewport { bool Group_rotate = true; bool Lookat_mode = false; bool Move_ships_when_undocking = true; + bool Always_save_display_names = false; bool Error_checker_checks_potential_issues = true; bool Error_checker_checks_potential_issues_once = false; diff --git a/qtfred/src/mission/missionsave.cpp b/qtfred/src/mission/missionsave.cpp index 223530f3be3..f6770e3d394 100644 --- a/qtfred/src/mission/missionsave.cpp +++ b/qtfred/src/mission/missionsave.cpp @@ -3467,15 +3467,19 @@ int CFred_mission_save::save_objects() // Display name // The display name is only written if there was one at the start to avoid introducing inconsistencies - if (save_format != MissionFormat::RETAIL && shipp->has_display_name()) { + if (save_format != MissionFormat::RETAIL && (_viewport->Always_save_display_names || shipp->has_display_name())) { char truncated_name[NAME_LENGTH]; strcpy_s(truncated_name, shipp->ship_name); end_string_at_first_hash_symbol(truncated_name); // Also, the display name is not written if it's just the truncation of the name at the hash - if (strcmp(shipp->get_display_name(), truncated_name) != 0) { - fout("\n$Display name:"); - fout_ext(" ", "%s", shipp->display_name.c_str()); + if (_viewport->Always_save_display_names || strcmp(shipp->get_display_name(), truncated_name) != 0) { + if (optional_string_fred("$Display name:", "$Class:")) { + parse_comments(); + } else { + fout("\n$Display name:"); + } + fout_ext(" ", "%s", shipp->get_display_name()); } } @@ -5003,13 +5007,13 @@ int CFred_mission_save::save_waypoints() if (save_format != MissionFormat::RETAIL) { // The display name is only written if there was one at the start to avoid introducing inconsistencies - if (jnp->HasDisplayName()) { + if (_viewport->Always_save_display_names || jnp->HasDisplayName()) { char truncated_name[NAME_LENGTH]; strcpy_s(truncated_name, jnp->GetName()); end_string_at_first_hash_symbol(truncated_name); // Also, the display name is not written if it's just the truncation of the name at the hash - if (strcmp(jnp->GetDisplayName(), truncated_name) != 0) { + if (_viewport->Always_save_display_names || strcmp(jnp->GetDisplayName(), truncated_name) != 0) { if (optional_string_fred("+Display Name:", "$Jump Node:")) { parse_comments(); } else { diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 9ad78f12458..6f484836bf4 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -135,6 +135,7 @@ void FredView::setEditor(Editor* editor, EditorViewport* viewport) { [this]() { ui->actionRestore_Camera_Pos->setEnabled(!IS_VEC_NULL(&_viewport->saved_cam_orient.vec.fvec)); }); connect(this, &FredView::viewIdle, this, [this]() { ui->actionMove_Ships_When_Undocking->setChecked(_viewport->Move_ships_when_undocking); }); + connect(this, &FredView::viewIdle, this, [this]() { ui->actionAlways_Save_Display_Names->setChecked(_viewport->Always_save_display_names); }); connect(this, &FredView::viewIdle, this, [this]() { ui->actionError_Checker_Checks_Potential_Issues->setChecked(_viewport->Error_checker_checks_potential_issues); }); } @@ -1125,6 +1126,9 @@ void FredView::on_actionCancel_Subsystem_triggered(bool) { void FredView::on_actionMove_Ships_When_Undocking_triggered(bool) { _viewport->Move_ships_when_undocking = !_viewport->Move_ships_when_undocking; } +void FredView::on_actionAlways_Save_Display_Names_triggered(bool) { + _viewport->Always_save_display_names = !_viewport->Always_save_display_names; +} void FredView::on_actionError_Checker_Checks_Potential_Issues_triggered(bool) { _viewport->Error_checker_checks_potential_issues = !_viewport->Error_checker_checks_potential_issues; } diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 25678fe85a9..efee90e75cc 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -129,6 +129,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionMove_Ships_When_Undocking_triggered(bool); + void on_actionAlways_Save_Display_Names_triggered(bool); void on_actionError_Checker_Checks_Potential_Issues_triggered(bool); void on_actionError_Checker_triggered(bool); diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index 806c8cfe99e..66690cfb25c 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -238,6 +238,7 @@ + @@ -1443,6 +1444,14 @@ Ctrl+Shift+D + + + true + + + Always save Display Names + + true From d99c4ca1721c230268590683714b41ee2475d180 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 1 Jul 2025 01:34:36 -0400 Subject: [PATCH 224/466] add display name text field to ship editor --- code/parse/parselo.cpp | 4 +- code/parse/parselo.h | 2 +- fred2/fred.rc | 114 +++++++++--------- fred2/jumpnodedlg.cpp | 4 +- fred2/management.cpp | 14 +++ fred2/management.h | 1 + fred2/resource.h | 2 +- fred2/shipeditordlg.cpp | 62 ++++++++-- fred2/shipeditordlg.h | 2 + fred2/wing_editor.cpp | 2 +- qtfred/src/mission/Editor.cpp | 13 ++ qtfred/src/mission/Editor.h | 1 + .../ShipEditor/ShipEditorDialogModel.cpp | 52 +++++++- .../ShipEditor/ShipEditorDialogModel.h | 6 +- .../dialogs/ShipEditor/ShipEditorDialog.cpp | 21 +++- .../ui/dialogs/ShipEditor/ShipEditorDialog.h | 1 + qtfred/ui/ShipEditorDialog.ui | 106 +++++++++------- 17 files changed, 283 insertions(+), 124 deletions(-) diff --git a/code/parse/parselo.cpp b/code/parse/parselo.cpp index b50fde1445e..6d77989cd3f 100644 --- a/code/parse/parselo.cpp +++ b/code/parse/parselo.cpp @@ -4445,7 +4445,7 @@ const char *get_pointer_to_first_hash_symbol(const char *src, bool ignore_double } // Goober5000 -int get_index_of_first_hash_symbol(SCP_string &src, bool ignore_doubled_hash) +int get_index_of_first_hash_symbol(const SCP_string &src, bool ignore_doubled_hash) { if (ignore_doubled_hash) { @@ -4456,7 +4456,7 @@ int get_index_of_first_hash_symbol(SCP_string &src, bool ignore_doubled_hash) if ((ch + 1) != src.end() && *(ch + 1) == '#') ++ch; else - return (int)std::distance(src.begin(), ch); + return static_cast(std::distance(src.begin(), ch)); } } return -1; diff --git a/code/parse/parselo.h b/code/parse/parselo.h index 04cee9626c6..d05e2c0733c 100644 --- a/code/parse/parselo.h +++ b/code/parse/parselo.h @@ -78,7 +78,7 @@ extern bool end_string_at_first_hash_symbol(char *src, bool ignore_doubled_hash extern bool end_string_at_first_hash_symbol(SCP_string &src, bool ignore_doubled_hash = false); extern char *get_pointer_to_first_hash_symbol(char *src, bool ignore_doubled_hash = false); extern const char *get_pointer_to_first_hash_symbol(const char *src, bool ignore_doubled_hash = false); -extern int get_index_of_first_hash_symbol(SCP_string &src, bool ignore_doubled_hash = false); +extern int get_index_of_first_hash_symbol(const SCP_string &src, bool ignore_doubled_hash = false); extern void consolidate_double_characters(char *str, char ch); diff --git a/fred2/fred.rc b/fred2/fred.rc index 49339b17248..33d466173e2 100644 --- a/fred2/fred.rc +++ b/fred2/fred.rc @@ -997,7 +997,7 @@ BEGIN GROUPBOX "Default Display",IDC_DISPLAY,107,7,136,56 END -IDD_SHIP_EDITOR DIALOGEX 0, 0, 322, 488 +IDD_SHIP_EDITOR DIALOGEX 0, 0, 322, 504 STYLE DS_SETFONT | DS_MODALFRAME | DS_3DLOOK | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU EXSTYLE WS_EX_NOPARENTNOTIFY | WS_EX_CLIENTEDGE | WS_EX_CONTEXTHELP CAPTION "Edit Ship" @@ -1008,19 +1008,21 @@ BEGIN PUSHBUTTON "Next",IDC_NEXT,291,7,24,14,0,WS_EX_STATICEDGE LTEXT "Ship Name",IDC_STATIC,7,10,36,8 EDITTEXT IDC_SHIP_NAME,47,7,94,14,ES_AUTOHSCROLL - LTEXT "Ship Class",IDC_STATIC,9,25,34,8 - COMBOBOX IDC_SHIP_CLASS,47,23,94,207,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - LTEXT "AI Class",IDC_STATIC,17,40,26,8 - COMBOBOX IDC_AI_CLASS,47,37,94,185,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - LTEXT "Team",IDC_STATIC,24,54,19,8 - COMBOBOX IDC_SHIP_TEAM,47,51,94,196,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - LTEXT "Cargo",IDC_STATIC,23,68,20,8 - COMBOBOX IDC_SHIP_CARGO1,47,65,94,258,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP - LTEXT "Alt Name",IDC_STATIC,14,82,30,8 - COMBOBOX IDC_SHIP_ALT,47,79,94,125,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP - LTEXT "Callsign",IDC_STATIC,18,95,25,8 - COMBOBOX IDC_SHIP_CALLSIGN,47,93,94,125,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP - PUSHBUTTON "Texture Replacement",IDC_TEXTURES,47,110,94,14,0,WS_EX_STATICEDGE + LTEXT "Display Name",IDC_STATIC,7,25,45,8 + EDITTEXT IDC_DISPLAY_NAME,56,22,85,14,ES_AUTOHSCROLL + LTEXT "Callsign",IDC_STATIC,18,40,25,8 + COMBOBOX IDC_SHIP_CALLSIGN,47,37,94,125,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP + LTEXT "Ship Class",IDC_STATIC,9,54,34,8 + COMBOBOX IDC_SHIP_CLASS,47,51,94,207,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Alt Name",IDC_STATIC,14,68,30,8 + COMBOBOX IDC_SHIP_ALT,47,65,94,125,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP + LTEXT "AI Class",IDC_STATIC,17,82,26,8 + COMBOBOX IDC_AI_CLASS,47,79,94,185,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Team",IDC_STATIC,24,96,19,8 + COMBOBOX IDC_SHIP_TEAM,47,93,94,196,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Cargo",IDC_STATIC,23,110,20,8 + COMBOBOX IDC_SHIP_CARGO1,47,107,94,258,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Texture Replacement",IDC_TEXTURES,47,125,94,14,0,WS_EX_STATICEDGE RTEXT "Wing:",IDC_STATIC,165,13,20,8 LTEXT "Static",IDC_WING,189,13,67,8 LTEXT "Hotkey",IDC_STATIC,161,26,24,8 @@ -1040,49 +1042,49 @@ BEGIN PUSHBUTTON "Player Orders",IDC_IGNORE_ORDERS,265,71,50,14,0,WS_EX_STATICEDGE PUSHBUTTON "Special Exp",IDC_SPECIAL_EXP,265,87,50,14,0,WS_EX_STATICEDGE PUSHBUTTON "Special Hits",IDC_SPECIAL_HITPOINTS,265,103,50,14,0,WS_EX_STATICEDGE - PUSHBUTTON "Misc",IDC_FLAGS,47,132,50,14,0,WS_EX_STATICEDGE - PUSHBUTTON "Initial Status",IDC_INITIAL_STATUS,101,132,50,14,0,WS_EX_STATICEDGE - PUSHBUTTON "Initial Orders",IDC_GOALS,156,132,50,14,0,WS_EX_STATICEDGE - PUSHBUTTON "TBL Info",IDC_SHIP_TBL,210,132,50,14,0,WS_EX_STATICEDGE - CONTROL "Hide Cues",IDC_HIDE_CUES,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,266,133,49,12 - GROUPBOX "Arrival",IDC_CUE_FRAME,7,149,150,240 - LTEXT "Location",IDC_STATIC,15,162,28,8 - COMBOBOX IDC_ARRIVAL_LOCATION,46,162,103,127,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - LTEXT "Target",IDC_STATIC,15,178,22,8 - COMBOBOX IDC_ARRIVAL_TARGET,46,176,103,262,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - LTEXT "Distance",IDC_STATIC,15,194,29,8 - EDITTEXT IDC_ARRIVAL_DISTANCE,46,190,40,14,ES_AUTOHSCROLL | ES_NUMBER - LTEXT "Delay",IDC_STATIC,15,210,19,8 - EDITTEXT IDC_ARRIVAL_DELAY,46,206,40,14,ES_AUTOHSCROLL | ES_NUMBER - CONTROL "Spin1",IDC_ARRIVAL_DELAY_SPIN,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS,86,206,11,14 - LTEXT "Seconds",IDC_STATIC,103,208,45,8 - PUSHBUTTON "Restrict Arrival Paths",IDC_RESTRICT_ARRIVAL,15,225,134,14,0,WS_EX_STATICEDGE - PUSHBUTTON "Custom Warp-in Parameters",IDC_CUSTOM_WARPIN_PARAMS,15,243,134,14,0,WS_EX_STATICEDGE - CONTROL "Update Cue",IDC_UPDATE_ARRIVAL,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,94,263,54,10 - LTEXT "Cue:",IDC_STATIC,15,263,16,8 - CONTROL "Tree1",IDC_ARRIVAL_TREE,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_EDITLABELS | WS_BORDER | WS_HSCROLL | WS_TABSTOP,15,273,133,91,WS_EX_CLIENTEDGE - CONTROL "No Warp Effect",IDC_NO_ARRIVAL_WARP,"Button",BS_3STATE | WS_TABSTOP,15,366,65,10 + PUSHBUTTON "Misc",IDC_FLAGS,47,148,50,14,0,WS_EX_STATICEDGE + PUSHBUTTON "Initial Status",IDC_INITIAL_STATUS,101,148,50,14,0,WS_EX_STATICEDGE + PUSHBUTTON "Initial Orders",IDC_GOALS,156,148,50,14,0,WS_EX_STATICEDGE + PUSHBUTTON "TBL Info",IDC_SHIP_TBL,210,148,50,14,0,WS_EX_STATICEDGE + CONTROL "Hide Cues",IDC_HIDE_CUES,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,266,149,49,12 + GROUPBOX "Arrival",IDC_CUE_FRAME,7,165,150,240 + LTEXT "Location",IDC_STATIC,15,178,28,8 + COMBOBOX IDC_ARRIVAL_LOCATION,46,178,103,127,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Target",IDC_STATIC,15,194,22,8 + COMBOBOX IDC_ARRIVAL_TARGET,46,192,103,262,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Distance",IDC_STATIC,15,208,29,8 + EDITTEXT IDC_ARRIVAL_DISTANCE,46,206,40,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Delay",IDC_STATIC,15,226,19,8 + EDITTEXT IDC_ARRIVAL_DELAY,46,222,40,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_ARRIVAL_DELAY_SPIN,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS,86,222,11,14 + LTEXT "Seconds",IDC_STATIC,103,224,45,8 + PUSHBUTTON "Restrict Arrival Paths",IDC_RESTRICT_ARRIVAL,15,241,134,14,0,WS_EX_STATICEDGE + PUSHBUTTON "Custom Warp-in Parameters",IDC_CUSTOM_WARPIN_PARAMS,15,259,134,14,0,WS_EX_STATICEDGE + CONTROL "Update Cue",IDC_UPDATE_ARRIVAL,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,94,279,54,10 + LTEXT "Cue:",IDC_STATIC,15,279,16,8 + CONTROL "Tree1",IDC_ARRIVAL_TREE,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_EDITLABELS | WS_BORDER | WS_HSCROLL | WS_TABSTOP,15,289,133,91,WS_EX_CLIENTEDGE + CONTROL "No Warp Effect",IDC_NO_ARRIVAL_WARP,"Button",BS_3STATE | WS_TABSTOP,15,382,65,10 CONTROL "Don't Adjust Warp When Docked",IDC_SAME_ARRIVAL_WARP_WHEN_DOCKED, - "Button",BS_3STATE | WS_TABSTOP,15,376,120,10 - GROUPBOX "Departure",IDC_STATIC,165,149,150,240 - LTEXT "Location",IDC_STATIC,172,162,28,8 - COMBOBOX IDC_DEPARTURE_LOCATION,202,162,103,127,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - LTEXT "Target",IDC_STATIC,172,178,22,8 - COMBOBOX IDC_DEPARTURE_TARGET,202,176,103,262,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - LTEXT "Delay",IDC_STATIC,172,210,19,8 - EDITTEXT IDC_DEPARTURE_DELAY,201,206,40,14,ES_AUTOHSCROLL | ES_NUMBER - CONTROL "Spin3",IDC_DEPARTURE_DELAY_SPIN,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS,240,206,11,14 - LTEXT "Seconds",IDC_STATIC,255,209,45,8 - PUSHBUTTON "Restrict Departure Paths",IDC_RESTRICT_DEPARTURE,171,225,134,14,0,WS_EX_STATICEDGE - PUSHBUTTON "Custom Warp-out Parameters",IDC_CUSTOM_WARPOUT_PARAMS,171,243,134,14,0,WS_EX_STATICEDGE - CONTROL "Update Cue",IDC_UPDATE_DEPARTURE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,251,263,54,10 - LTEXT "Cue:",IDC_STATIC,172,263,16,8 - CONTROL "Tree1",IDC_DEPARTURE_TREE,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_EDITLABELS | WS_BORDER | WS_HSCROLL | WS_TABSTOP,172,273,133,91,WS_EX_CLIENTEDGE - CONTROL "No Warp Effect",IDC_NO_DEPARTURE_WARP,"Button",BS_3STATE | WS_TABSTOP,172,366,65,10 + "Button",BS_3STATE | WS_TABSTOP,15,392,120,10 + GROUPBOX "Departure",IDC_STATIC,165,165,150,240 + LTEXT "Location",IDC_STATIC,172,178,28,8 + COMBOBOX IDC_DEPARTURE_LOCATION,202,178,103,127,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Target",IDC_STATIC,172,194,22,8 + COMBOBOX IDC_DEPARTURE_TARGET,202,192,103,262,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Delay",IDC_STATIC,172,226,19,8 + EDITTEXT IDC_DEPARTURE_DELAY,201,222,40,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin3",IDC_DEPARTURE_DELAY_SPIN,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS,240,222,11,14 + LTEXT "Seconds",IDC_STATIC,255,224,45,8 + PUSHBUTTON "Restrict Departure Paths",IDC_RESTRICT_DEPARTURE,171,241,134,14,0,WS_EX_STATICEDGE + PUSHBUTTON "Custom Warp-out Parameters",IDC_CUSTOM_WARPOUT_PARAMS,171,259,134,14,0,WS_EX_STATICEDGE + CONTROL "Update Cue",IDC_UPDATE_DEPARTURE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,251,279,54,10 + LTEXT "Cue:",IDC_STATIC,172,279,16,8 + CONTROL "Tree1",IDC_DEPARTURE_TREE,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_EDITLABELS | WS_BORDER | WS_HSCROLL | WS_TABSTOP,172,289,133,91,WS_EX_CLIENTEDGE + CONTROL "No Warp Effect",IDC_NO_DEPARTURE_WARP,"Button",BS_3STATE | WS_TABSTOP,172,382,65,10 CONTROL "Don't Adjust Warp When Docked",IDC_SAME_DEPARTURE_WARP_WHEN_DOCKED, - "Button",BS_3STATE | WS_TABSTOP,172,376,120,10 - EDITTEXT IDC_MINI_HELP_BOX,7,392,308,14,ES_MULTILINE | ES_READONLY,WS_EX_DLGMODALFRAME | WS_EX_TRANSPARENT - EDITTEXT IDC_HELP_BOX,7,405,308,78,ES_MULTILINE | ES_READONLY,WS_EX_TRANSPARENT + "Button",BS_3STATE | WS_TABSTOP,172,392,120,10 + EDITTEXT IDC_MINI_HELP_BOX,7,408,308,14,ES_MULTILINE | ES_READONLY,WS_EX_DLGMODALFRAME | WS_EX_TRANSPARENT + EDITTEXT IDC_HELP_BOX,7,421,308,78,ES_MULTILINE | ES_READONLY,WS_EX_TRANSPARENT END IDD_WEAPON_EDITOR DIALOGEX 0, 0, 564, 79 @@ -1608,7 +1610,7 @@ BEGIN LTEXT "Name",IDC_STATIC,7,9,20,8 EDITTEXT IDC_NAME,32,7,106,14,ES_AUTOHSCROLL LTEXT "Display Name",IDC_STATIC,7,25,48,8 - EDITTEXT IDC_ALT_NAME,54,22,84,14,ES_AUTOHSCROLL + EDITTEXT IDC_DISPLAY_NAME,54,22,84,14,ES_AUTOHSCROLL LTEXT "Model File",IDC_STATIC,7,46,48,8 EDITTEXT IDC_MODEL_FILENAME,44,43,94,14,ES_AUTOHSCROLL LTEXT "R",IDC_STATIC,9,78,8,8 diff --git a/fred2/jumpnodedlg.cpp b/fred2/jumpnodedlg.cpp index 84d31102f97..071d7651306 100644 --- a/fred2/jumpnodedlg.cpp +++ b/fred2/jumpnodedlg.cpp @@ -49,7 +49,7 @@ void jumpnode_dlg::DoDataExchange(CDataExchange* pDX) CDialog::DoDataExchange(pDX); //{{AFX_DATA_MAP(jumpnode_dlg) DDX_Text(pDX, IDC_NAME, m_name); - DDX_Text(pDX, IDC_ALT_NAME, m_display); + DDX_Text(pDX, IDC_DISPLAY_NAME, m_display); DDX_Text(pDX, IDC_MODEL_FILENAME, m_filename); DDX_Text(pDX, IDC_NODE_R, m_color_r); DDV_MinMaxInt(pDX, m_color_r, 0, 255); @@ -383,5 +383,5 @@ void jumpnode_dlg::OnKillfocusName() end_string_at_first_hash_symbol(buffer); // set the display name derived from this name - SetDlgItemText(IDC_ALT_NAME, buffer); + SetDlgItemText(IDC_DISPLAY_NAME, buffer); } diff --git a/fred2/management.cpp b/fred2/management.cpp index 13436ae1564..b1c8b62e978 100644 --- a/fred2/management.cpp +++ b/fred2/management.cpp @@ -234,6 +234,20 @@ void lcl_fred_replace_stuff(CString &text) text.Replace("\\", "$backslash"); } +CString get_display_name_for_text_box(const char *orig_name) +{ + auto p = get_pointer_to_first_hash_symbol(orig_name); + if (p) + { + // use the same logic as in end_string_at_first_hash_symbol, but rewritten for CString + CString display_name(orig_name, static_cast(p - orig_name)); + display_name.TrimRight(); + return display_name; + } + else + return ""; +} + void fred_preload_all_briefing_icons() { for (SCP_vector::iterator ii = Briefing_icon_info.begin(); ii != Briefing_icon_info.end(); ++ii) diff --git a/fred2/management.h b/fred2/management.h index de315b1fe00..47ba40ebb7d 100644 --- a/fred2/management.h +++ b/fred2/management.h @@ -68,6 +68,7 @@ void deconvert_multiline_string(SCP_string& dest, const CString& str); void strip_quotation_marks(CString& str); void pad_with_newline(CString& str, int max_size); void lcl_fred_replace_stuff(CString& text); +CString get_display_name_for_text_box(const char *orig_name); bool fred_init(std::unique_ptr&& graphicsOps); void set_physics_controls(); diff --git a/fred2/resource.h b/fred2/resource.h index 4105c291c2a..837c1a88ddb 100644 --- a/fred2/resource.h +++ b/fred2/resource.h @@ -712,7 +712,7 @@ #define IDC_REINFORCEMENT 1323 #define IDC_MAIN_HALL 1323 #define IDC_DEBRIEFING_PERSONA 1324 -#define IDC_ALT_NAME 1325 +#define IDC_DISPLAY_NAME 1325 #define IDC_DOCK1 1327 #define IDC_INNER_MIN_X 1327 #define IDC_DOCK2 1328 diff --git a/fred2/shipeditordlg.cpp b/fred2/shipeditordlg.cpp index be4e88c425f..cc3555b536a 100644 --- a/fred2/shipeditordlg.cpp +++ b/fred2/shipeditordlg.cpp @@ -113,6 +113,7 @@ CShipEditorDlg::CShipEditorDlg(CWnd* pParent /*=NULL*/) { //{{AFX_DATA_INIT(CShipEditorDlg) m_ship_name = _T(""); + m_ship_display_name = _T(""); m_cargo1 = _T(""); m_ship_class_combo_index = -1; m_team = -1; @@ -159,6 +160,7 @@ void CShipEditorDlg::DoDataExchange(CDataExchange* pDX) DDX_Control(pDX, IDC_PLAYER_SHIP, m_player_ship); DDX_Text(pDX, IDC_SHIP_NAME, m_ship_name); DDV_MaxChars(pDX, m_ship_name, NAME_LENGTH - 1); + DDX_Text(pDX, IDC_DISPLAY_NAME, m_ship_display_name); DDX_CBString(pDX, IDC_SHIP_CARGO1, m_cargo1); DDV_MaxChars(pDX, m_cargo1, NAME_LENGTH - 1); DDX_CBIndex(pDX, IDC_SHIP_CLASS, m_ship_class_combo_index); @@ -210,6 +212,7 @@ BEGIN_MESSAGE_MAP(CShipEditorDlg, CDialog) ON_NOTIFY(TVN_ENDLABELEDIT, IDC_ARRIVAL_TREE, OnEndlabeleditArrivalTree) ON_NOTIFY(TVN_ENDLABELEDIT, IDC_DEPARTURE_TREE, OnEndlabeleditDepartureTree) ON_BN_CLICKED(IDC_GOALS, OnGoals) + ON_EN_CHANGE(IDC_SHIP_NAME, OnChangeShipName) ON_CBN_SELCHANGE(IDC_SHIP_CLASS, OnSelchangeShipClass) ON_BN_CLICKED(IDC_INITIAL_STATUS, OnInitialStatus) ON_BN_CLICKED(IDC_WEAPONS, OnWeapons) @@ -507,9 +510,11 @@ void CShipEditorDlg::initialize_data(int full_update) if (!multi_edit) { Assert((ship_count == 1) && (base_ship >= 0)); - m_ship_name = Ships[base_ship].ship_name; + m_ship_name = Ships[base_ship].ship_name; + m_ship_display_name = Ships[base_ship].has_display_name() ? Ships[base_ship].get_display_name() : ""; } else { m_ship_name = _T(""); + m_ship_display_name = _T(""); } m_update_arrival = m_update_departure = 1; @@ -721,6 +726,7 @@ void CShipEditorDlg::initialize_data(int full_update) if (player_count > 1) { // multiple player ships selected Assert(base_player >= 0); m_ship_name = _T(""); + m_ship_display_name = _T(""); m_player_ship.SetCheck(TRUE); objp = GET_FIRST(&obj_used_list); while (objp != END_OF_LIST(&obj_used_list)) { @@ -747,12 +753,14 @@ void CShipEditorDlg::initialize_data(int full_update) Assert((player_count == 1) && !multi_edit); player_ship = Objects[cur_object_index].instance; m_ship_name = Ships[player_ship].ship_name; + m_ship_display_name = Ships[player_ship].has_display_name() ? Ships[player_ship].get_display_name() : ""; m_ship_class_combo_index = ship_class_to_combo_index(Ships[player_ship].ship_info_index); m_team = Ships[player_ship].team; m_player_ship.SetCheck(TRUE); } else { // no ships or players selected.. m_ship_name = _T(""); + m_ship_display_name = _T(""); m_ship_class_combo_index = -1; m_team = -1; m_persona = -1; @@ -924,6 +932,7 @@ void CShipEditorDlg::initialize_data(int full_update) if (total_count) { GetDlgItem(IDC_SHIP_NAME)->EnableWindow(!multi_edit); + GetDlgItem(IDC_DISPLAY_NAME)->EnableWindow(!multi_edit); GetDlgItem(IDC_SHIP_CLASS)->EnableWindow(TRUE); GetDlgItem(IDC_SHIP_ALT)->EnableWindow(TRUE); GetDlgItem(IDC_INITIAL_STATUS)->EnableWindow(TRUE); @@ -935,6 +944,7 @@ void CShipEditorDlg::initialize_data(int full_update) GetDlgItem(IDC_SPECIAL_HITPOINTS)->EnableWindow(TRUE); } else { GetDlgItem(IDC_SHIP_NAME)->EnableWindow(FALSE); + GetDlgItem(IDC_DISPLAY_NAME)->EnableWindow(FALSE); GetDlgItem(IDC_SHIP_CLASS)->EnableWindow(FALSE); GetDlgItem(IDC_SHIP_ALT)->EnableWindow(FALSE); GetDlgItem(IDC_INITIAL_STATUS)->EnableWindow(FALSE); @@ -1082,7 +1092,22 @@ int CShipEditorDlg::update_data(int redraw) } else if (single_ship >= 0) { // editing a single ship m_ship_name.TrimLeft(); - m_ship_name.TrimRight(); + m_ship_name.TrimRight(); + if (m_ship_name.IsEmpty()) { + if (bypass_errors) + return 1; + + bypass_errors = 1; + z = MessageBox("A ship name cannot be empty\n" + "Press OK to restore old name", "Error", MB_ICONEXCLAMATION | MB_OKCANCEL); + + if (z == IDCANCEL) + return -1; + + m_ship_name = _T(Ships[single_ship].ship_name); + UpdateData(FALSE); + } + ptr = GET_FIRST(&obj_used_list); while (ptr != END_OF_LIST(&obj_used_list)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (single_ship != ptr->instance)) { @@ -1230,11 +1255,6 @@ int CShipEditorDlg::update_data(int redraw) strcpy_s(Reinforcements[i].name, str); } - if (Ships[single_ship].has_display_name()) { - Ships[single_ship].flags.remove(Ship::Ship_Flags::Has_display_name); - Ships[single_ship].display_name = ""; - } - Update_window = 1; } } @@ -1271,6 +1291,22 @@ int CShipEditorDlg::update_ship(int ship) CComboBox *box; int persona; + // the display name was precalculated, so now just assign it + if (m_ship_display_name == m_ship_name || m_ship_display_name.CompareNoCase("") == 0) + { + if (Ships[ship].flags[Ship::Ship_Flags::Has_display_name]) + set_modified(); + Ships[ship].display_name = ""; + Ships[ship].flags.remove(Ship::Ship_Flags::Has_display_name); + } + else + { + if (!Ships[ship].flags[Ship::Ship_Flags::Has_display_name]) + set_modified(); + Ships[ship].display_name = m_ship_display_name; + Ships[ship].flags.set(Ship::Ship_Flags::Has_display_name); + } + // THIS DIALOG IS THE SOME OF THE WORST CODE I HAVE EVER SEEN IN MY ENTIRE LIFE. // IT TOOK A RIDICULOUSLY LONG AMOUNT OF TIME TO ADD 2 FUNCTIONS. OMG ship_alt_name_close(ship); @@ -1618,6 +1654,18 @@ void CShipEditorDlg::OnGoals() MessageBox("This ship's wing also has initial orders", "Possible conflict"); } +void CShipEditorDlg::OnChangeShipName() +{ + // sync the edit box to the variable + UpdateData(TRUE); + + // automatically determine or reset the display name + m_ship_display_name = get_display_name_for_text_box(m_ship_name); + + // sync the variable to the edit box + UpdateData(FALSE); +} + void CShipEditorDlg::OnSelchangeShipClass() { object *ptr; diff --git a/fred2/shipeditordlg.h b/fred2/shipeditordlg.h index 80940dce09a..272c8b5399f 100644 --- a/fred2/shipeditordlg.h +++ b/fred2/shipeditordlg.h @@ -104,6 +104,7 @@ class CShipEditorDlg : public CDialog sexp_tree m_arrival_tree; sexp_tree m_departure_tree; CString m_ship_name; + CString m_ship_display_name; CString m_cargo1; int m_ship_class_combo_index; int m_team; @@ -148,6 +149,7 @@ class CShipEditorDlg : public CDialog afx_msg void OnEndlabeleditArrivalTree(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnEndlabeleditDepartureTree(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnGoals(); + afx_msg void OnChangeShipName(); afx_msg void OnSelchangeShipClass(); afx_msg void OnInitialStatus(); afx_msg void OnWeapons(); diff --git a/fred2/wing_editor.cpp b/fred2/wing_editor.cpp index 058f23f68c3..b6dac0ed2c0 100644 --- a/fred2/wing_editor.cpp +++ b/fred2/wing_editor.cpp @@ -1189,7 +1189,7 @@ void wing_editor::calc_cue_height() CRect cue; GetDlgItem(IDC_CUE_FRAME)->GetWindowRect(cue); - cue_height = (cue.bottom - cue.top) + 10; + cue_height = (cue.bottom - cue.top) + 1; } void wing_editor::show_hide_sexp_help() diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index 85c5fe7d091..ba1cd31c85d 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -3325,6 +3325,19 @@ void Editor::lcl_fred_replace_stuff(QString& text) text.replace("\\", "$backslash"); } +SCP_string Editor::get_display_name_for_text_box(const SCP_string &orig_name) +{ + auto index = get_index_of_first_hash_symbol(orig_name); + if (index >= 0) + { + SCP_string display_name(orig_name); + end_string_at_first_hash_symbol(display_name); + return display_name; + } + else + return ""; +} + SCP_vector Editor::getStartingWingLoadoutUseCounts() { // update before sending so that we have the most up to date info. updateStartingWingLoadoutUseCounts(); diff --git a/qtfred/src/mission/Editor.h b/qtfred/src/mission/Editor.h index e7f1187c632..0dcadc3abe6 100644 --- a/qtfred/src/mission/Editor.h +++ b/qtfred/src/mission/Editor.h @@ -188,6 +188,7 @@ class Editor : public QObject { static void strip_quotation_marks(SCP_string& str); static void pad_with_newline(SCP_string& str, size_t max_size); static void lcl_fred_replace_stuff(QString& text); + static SCP_string get_display_name_for_text_box(const SCP_string &orig_name); SCP_vector getStartingWingLoadoutUseCounts(); diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp index 68f28a2d20e..0e6f575cda2 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp @@ -169,9 +169,11 @@ namespace fso { if (!multi_edit) { Assert((ship_count == 1) && (base_ship >= 0)); _m_ship_name = Ships[base_ship].ship_name; + _m_ship_display_name = Ships[base_ship].has_display_name() ? Ships[base_ship].get_display_name() : ""; } else { _m_ship_name = ""; + _m_ship_display_name = ""; } _m_update_arrival = _m_update_departure = true; @@ -348,6 +350,7 @@ namespace fso { if (player_count > 1) { // multiple player ships selected Assert(base_player >= 0); _m_ship_name = ""; + _m_ship_display_name = ""; _m_player_ship = true; objp = GET_FIRST(&obj_used_list); while (objp != END_OF_LIST(&obj_used_list)) { @@ -375,6 +378,7 @@ namespace fso { // Assert((player_count == 1) && !multi_edit); player_ship = Objects[_editor->currentObject].instance; _m_ship_name = Ships[player_ship].ship_name; + _m_ship_display_name = Ships[player_ship].has_display_name() ? Ships[player_ship].get_display_name() : ""; _m_ship_class = Ships[player_ship].ship_info_index; _m_team = Ships[player_ship].team; _m_player_ship = true; @@ -382,6 +386,7 @@ namespace fso { } else { // no ships or players selected.. _m_ship_name = ""; + _m_ship_display_name = ""; _m_ship_class = -1; _m_team = -1; _m_persona = -1; @@ -428,6 +433,20 @@ namespace fso { } else if (single_ship >= 0) { // editing a single ship drop_white_space(_m_ship_name); + if (_m_ship_name.empty()) { + auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, + "Ship Name Error", + "A ship name cannot be empty.\n Press OK to restore old name", + { DialogButton::Ok, DialogButton::Cancel }); + if (button == DialogButton::Cancel) { + return false; + } + else { + _m_ship_name = Ships[single_ship].ship_name; + modelChanged(); + } + } + ptr = GET_FIRST(&obj_used_list); while (ptr != END_OF_LIST(&obj_used_list)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (single_ship != ptr->instance)) { @@ -579,11 +598,6 @@ namespace fso { } } - if (Ships[single_ship].has_display_name()) { - Ships[single_ship].flags.remove(Ship::Ship_Flags::Has_display_name); - Ships[single_ship].display_name = ""; - } - _editor->missionChanged(); } } @@ -622,6 +636,22 @@ namespace fso { int z, d; SCP_string str; + // the display name was precalculated, so now just assign it + if (_m_ship_display_name == _m_ship_name || stricmp(_m_ship_display_name.c_str(), "") == 0) + { + if (Ships[ship].flags[Ship::Ship_Flags::Has_display_name]) + set_modified(); + Ships[ship].display_name = ""; + Ships[ship].flags.remove(Ship::Ship_Flags::Has_display_name); + } + else + { + if (!Ships[ship].flags[Ship::Ship_Flags::Has_display_name]) + set_modified(); + Ships[ship].display_name = _m_ship_display_name; + Ships[ship].flags.set(Ship::Ship_Flags::Has_display_name); + } + ship_alt_name_close(ship); ship_callsign_close(ship); @@ -870,7 +900,8 @@ namespace fso { "Couldn't add new Callsign. Already using too many!", { DialogButton::Ok }); } - void ShipEditorDialogModel::setShipName(const SCP_string m_ship_name) + + void ShipEditorDialogModel::setShipName(const SCP_string &m_ship_name) { modify(_m_ship_name, m_ship_name); } @@ -879,6 +910,15 @@ namespace fso { return _m_ship_name; } + void ShipEditorDialogModel::setShipDisplayName(const SCP_string &m_ship_display_name) + { + modify(_m_ship_display_name, m_ship_display_name); + } + SCP_string ShipEditorDialogModel::getShipDisplayName() const + { + return _m_ship_display_name; + } + void ShipEditorDialogModel::setShipClass(int m_ship_class) { object* ptr; diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h index 5a2ac86f0a9..27fae31e081 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h @@ -21,6 +21,7 @@ class ShipEditorDialogModel : public AbstractDialogModel { int _m_departure_tree_formula; int _m_arrival_tree_formula; SCP_string _m_ship_name; + SCP_string _m_ship_display_name; SCP_string _m_cargo1; SCP_string _m_alt_name; SCP_string _m_callsign; @@ -76,9 +77,12 @@ class ShipEditorDialogModel : public AbstractDialogModel { bool apply() override; void reject() override; - void setShipName(const SCP_string m_ship_name); + void setShipName(const SCP_string &m_ship_name); SCP_string getShipName() const; + void setShipDisplayName(const SCP_string &m_ship_display_name); + SCP_string getShipDisplayName() const; + void setShipClass(const int); int getShipClass() const; diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index d69dc3a7b5d..9f371c81960 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -29,6 +29,7 @@ ShipEditorDialog::ShipEditorDialog(FredView* parent, EditorViewport* viewport) // Column One connect(ui->shipNameEdit, (&QLineEdit::editingFinished), this, &ShipEditorDialog::shipNameChanged); + connect(ui->shipDisplayNameEdit, (&QLineEdit::editingFinished), this, &ShipEditorDialog::shipDisplayNameChanged); connect(ui->shipClassCombo, static_cast(&QComboBox::currentIndexChanged), @@ -223,6 +224,7 @@ void ShipEditorDialog::updateColumnOne() { util::SignalBlockers blockers(this); ui->shipNameEdit->setText(_model->getShipName().c_str()); + ui->shipDisplayNameEdit->setText(_model->getShipDisplayName().c_str()); size_t i; auto idx = _model->getShipClass(); ui->shipClassCombo->clear(); @@ -555,6 +557,7 @@ void ShipEditorDialog::enableDisable() if (_model->getNumSelectedObjects()) { ui->shipNameEdit->setEnabled(!_model->getIfMultipleShips()); + ui->shipDisplayNameEdit->setEnabled(!_model->getIfMultipleShips()); ui->shipClassCombo->setEnabled(true); ui->altNameCombo->setEnabled(true); ui->initialStatusButton->setEnabled(true); @@ -565,6 +568,7 @@ void ShipEditorDialog::enableDisable() ui->specialStatsButton->setEnabled(true); } else { ui->shipNameEdit->setEnabled(false); + ui->shipDisplayNameEdit->setEnabled(false); ui->shipClassCombo->setEnabled(false); ui->altNameCombo->setEnabled(false); ui->initialStatusButton->setEnabled(false); @@ -659,7 +663,7 @@ void ShipEditorDialog::enableDisable() /*--------------------------------------------------------- WARNING -Do not try to optimise string entries this convoluted method is necessary to avoid fata errors caused by QT +Do not try to optimise string entries; this convoluted method is necessary to avoid fatal errors caused by QT -----------------------------------------------------------*/ void ShipEditorDialog::shipNameChanged() { @@ -669,6 +673,21 @@ void ShipEditorDialog::shipNameChanged() const std::string NewShipName = textBytes.toStdString(); _model->setShipName(NewShipName); } + + // automatically determine or reset the display name + _model->setShipDisplayName(Editor::get_display_name_for_text_box(_model->getShipName())); + + // sync the variable to the edit box + ui->shipDisplayNameEdit->setText(_model->getShipDisplayName().c_str()); +} +void ShipEditorDialog::shipDisplayNameChanged() +{ + const QString entry = ui->shipDisplayNameEdit->text(); + if (entry != _model->getShipDisplayName().c_str()) { + const auto textBytes = entry.toUtf8(); + const std::string NewShipDisplayName = textBytes.toStdString(); + _model->setShipDisplayName(NewShipDisplayName); + } } void ShipEditorDialog::shipClassChanged(const int index) { diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h index 39869539d36..6d6fe3ab5d1 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h @@ -101,6 +101,7 @@ class ShipEditorDialog : public QDialog, public SexpTreeEditorInterface { //column one void shipNameChanged(); + void shipDisplayNameChanged(); void shipClassChanged(const int); void aiClassChanged(const int); void teamChanged(const int); diff --git a/qtfred/ui/ShipEditorDialog.ui b/qtfred/ui/ShipEditorDialog.ui index 919134dcf3f..fa989e19395 100644 --- a/qtfred/ui/ShipEditorDialog.ui +++ b/qtfred/ui/ShipEditorDialog.ui @@ -37,76 +37,76 @@ - - + + - Team + Ship Name - - + + - <html><head/><body><p>Sets the Ship's AI</p></body></html> + <html><head/><body><p>Sets the ship's name</p></body></html> - - - - <html><head/><body><p>Type to add Ship's Cargo or select previous</p></body></html> - - - true + + + + Display Name - + - <html><head/><body><p>Sets the ship's class</p></body></html> + <html><head/><body><p>Sets the ship's display name</p></body></html> - - - - <html><head/><body><p>Sets the ship's team</p></body></html> + + + + Callsign - - + + - <html><head/><body><p>Sets the ship's name</p></body></html> + <html><head/><body><p>Sets ship's callsign (Replaces name in messages)</p></body></html> + + + true - - + + - Cargo + Ship Class - - - - AI Class + + + + <html><head/><body><p>Sets the ship's class</p></body></html> - - + + - Ship Name + Alt Name - - + + - <html><head/><body><p>Sets ship's callsign (Replaces name in messages)</p></body></html> + <html><head/><body><p>Change ship's class name (i.e. GTF Ulysses -&gt; NTF Ulysses)</p></body></html> true @@ -114,30 +114,44 @@ - + - Alt Name + AI Class - - - - Ship Class + + + + <html><head/><body><p>Sets the Ship's AI</p></body></html> - + - Callsign + Team - - + + - <html><head/><body><p>Change ship's class name (i.e. GTF Ulysses -&gt; NTF Ulysses)</p></body></html> + <html><head/><body><p>Sets the ship's team</p></body></html> + + + + + + + Cargo + + + + + + + <html><head/><body><p>Type to add Ship's Cargo or select previous</p></body></html> true From 672aa7f81f10edaedc459e721c6205272f06df2b Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 6 Jul 2025 23:50:19 -0400 Subject: [PATCH 225/466] apply display name updates to jump nodes too --- code/jumpnode/jumpnode.cpp | 9 +++++++-- fred2/jumpnodedlg.cpp | 33 +++++++++++++++++++++++++-------- fred2/jumpnodedlg.h | 2 +- 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/code/jumpnode/jumpnode.cpp b/code/jumpnode/jumpnode.cpp index 183925e3d7c..1b2112e750b 100644 --- a/code/jumpnode/jumpnode.cpp +++ b/code/jumpnode/jumpnode.cpp @@ -285,6 +285,11 @@ void CJumpNode::SetName(const char *new_name) end_string_at_first_hash_symbol(m_display); m_flags |= JN_HAS_DISPLAY_NAME; } + else + { + *m_display = '\0'; + m_flags &= ~JN_HAS_DISPLAY_NAME; + } } /** @@ -296,8 +301,8 @@ void CJumpNode::SetDisplayName(const char *new_display_name) { Assert(new_display_name != NULL); - // if display name is blank or matches the actual name, clear it - if (*new_display_name == '\0' || !stricmp(new_display_name, m_name)) + // if display name matches the actual name, clear it + if (stricmp(new_display_name, m_name) == 0) { *m_display = '\0'; m_flags &= ~JN_HAS_DISPLAY_NAME; diff --git a/fred2/jumpnodedlg.cpp b/fred2/jumpnodedlg.cpp index 071d7651306..eb934f2ed68 100644 --- a/fred2/jumpnodedlg.cpp +++ b/fred2/jumpnodedlg.cpp @@ -68,7 +68,7 @@ BEGIN_MESSAGE_MAP(jumpnode_dlg, CDialog) ON_BN_CLICKED(IDC_NODE_HIDDEN, OnHidden) ON_WM_CLOSE() ON_WM_INITMENU() - ON_EN_KILLFOCUS(IDC_NAME, OnKillfocusName) + ON_EN_CHANGE(IDC_NAME, OnChangeName) //}}AFX_MSG_MAP END_MESSAGE_MAP() @@ -134,7 +134,7 @@ void jumpnode_dlg::initialize_data(int full_update) if (Objects[cur_object_index].type == OBJ_JUMP_NODE) { auto jnp = jumpnode_get_by_objnum(cur_object_index); m_name = _T(jnp->GetName()); - m_display = _T(jnp->GetDisplayName()); + m_display = _T(jnp->HasDisplayName() ? jnp->GetDisplayName() : ""); int model = jnp->GetModelNumber(); polymodel* pm = model_get(model); @@ -179,6 +179,24 @@ int jumpnode_dlg::update_data() if (query_valid_object() && Objects[cur_object_index].type == OBJ_JUMP_NODE) { auto jnp = jumpnode_get_by_objnum(cur_object_index); + m_name.TrimLeft(); + m_name.TrimRight(); + if (m_name.IsEmpty()) + { + if (bypass_errors) + return 1; + + bypass_errors = 1; + z = MessageBox("A jump node name cannot be empty\n" + "Press OK to restore old name", "Error", MB_ICONEXCLAMATION | MB_OKCANCEL); + + if (z == IDCANCEL) + return -1; + + m_name = _T(jnp->GetName()); + UpdateData(FALSE); + } + for (i=0; iGetName()); jnp->SetName((LPCSTR) m_name); - jnp->SetDisplayName((LPCSTR) m_display); + jnp->SetDisplayName((m_display.CompareNoCase("") == 0) ? m_name : m_display); int model = jnp->GetModelNumber(); polymodel* pm = model_get(model); @@ -369,7 +387,7 @@ void jumpnode_dlg::OnHidden() ((CButton*)GetDlgItem(IDC_NODE_HIDDEN))->SetCheck(m_hidden); } -void jumpnode_dlg::OnKillfocusName() +void jumpnode_dlg::OnChangeName() { char buffer[NAME_LENGTH]; @@ -378,10 +396,9 @@ void jumpnode_dlg::OnKillfocusName() // grab the name GetDlgItemText(IDC_NAME, buffer, NAME_LENGTH); - // if this name has a hash, truncate it for the display name - if (get_pointer_to_first_hash_symbol(buffer)) - end_string_at_first_hash_symbol(buffer); + // automatically determine or reset the display name + auto display_name = get_display_name_for_text_box(buffer); // set the display name derived from this name - SetDlgItemText(IDC_DISPLAY_NAME, buffer); + SetDlgItemText(IDC_DISPLAY_NAME, (LPCTSTR)display_name); } diff --git a/fred2/jumpnodedlg.h b/fred2/jumpnodedlg.h index 92a0cb53429..2919e174a3a 100644 --- a/fred2/jumpnodedlg.h +++ b/fred2/jumpnodedlg.h @@ -55,7 +55,7 @@ class jumpnode_dlg : public CDialog afx_msg void OnInitMenu(CMenu* pMenu); afx_msg void OnClose(); afx_msg void OnHidden(); - afx_msg void OnKillfocusName(); + afx_msg void OnChangeName(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; From 650b8d4f072a1b4e700d33686f0d93e1a44e8b63 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Tue, 8 Jul 2025 11:05:11 +0200 Subject: [PATCH 226/466] Disable uneccesary raycast (#6811) --- code/ai/aibig.cpp | 4 ++++ code/mod_table/mod_table.cpp | 7 +++++++ code/mod_table/mod_table.h | 1 + 3 files changed, 12 insertions(+) diff --git a/code/ai/aibig.cpp b/code/ai/aibig.cpp index 934bd5f479f..c2831654394 100644 --- a/code/ai/aibig.cpp +++ b/code/ai/aibig.cpp @@ -146,6 +146,10 @@ void ai_bpap(const object *objp, const vec3d *attacker_objp_pos, const vec3d *at *attack_point = best_point; + //The following raycast tends to make up 10%(!) of total AI-frametime for basically no benefit, especially in the common case for turrets where the normal isn't even queried. + if (surface_normal == nullptr && Disable_expensive_turret_target_check) + return; + // Cast from attack_objp_pos to local_attack_pos and check for nearest collision. // If no collision, cast to (0,0,0) [center of big ship]** [best_point initialized to 000] diff --git a/code/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index bce433f2ced..ffec4fcaa41 100644 --- a/code/mod_table/mod_table.cpp +++ b/code/mod_table/mod_table.cpp @@ -171,6 +171,7 @@ EscapeKeyBehaviorInOptions escape_key_behavior_in_options; bool Fix_asteroid_bounding_box_check; bool Disable_intro_movie; bool Show_locked_status_scramble_missions; +bool Disable_expensive_turret_target_check; #ifdef WITH_DISCORD @@ -1534,6 +1535,10 @@ void parse_mod_table(const char *filename) stuff_boolean(&Show_locked_status_scramble_missions); } + if (optional_string("$Disable expensive turret target check:")) { + stuff_boolean(&Disable_expensive_turret_target_check); + } + // end of options ---------------------------------------- // if we've been through once already and are at the same place, force a move @@ -1769,6 +1774,7 @@ void mod_table_reset() Fix_asteroid_bounding_box_check = false; Disable_intro_movie = false; Show_locked_status_scramble_missions = false; + Disable_expensive_turret_target_check = false; } void mod_table_set_version_flags() @@ -1794,5 +1800,6 @@ void mod_table_set_version_flags() Use_model_eyepoint_for_set_camera_host = true; Use_model_eyepoint_normals = true; Fix_asteroid_bounding_box_check = true; + Disable_expensive_turret_target_check = true; } } diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index 85878b6f1b8..244a1094eab 100644 --- a/code/mod_table/mod_table.h +++ b/code/mod_table/mod_table.h @@ -186,6 +186,7 @@ extern EscapeKeyBehaviorInOptions escape_key_behavior_in_options; extern bool Fix_asteroid_bounding_box_check; extern bool Disable_intro_movie; extern bool Show_locked_status_scramble_missions; +extern bool Disable_expensive_turret_target_check; void mod_table_init(); void mod_table_post_process(); From 8989b6e9259ac96d2a3addaeba437198e71e4d96 Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Tue, 8 Jul 2025 02:05:26 -0700 Subject: [PATCH 227/466] make change (#6809) --- code/utils/modular_curves.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/utils/modular_curves.h b/code/utils/modular_curves.h index 4def11b9b5e..d25b6c2e855 100644 --- a/code/utils/modular_curves.h +++ b/code/utils/modular_curves.h @@ -241,7 +241,7 @@ struct modular_curves_entry { int curve_idx = -1; ::util::ParsedRandomFloatRange scaling_factor = ::util::UniformFloatRange(1.f); ::util::ParsedRandomFloatRange translation = ::util::UniformFloatRange(0.f); - bool wraparound = true; + bool wraparound = false; }; // @@ -355,7 +355,7 @@ struct modular_curves_definition { curve_entry.translation = ::util::UniformFloatRange(0.0f); } - curve_entry.wraparound = true; + curve_entry.wraparound = false; parse_optional_bool_into("+Wraparound:", &curve_entry.wraparound); curves[static_cast>(output_idx)].emplace_back(input_idx, curve_entry); From 497b83476676f347df0747d474e0dee5e25d4e62 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Tue, 8 Jul 2025 14:26:43 +0200 Subject: [PATCH 228/466] Speed up random range creation (#6812) --- code/utils/RandomRange.h | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/code/utils/RandomRange.h b/code/utils/RandomRange.h index ed13709c08f..7ccf21c9e4a 100644 --- a/code/utils/RandomRange.h +++ b/code/utils/RandomRange.h @@ -77,10 +77,14 @@ class RandomRange { ValueType m_minValue; ValueType m_maxValue; + //Sampling a random_device is REALLY expensive. + //Instead of sampling one for each seed, create a pseudorandom seeder which is initialized ONCE from a random_device. + inline static std::mt19937 seeder {std::random_device()()}; + public: template =1 || !std::is_convertible::value) && !std::is_same_v, RandomRange>, int>::type> explicit RandomRange(T&& distributionFirstParameter, Ts&&... distributionParameters) - : m_generator(std::random_device()()), m_distribution(distributionFirstParameter, distributionParameters...) + : m_generator(seeder()), m_distribution(distributionFirstParameter, distributionParameters...) { m_minValue = static_cast(m_distribution.min()); m_maxValue = static_cast(m_distribution.max()); @@ -94,7 +98,7 @@ class RandomRange { m_constant = true; } - RandomRange() : m_generator(std::random_device()()), m_distribution() + RandomRange() : m_generator(seeder()), m_distribution() { m_minValue = static_cast(0.0); m_maxValue = static_cast(0.0); From 342958b99fbfc0397b5e0e79af74eb0facffd1d2 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 8 Jul 2025 22:31:56 -0400 Subject: [PATCH 229/466] fix qtfred memory corruption Fix a qtFRED memory access bug when checking for autosaved files. Passing a temporary variable to a function as a reference, then returning it, led to a dangling reference access. Rewrite the `maybeUseAutosave` function to avoid this. --- qtfred/src/mission/Editor.cpp | 10 ++++------ qtfred/src/mission/Editor.h | 2 +- qtfred/src/ui/FredView.cpp | 3 ++- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index 85c5fe7d091..229f3f20e97 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -130,18 +130,18 @@ void Editor::update() { } } -std::string Editor::maybeUseAutosave(const std::string& filepath) +void Editor::maybeUseAutosave(std::string& filepath) { // first, just grab the info of this mission if (!parse_main(filepath.c_str(), MPF_ONLY_MISSION_INFO)) - return filepath; + return; SCP_string created = The_mission.created; CFileLocation res = cf_find_file_location(filepath.c_str(), CF_TYPE_ANY); time_t modified = res.m_time; if (!res.found) { UNREACHABLE("Couldn't find path '%s' even though parse_main() succeeded!", filepath.c_str()); - return filepath; // just load the actual specified file + return; } // now check all the autosaves @@ -179,10 +179,8 @@ std::string Editor::maybeUseAutosave(const std::string& filepath) prompt.c_str(), { DialogButton::Yes, DialogButton::No }); if (z == DialogButton::Yes) - return backup_res.full_name.c_str(); + filepath = backup_res.full_name; // replace the specified file with the autosave file } - - return filepath; } bool Editor::loadMission(const std::string& mission_name, int flags) { diff --git a/qtfred/src/mission/Editor.h b/qtfred/src/mission/Editor.h index e7f1187c632..35cbe98ee8f 100644 --- a/qtfred/src/mission/Editor.h +++ b/qtfred/src/mission/Editor.h @@ -39,7 +39,7 @@ class Editor : public QObject { void createNewMission(); - std::string maybeUseAutosave(const std::string& filepath); + void maybeUseAutosave(std::string& filepath); /*! Load a mission. */ bool loadMission(const std::string& filepath, int flags = 0); diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 851a71faa1f..da9baeaaaea 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -143,7 +143,8 @@ void FredView::loadMissionFile(const QString& pathName) { try { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); - auto pathToLoad = fred->maybeUseAutosave(pathName.toStdString()); + auto pathToLoad = pathName.toStdString(); + fred->maybeUseAutosave(pathToLoad); fred->loadMission(pathToLoad); From bd767ece098425ab3209f4df299813c436392478 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 8 Jul 2025 22:35:45 -0400 Subject: [PATCH 230/466] qtfred cleanup Various cleanup associated with the `toStdString()` function in qtFRED. Now that `lcase_equal` exists, it is no longer necessary to compare the C strings. Additionally, QString has its own `isEmpty()` function. --- .../dialogs/CustomWingNamesDialogModel.cpp | 22 ++++---- .../ShipTextureReplacementDialogModel.cpp | 2 +- qtfred/src/ui/dialogs/LoadoutDialog.cpp | 54 +++++++++---------- .../ui/dialogs/ReinforcementsEditorDialog.cpp | 6 +-- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.cpp b/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.cpp index 4bbeb119389..bcfa960cd8e 100644 --- a/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.cpp +++ b/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.cpp @@ -26,7 +26,7 @@ bool CustomWingNamesDialogModel::apply() { Editor::strip_quotation_marks(wing); } - if (strcmp(_m_starting[0].c_str(), _m_tvt[0].c_str()) != 0) + if (_m_starting[0] != _m_tvt[0]) { auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Custom Wing Error", "The first starting wing and the first team-versus-team wing must have the same wing name.", { DialogButton::Ok }); @@ -35,8 +35,8 @@ bool CustomWingNamesDialogModel::apply() { } } - if (!stricmp(_m_starting[0].c_str(), _m_starting[1].c_str()) || !stricmp(_m_starting[0].c_str(), _m_starting[2].c_str()) - || !stricmp(_m_starting[1].c_str(), _m_starting[2].c_str())) + if (lcase_equal(_m_starting[0], _m_starting[1]) || lcase_equal(_m_starting[0], _m_starting[2]) + || lcase_equal(_m_starting[1], _m_starting[2])) { auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Custom Wing Error", "Duplicate wing names in starting wing list.", { DialogButton::Ok }); @@ -45,10 +45,10 @@ bool CustomWingNamesDialogModel::apply() { } } - if (!stricmp(_m_squadron[0].c_str(), _m_squadron[1].c_str()) || !stricmp(_m_squadron[0].c_str(), _m_squadron[2].c_str()) || !stricmp(_m_squadron[0].c_str(), _m_squadron[3].c_str()) || !stricmp(_m_squadron[0].c_str(), _m_squadron[4].c_str()) - || !stricmp(_m_squadron[1].c_str(), _m_squadron[2].c_str()) || !stricmp(_m_squadron[1].c_str(), _m_squadron[3].c_str()) || !stricmp(_m_squadron[1].c_str(), _m_squadron[4].c_str()) - || !stricmp(_m_squadron[2].c_str(), _m_squadron[3].c_str()) || !stricmp(_m_squadron[2].c_str(), _m_squadron[4].c_str()) - || !stricmp(_m_squadron[3].c_str(), _m_squadron[4].c_str())) + if (lcase_equal(_m_squadron[0], _m_squadron[1]) || lcase_equal(_m_squadron[0], _m_squadron[2]) || lcase_equal(_m_squadron[0], _m_squadron[3]) || lcase_equal(_m_squadron[0], _m_squadron[4]) + || lcase_equal(_m_squadron[1], _m_squadron[2]) || lcase_equal(_m_squadron[1], _m_squadron[3]) || lcase_equal(_m_squadron[1], _m_squadron[4]) + || lcase_equal(_m_squadron[2], _m_squadron[3]) || lcase_equal(_m_squadron[2], _m_squadron[4]) + || lcase_equal(_m_squadron[3], _m_squadron[4])) { auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Custom Wing Error", "Duplicate wing names in squadron wing list.", { DialogButton::Ok }); @@ -57,7 +57,7 @@ bool CustomWingNamesDialogModel::apply() { } } - if (!stricmp(_m_tvt[0].c_str(), _m_tvt[1].c_str())) + if (lcase_equal(_m_tvt[0], _m_tvt[1])) { auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Custom Wing Error", "Duplicate wing names in team-versus-team wing list.", { DialogButton::Ok }); @@ -134,9 +134,9 @@ SCP_string CustomWingNamesDialogModel::getTvTWing(int index) { } bool CustomWingNamesDialogModel::query_modified() { - return strcmp(Starting_wing_names[0], _m_starting[0].c_str()) != 0 || strcmp(Starting_wing_names[1], _m_starting[1].c_str()) != 0 || strcmp(Starting_wing_names[2], _m_starting[2].c_str()) != 0 - || strcmp(Squadron_wing_names[0], _m_squadron[0].c_str()) != 0 || strcmp(Squadron_wing_names[1], _m_squadron[1].c_str()) != 0 || strcmp(Squadron_wing_names[2], _m_squadron[2].c_str()) != 0 || strcmp(Squadron_wing_names[3], _m_squadron[3].c_str()) != 0 || strcmp(Squadron_wing_names[4], _m_squadron[4].c_str()) != 0 - || strcmp(TVT_wing_names[0], _m_tvt[0].c_str()) != 0 || strcmp(TVT_wing_names[1], _m_tvt[1].c_str()) != 0;; + return Starting_wing_names[0] != _m_starting[0] || Starting_wing_names[1] != _m_starting[1] || Starting_wing_names[2] != _m_starting[2] + || Squadron_wing_names[0] != _m_squadron[0] || Squadron_wing_names[1] != _m_squadron[1] || Squadron_wing_names[2] != _m_squadron[2] || Squadron_wing_names[3] != _m_squadron[3] || Squadron_wing_names[4] != _m_squadron[4] + || TVT_wing_names[0] != _m_tvt[0] || TVT_wing_names[1] != _m_tvt[1]; } } diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp index c935d602d6e..d5e14c9c016 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp @@ -86,7 +86,7 @@ namespace fso { for (size_t i = 0; i < defaultTextures.size(); i++) { // if match - if (!stricmp(defaultTextures[i].c_str(), pureName.c_str())) + if (lcase_equal(defaultTextures[i], pureName)) { SCP_string newText = Fred_texture_replacement.new_texture; npos = newText.find_last_of('-'); diff --git a/qtfred/src/ui/dialogs/LoadoutDialog.cpp b/qtfred/src/ui/dialogs/LoadoutDialog.cpp index 8952ad3d317..8a25edc2683 100644 --- a/qtfred/src/ui/dialogs/LoadoutDialog.cpp +++ b/qtfred/src/ui/dialogs/LoadoutDialog.cpp @@ -321,7 +321,7 @@ void LoadoutDialog::addShipButtonClicked() SCP_vector list; for (const auto& item : ui->listShipsNotUsed->selectedItems()){ - list.push_back(item->text().toStdString()); + list.emplace_back(item->text().toStdString()); } if (_mode == TABLE_MODE) { @@ -338,7 +338,7 @@ void LoadoutDialog::addWeaponButtonClicked() SCP_vector list; for (const auto& item: ui->listWeaponsNotUsed->selectedItems()){ - list.push_back(item->text().toStdString()); + list.emplace_back(item->text().toStdString()); } if (_mode == TABLE_MODE) { @@ -355,7 +355,7 @@ void LoadoutDialog::removeShipButtonClicked() SCP_vector list; for (const auto& item : ui->usedShipsList->selectedItems()){ - list.push_back(item->text().toStdString()); + list.emplace_back(item->text().toStdString()); } if (_mode == TABLE_MODE) { @@ -372,7 +372,7 @@ void LoadoutDialog::removeWeaponButtonClicked() SCP_vector list; for (const auto& item : ui->usedWeaponsList->selectedItems()){ - list.push_back(item->text().toStdString()); + list.emplace_back(item->text().toStdString()); } if (_mode == TABLE_MODE) { @@ -578,7 +578,7 @@ void LoadoutDialog::updateUI() bool found = false; for (int x = 0; x < ui->usedShipsList->rowCount(); ++x){ - if (ui->usedShipsList->item(x,0) && !stricmp(ui->usedShipsList->item(x, 0)->text().toStdString().c_str(), shipName.c_str())) { + if (ui->usedShipsList->item(x,0) && lcase_equal(ui->usedShipsList->item(x, 0)->text().toStdString(), shipName)) { found = true; // update the quantities here, and make sure it's visible ui->usedShipsList->item(x, 1)->setText(newShip.first.substr(divider + 1).c_str()); @@ -600,7 +600,7 @@ void LoadoutDialog::updateUI() // remove from the unused list for (int x = 0; x < ui->listShipsNotUsed->count(); ++x) { - if (!stricmp(ui->listShipsNotUsed->item(x)->text().toStdString().c_str(), shipName.c_str())) { + if (lcase_equal(ui->listShipsNotUsed->item(x)->text().toStdString(), shipName)) { ui->listShipsNotUsed->setRowHidden(x, true); break; } @@ -610,7 +610,7 @@ void LoadoutDialog::updateUI() bool found = false; for (int x = 0; x < ui->listShipsNotUsed->count(); ++x){ - if (!stricmp(ui->listShipsNotUsed->item(x)->text().toStdString().c_str(), shipName.c_str())) { + if (lcase_equal(ui->listShipsNotUsed->item(x)->text().toStdString(), shipName)) { found = true; ui->listShipsNotUsed->setRowHidden(x, false); break; @@ -624,7 +624,7 @@ void LoadoutDialog::updateUI() // remove from the used list for (int x = 0; x < ui->usedShipsList->rowCount(); ++x) { if (ui->usedShipsList->item(x, 0) && - !stricmp(ui->usedShipsList->item(x, 0)->text().toStdString().c_str(), shipName.c_str())) { + lcase_equal(ui->usedShipsList->item(x, 0)->text().toStdString(), shipName)) { ui->usedShipsList->setRowHidden(x, true); break; } @@ -641,7 +641,7 @@ void LoadoutDialog::updateUI() // Add or update in the used list for (int x = 0; x < ui->usedWeaponsList->rowCount(); ++x) { - if (ui->usedWeaponsList->item(x,0) && !stricmp(ui->usedWeaponsList->item(x, 0)->text().toStdString().c_str(), weaponName.c_str())) { + if (ui->usedWeaponsList->item(x,0) && lcase_equal(ui->usedWeaponsList->item(x, 0)->text().toStdString(), weaponName)) { found = true; // only need to update the quantities here. ui->usedWeaponsList->item(x, 1)->setText(newWeapon.first.substr(divider + 1).c_str()); @@ -664,7 +664,7 @@ void LoadoutDialog::updateUI() // remove from the unused list for (int x = 0; x < ui->listWeaponsNotUsed->count(); ++x) { - if (!stricmp(ui->listWeaponsNotUsed->item(x)->text().toStdString().c_str(), weaponName.c_str())) { + if (lcase_equal(ui->listWeaponsNotUsed->item(x)->text().toStdString(), weaponName)) { ui->listWeaponsNotUsed->setRowHidden(x, true); break; } @@ -674,7 +674,7 @@ void LoadoutDialog::updateUI() bool found = false; for (int x = 0; x < ui->listWeaponsNotUsed->count(); ++x){ - if (ui->listWeaponsNotUsed->item(x) && !stricmp(ui->listWeaponsNotUsed->item(x)->text().toStdString().c_str(), weaponName.c_str())) { + if (ui->listWeaponsNotUsed->item(x) && lcase_equal(ui->listWeaponsNotUsed->item(x)->text().toStdString(), weaponName)) { found = true; ui->listWeaponsNotUsed->setRowHidden(x, false); break; @@ -688,7 +688,7 @@ void LoadoutDialog::updateUI() // remove from the used list for (int x = 0; x < ui->usedWeaponsList->rowCount(); ++x) { if (ui->usedWeaponsList->item(x, 0) && - !stricmp(ui->usedWeaponsList->item(x, 0)->text().toStdString().c_str(), weaponName.c_str())) { + lcase_equal(ui->usedWeaponsList->item(x, 0)->text().toStdString(), weaponName)) { ui->usedWeaponsList->setRowHidden(x, true); break; } @@ -699,9 +699,9 @@ void LoadoutDialog::updateUI() // Go through the lists and make sure that we don't have random empty entries for (int x = 0; x < ui->listShipsNotUsed->count(); ++x) { - if (ui->listShipsNotUsed->item(x) && !strlen(ui->listShipsNotUsed->item(x)->text().toStdString().c_str())) { + if (ui->listShipsNotUsed->item(x) && ui->listShipsNotUsed->item(x)->text().isEmpty()) { for (int y = x + 1; y < ui->listShipsNotUsed->count(); ++y) { - if (ui->listShipsNotUsed->item(y) && strlen(ui->listShipsNotUsed->item(y)->text().toStdString().c_str())) { + if (ui->listShipsNotUsed->item(y) && !ui->listShipsNotUsed->item(y)->text().isEmpty()) { ui->listShipsNotUsed->item(x)->setText(ui->listShipsNotUsed->item(y)->text()); ui->listShipsNotUsed->item(y)->setText(""); break; @@ -712,9 +712,9 @@ void LoadoutDialog::updateUI() for (int x = 0; x < ui->listWeaponsNotUsed->count(); ++x) { - if (ui->listWeaponsNotUsed->item(x) && !strlen(ui->listWeaponsNotUsed->item(x)->text().toStdString().c_str())) { + if (ui->listWeaponsNotUsed->item(x) && ui->listWeaponsNotUsed->item(x)->text().isEmpty()) { for (int y = x + 1; y < ui->listWeaponsNotUsed->count(); ++y) { - if (ui->listWeaponsNotUsed->item(y) && strlen(ui->listWeaponsNotUsed->item(y)->text().toStdString().c_str())) { + if (ui->listWeaponsNotUsed->item(y) && !ui->listWeaponsNotUsed->item(y)->text().isEmpty()) { ui->listWeaponsNotUsed->item(x)->setText(ui->listWeaponsNotUsed->item(y)->text()); ui->listWeaponsNotUsed->item(y)->setText(""); break; @@ -724,10 +724,10 @@ void LoadoutDialog::updateUI() } for (int x = 0; x < ui->usedShipsList->rowCount(); ++x) { - if (ui->usedShipsList->item(x, 0) && !strlen(ui->usedShipsList->item(x, 0)->text().toStdString().c_str()) && ui->usedShipsList->item(x, 1)) { + if (ui->usedShipsList->item(x, 0) && ui->usedShipsList->item(x, 0)->text().isEmpty() && ui->usedShipsList->item(x, 1)) { for (int y = x + 1; y < ui->usedShipsList->rowCount(); ++y) { - if (ui->usedShipsList->item(y, 0) && strlen(ui->usedShipsList->item(y, 0)->text().toStdString().c_str()) - && ui->usedShipsList->item(y, 1) && strlen(ui->usedShipsList->item(y, 1)->text().toStdString().c_str())) { + if (ui->usedShipsList->item(y, 0) && !ui->usedShipsList->item(y, 0)->text().isEmpty() + && ui->usedShipsList->item(y, 1) && !ui->usedShipsList->item(y, 1)->text().isEmpty()) { ui->usedShipsList->item(x, 0)->setText(ui->usedShipsList->item(y, 0)->text()); ui->usedShipsList->item(y, 0)->setText(""); @@ -741,11 +741,11 @@ void LoadoutDialog::updateUI() } for (int x = 0; x < ui->usedWeaponsList->rowCount(); ++x) { - if (ui->usedWeaponsList->item(x, 0) && !strlen(ui->usedWeaponsList->item(x, 0)->text().toStdString().c_str()) && + if (ui->usedWeaponsList->item(x, 0) && ui->usedWeaponsList->item(x, 0)->text().isEmpty() && ui->usedWeaponsList->item(x, 1)) { for (int y = x + 1; y < ui->usedWeaponsList->rowCount(); ++y) { - if (ui->usedWeaponsList->item(y, 0) && strlen(ui->usedWeaponsList->item(y, 0)->text().toStdString().c_str()) - && ui->usedWeaponsList->item(y, 1) && strlen(ui->usedWeaponsList->item(y, 1)->text().toStdString().c_str())) { + if (ui->usedWeaponsList->item(y, 0) && !ui->usedWeaponsList->item(y, 0)->text().isEmpty() + && ui->usedWeaponsList->item(y, 1) && !ui->usedWeaponsList->item(y, 1)->text().isEmpty()) { ui->usedWeaponsList->item(x, 0)->setText(ui->usedWeaponsList->item(y, 0)->text()); ui->usedWeaponsList->item(y, 0)->setText(""); @@ -832,8 +832,8 @@ void LoadoutDialog::updateUI() ui->extraItemsViaVariableCombo->setCurrentIndex(0); } else { for (int x = 0; x < ui->extraItemsViaVariableCombo->count(); ++x) { - if (!stricmp(ui->extraItemsViaVariableCombo->itemText(x).toStdString().c_str(), - currentVariable.c_str())) { + if (lcase_equal(ui->extraItemsViaVariableCombo->itemText(x).toStdString(), + currentVariable)) { ui->extraItemsViaVariableCombo->setCurrentIndex(x); break; } @@ -850,7 +850,7 @@ void LoadoutDialog::updateUI() bool found = false; for (const auto& weapon : requiredWeapons) { - if (ui->usedWeaponsList->item(x, 0) && ui->usedWeaponsList->item(x,2) && !stricmp(ui->usedWeaponsList->item(x, 0)->text().toStdString().c_str(), weapon.c_str())) { + if (ui->usedWeaponsList->item(x, 0) && ui->usedWeaponsList->item(x,2) && lcase_equal(ui->usedWeaponsList->item(x, 0)->text().toStdString(), weapon)) { found = true; ui->usedWeaponsList->item(x, 2)->setText("Yes"); break; @@ -870,7 +870,7 @@ SCP_vector LoadoutDialog::getSelectedShips() for (int x = 0; x < ui->usedShipsList->rowCount(); ++x) { if (ui->usedShipsList->item(x, 0) && ui->usedShipsList->item(x,0)->isSelected()) { - namesOut.emplace_back(ui->usedShipsList->item(x, 0)->text().toStdString().c_str()); + namesOut.emplace_back(ui->usedShipsList->item(x, 0)->text().toStdString()); } } @@ -883,7 +883,7 @@ SCP_vector LoadoutDialog::getSelectedWeapons() for (int x = 0; x < ui->usedWeaponsList->rowCount(); ++x) { if (ui->usedWeaponsList->item(x, 0) && ui->usedWeaponsList->item(x, 0)->isSelected()) { - namesOut.emplace_back(ui->usedWeaponsList->item(x, 0)->text().toStdString().c_str()); + namesOut.emplace_back(ui->usedWeaponsList->item(x, 0)->text().toStdString()); } } diff --git a/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp b/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp index 5021ae806fd..a2cb01c96ad 100644 --- a/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp @@ -119,7 +119,7 @@ namespace dialogs { { SCP_vector listOut; for (auto& currentItem : ui->chosenShipsList->selectedItems()){ - listOut.push_back(currentItem->text().toStdString()); + listOut.emplace_back(currentItem->text().toStdString()); } if (ui->chosenShipsList->selectedItems().count() > 0) { @@ -156,7 +156,7 @@ namespace dialogs { for (int i = 0; i < ui->chosenShipsList->count(); i++) { auto current = ui->chosenShipsList->item(i); if (current->isSelected()) { - selectedItems.push_back(current->text().toStdString()); + selectedItems.emplace_back(current->text().toStdString()); } } @@ -173,7 +173,7 @@ namespace dialogs { for (int i = 0; i < ui->possibleShipsList->count(); i++) { auto current = ui->possibleShipsList->item(i); if (current->isSelected()) { - selectedItems.push_back(current->text().toStdString()); + selectedItems.emplace_back(current->text().toStdString()); } } From 82dfdbc96222a1960c0c24447b9b360b7098d980 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:47:14 +0200 Subject: [PATCH 231/466] Cleanup of Level 0 Particles (#6808) * Make scripting use the full-blown particle system * Add reverse option * Add animation reversal as an option to modern particles * Start implementation of beam particles * Add multi-emission particle source API for legacy particles * Attach beam muzzle particles to the correct host * Reorder code in order to facilitate apparent visual size as modular curve input for particles * fix lua api * Typo * Fix scripting and beam muzzle particles * Remove muzzleflash code, prepare to replace with particles * Begin mflash effect conversion * Pass final parameters to mflashes and fix curve * Remove deprecated particle function * Add debris flame particles * remove weapon_explosion table * Convert shield explosion particles * Fix modular curve full inputs and convert particle curve inputs accordingly * Allow particle volume offset and rotation with curve-based rot around fvec * Add Ring and Point Volumes * Convert PSPEW to new system * Make particle curve timing aware of interp * Update todos * get rid of performance waste * Add last parameter for converted pspews * Fix pspew bugs * Fix direction for default pspew * Fix order for voluem rotation * particle vel interp * particle vel interp 2 * Make beam muzzle stay parented to the firing ship, as it's likely the more commonly intended variamnt * fix scripting particle lifetime * Allow beam muzzle particle override * Skip invalid pspews * remove pspew dcf code * Fix sparkler stretch * Correct sparkler bia * General sparkler weirdness * Tune distribution of sparklers * Convert Asteroid particles * Fix conversion warning * Fix conversion warning again * Fix conversion warning again again * Really clang? --- code/ai/aiturret.cpp | 6 - code/asteroid/asteroid.cpp | 64 +- code/debris/debris.cpp | 17 + code/globalincs/pstypes.h | 9 - code/lab/manager/lab_manager.cpp | 1 - code/network/multimsgs.cpp | 23 +- code/object/collideshipweapon.cpp | 4 +- code/particle/ParticleEffect.cpp | 123 +- code/particle/ParticleEffect.h | 38 +- code/particle/ParticleParse.cpp | 27 +- code/particle/ParticleSource.cpp | 17 +- code/particle/ParticleSource.h | 2 + code/particle/ParticleVolume.h | 38 +- code/particle/volumes/ConeVolume.cpp | 17 +- code/particle/volumes/ConeVolume.h | 16 +- .../particle/volumes/LegacyAACuboidVolume.cpp | 5 +- code/particle/volumes/LegacyAACuboidVolume.h | 7 +- code/particle/volumes/PointVolume.cpp | 21 + code/particle/volumes/PointVolume.h | 31 + code/particle/volumes/RingVolume.cpp | 33 + code/particle/volumes/RingVolume.h | 28 + code/particle/volumes/SpheroidVolume.cpp | 16 +- code/particle/volumes/SpheroidVolume.h | 21 +- code/scripting/api/libs/graphics.cpp | 247 ++-- code/scripting/api/libs/testing.cpp | 62 +- code/ship/ship.cpp | 79 +- code/ship/ship.h | 3 +- code/ship/shipfx.cpp | 3 - code/source_groups.cmake | 4 + code/starfield/supernova.cpp | 9 + code/utils/modular_curves.h | 58 +- code/weapon/beam.cpp | 114 +- code/weapon/beam.h | 1 - code/weapon/muzzleflash.cpp | 229 +-- code/weapon/muzzleflash.h | 18 +- code/weapon/weapon.h | 82 +- code/weapon/weapons.cpp | 1266 +++++------------ freespace2/freespace.cpp | 2 - freespace2/levelpaging.cpp | 2 - 39 files changed, 1249 insertions(+), 1494 deletions(-) create mode 100644 code/particle/volumes/PointVolume.cpp create mode 100644 code/particle/volumes/PointVolume.h create mode 100644 code/particle/volumes/RingVolume.cpp create mode 100644 code/particle/volumes/RingVolume.h diff --git a/code/ai/aiturret.cpp b/code/ai/aiturret.cpp index 4c2f85c6449..ae259cf1f09 100644 --- a/code/ai/aiturret.cpp +++ b/code/ai/aiturret.cpp @@ -2043,9 +2043,6 @@ bool turret_fire_weapon(int weapon_num, particleSource->setTriggerVelocity(vm_vec_mag_quick(&objp->phys_info.vel)); particleSource->finishCreation(); } - else if (wip->muzzle_flash >= 0) { - mflash_create(firing_pos, firing_vec, &Objects[parent_ship->objnum].phys_info, wip->muzzle_flash); - } // in multiplayer (and the master), then send a turret fired packet. if ( MULTIPLAYER_MASTER && (weapon_objnum != -1) ) { @@ -2166,9 +2163,6 @@ void turret_swarm_fire_from_turret(turret_swarm_info *tsi) particleSource->setTriggerRadius(Objects[weapon_objnum].radius); particleSource->finishCreation(); } - else if (Weapon_info[tsi->weapon_class].muzzle_flash >= 0) { - mflash_create(&turret_pos, &turret_fvec, &Objects[tsi->parent_objnum].phys_info, Weapon_info[tsi->weapon_class].muzzle_flash); - } // maybe sound if ( Weapon_info[tsi->weapon_class].launch_snd.isValid() ) { diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index 5e0126f4806..114e6a97f0a 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -33,7 +33,7 @@ #include "object/object.h" #include "parse/parselo.h" #include "scripting/global_hooks.h" -#include "particle/particle.h" +#include "particle/hosts/EffectHostVector.h" #include "render/3d.h" #include "ship/ship.h" #include "ship/shipfx.h" @@ -63,8 +63,7 @@ asteroid Asteroids[MAX_ASTEROIDS]; asteroid_field Asteroid_field; -static int Asteroid_impact_explosion_ani; -static float Asteroid_impact_explosion_radius; +static particle::ParticleEffectHandle Asteroid_impact_explosion_ani; char Asteroid_icon_closeup_model[NAME_LENGTH]; vec3d Asteroid_icon_closeup_position; float Asteroid_icon_closeup_zoom; @@ -1752,7 +1751,9 @@ void asteroid_hit( object * pasteroid_obj, object * other_obj, vec3d * hitpos, f wip = &Weapon_info[Weapons[other_obj->instance].weapon_info_index]; // If the weapon didn't play any impact animation, play custom asteroid impact animation if (!wip->impact_weapon_expl_effect.isValid()) { - particle::create( hitpos, &vmd_zero_vector, 0.0f, Asteroid_impact_explosion_radius, Asteroid_impact_explosion_ani ); + auto source = particle::ParticleManager::get()->createSource(Asteroid_impact_explosion_ani); + source->setHost(std::make_unique(*hitpos, vmd_identity_matrix, vmd_zero_vector)); + source->finishCreation(); } } } @@ -2461,18 +2462,52 @@ static void asteroid_parse_tbl(const char* filename) required_string("#End"); - if (optional_string("$Impact Explosion:")) { + if (optional_string("$Impact Explosion Effect:")) { + Asteroid_impact_explosion_ani = particle::util::parseEffect(); + } + else { char impact_ani_file[MAX_FILENAME_LEN]; - stuff_string(impact_ani_file, F_NAME, MAX_FILENAME_LEN); + float Asteroid_impact_explosion_radius; + int num_frames; - if (VALID_FNAME(impact_ani_file)) { - int num_frames; - Asteroid_impact_explosion_ani = bm_load_animation(impact_ani_file, &num_frames, nullptr, nullptr, nullptr, true); + if (optional_string("$Impact Explosion:")) { + stuff_string(impact_ani_file, F_NAME, MAX_FILENAME_LEN); + } + if (optional_string("$Impact Explosion Radius:")) { + stuff_float(&Asteroid_impact_explosion_radius); } - } - if (optional_string("$Impact Explosion Radius:")) { - stuff_float(&Asteroid_impact_explosion_radius); + if(VALID_FNAME(impact_ani_file)) { + Asteroid_impact_explosion_ani = particle::ParticleManager::get()->addEffect(particle::ParticleEffect( + "", //Name + ::util::UniformFloatRange(1.f), //Particle num + particle::ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange(-1.f), //Single particle only + particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction + ::util::UniformFloatRange(), //Velocity Inherit + false, //Velocity Inherit absolute? + nullptr, //Velocity volume + ::util::UniformFloatRange(), //Velocity volume multiplier + particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling + std::nullopt, //Orientation-based velocity + std::nullopt, //Position-based velocity + nullptr, //Position volume + particle::ParticleEffectHandle::invalid(), //Trail + 1.f, //Chance + false, //Affected by detail + -1.f, //Culling range multiplier + false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset + ::util::UniformFloatRange(-1.f), //Lifetime + ::util::UniformFloatRange(Asteroid_impact_explosion_radius), //Radius + bm_load_animation(impact_ani_file, &num_frames, nullptr, nullptr, nullptr, true))); //Bitmap + } } if (optional_string("$Briefing Icon Closeup Model:")) { @@ -2622,8 +2657,7 @@ static void verify_asteroid_list() */ void asteroid_init() { - Asteroid_impact_explosion_ani = -1; - Asteroid_impact_explosion_radius = 20.0; // retail value + Asteroid_impact_explosion_ani = particle::ParticleEffectHandle::invalid(); Asteroid_icon_closeup_model[0] = '\0'; vm_vec_make(&Asteroid_icon_closeup_position, 0.0f, 0.0f, -334.0f); // magic numbers from retail Asteroid_icon_closeup_zoom = 0.5f; // magic number from retail @@ -2644,7 +2678,7 @@ void asteroid_init() // now that Asteroid_info is filled in we can verify the asteroid splits and set their indecies verify_asteroid_splits(); - if (Asteroid_impact_explosion_ani == -1) { + if (!Asteroid_impact_explosion_ani.isValid()) { Error(LOCATION, "Missing valid asteroid impact explosion definition in asteroid.tbl!"); } diff --git a/code/debris/debris.cpp b/code/debris/debris.cpp index 56bf35e714f..83bebee81be 100644 --- a/code/debris/debris.cpp +++ b/code/debris/debris.cpp @@ -121,6 +121,9 @@ void debris_init() Debris_hit_particle = particle::ParticleManager::get()->addEffect(particle::ParticleEffect( "", //Name ::util::UniformFloatRange(10.f), //Particle num + particle::ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(1.f), //Velocity Inherit false, //Velocity Inherit absolute? @@ -135,6 +138,12 @@ void debris_init() true, //Affected by detail 1.f, //Culling range multiplier true, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset ::util::UniformFloatRange(0.25f, 0.75f), //Lifetime ::util::UniformFloatRange(0.2f, 0.4f), //Radius particle::Anim_bitmap_id_fire)); //Bitmap @@ -418,6 +427,14 @@ object *debris_create(object *source_obj, int model_num, int submodel_num, const { debris_create_set_velocity(&Debris[obj->instance], shipp, exp_center, exp_force, source_subsys); debris_create_fire_hook(obj, source_obj); + const auto& sip = Ship_info[Ships[source_obj->instance].ship_info_index]; + if (sip.debris_flame_particles.isValid()) { + auto source = particle::ParticleManager::get()->createSource(sip.debris_flame_particles); + source->setHost(std::make_unique(obj, vmd_zero_vector)); + source->setTriggerRadius(source_obj->radius); + source->setTriggerVelocity(vm_vec_mag_quick(&source_obj->phys_info.vel)); + source->finishCreation(); + } } return obj; diff --git a/code/globalincs/pstypes.h b/code/globalincs/pstypes.h index 8c28441f28a..2f8973d78f8 100644 --- a/code/globalincs/pstypes.h +++ b/code/globalincs/pstypes.h @@ -397,15 +397,6 @@ const size_t INVALID_SIZE = static_cast(-1); // the trailing underscores are to avoid conflicts with previously #define'd tokens enum class TriStateBool : int { FALSE_ = 0, TRUE_ = 1, UNKNOWN_ = -1 }; - -// lod checker for (modular) table parsing -typedef struct lod_checker { - char filename[MAX_FILENAME_LEN]; - int num_lods; - int override; -} lod_checker; - - // Callback Loading function. // If you pass a function to this, that function will get called // around 10x per second, so you can update the screen. diff --git a/code/lab/manager/lab_manager.cpp b/code/lab/manager/lab_manager.cpp index dec81cc5255..0b6239d6e11 100644 --- a/code/lab/manager/lab_manager.cpp +++ b/code/lab/manager/lab_manager.cpp @@ -48,7 +48,6 @@ LabManager::LabManager() { shockwave_level_init(); ship_level_init(); shipfx_flash_init(); - mflash_page_in(true); weapon_level_init(); beam_level_init(); particle::init(); diff --git a/code/network/multimsgs.cpp b/code/network/multimsgs.cpp index e7792bac095..9e32d4b788a 100644 --- a/code/network/multimsgs.cpp +++ b/code/network/multimsgs.cpp @@ -8770,15 +8770,30 @@ void process_flak_fired_packet(ubyte *data, header *hinfo) // create the weapon object weapon_objnum = weapon_create( &pos, &orient, wid, OBJ_INDEX(objp), -1, true, false, 0.0f, ssp, launch_curve_data); if (weapon_objnum != -1) { - if ( Weapon_info[wid].launch_snd.isValid() ) { + const weapon_info& wip = Weapon_info[wid]; + if ( wip.launch_snd.isValid() ) { snd_play_3d( gamesnd_get_game_sound(Weapon_info[wid].launch_snd), &pos, &View_position ); } - // create a muzzle flash from a flak gun based upon firing position and weapon type - mflash_create(&pos, &dir, &objp->phys_info, Weapon_info[wid].muzzle_flash); + object& wp_obj = Objects[weapon_objnum]; + const weapon& wp = Weapons[wp_obj.instance]; + + if (wip.muzzle_effect.isValid()) { + float radius_mult = 1.f; + if (wip.render_type == WRT_LASER) { + radius_mult = wip.weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_RADIUS_MULT, wp, &wp.modular_curves_instance); + } + //spawn particle effect + auto particleSource = particle::ParticleManager::get()->createSource(wip.muzzle_effect); + //This could potentially be attached to the ship, but might look weird if the spawn position of the weapon is ever interpolated away from the ship's barrel. + particleSource->setHost(make_unique(pos, orient, objp->phys_info.vel)); + particleSource->setTriggerRadius(wp_obj.radius * radius_mult); + particleSource->setTriggerVelocity(vm_vec_mag_quick(&wp_obj.phys_info.vel)); + particleSource->finishCreation(); + } // set its range explicitly - make it long enough so that it's guaranteed to still exist when the server tells us it blew up - flak_set_range(&Objects[weapon_objnum], (float)flak_range); + flak_set_range(&wp_obj, (float)flak_range); } } diff --git a/code/object/collideshipweapon.cpp b/code/object/collideshipweapon.cpp index 5e500635958..00f90367036 100644 --- a/code/object/collideshipweapon.cpp +++ b/code/object/collideshipweapon.cpp @@ -527,8 +527,8 @@ static int ship_weapon_check_collision(object *ship_objp, object *weapon_objp, f if(!ship_override && !weapon_override) { if (shield_collision && quadrant_num >= 0) { - if ((sip->shield_impact_explosion_anim > -1) && (wip->shield_impact_explosion_radius > 0)) { - shield_impact_explosion(&mc->hit_point, ship_objp, wip->shield_impact_explosion_radius, sip->shield_impact_explosion_anim); + if ((sip->shield_impact_explosion_anim.isValid()) && (wip->shield_impact_explosion_radius > 0)) { + shield_impact_explosion(mc->hit_point, mc->hit_normal, ship_objp, weapon_objp, wip->shield_impact_explosion_radius, sip->shield_impact_explosion_anim); } } ship_weapon_do_hit_stuff(ship_objp, weapon_objp, &mc->hit_point_world, &mc->hit_point, quadrant_num, mc->hit_submodel, &mc->hit_normal); diff --git a/code/particle/ParticleEffect.cpp b/code/particle/ParticleEffect.cpp index 0077fd7075a..eaaa5a1b0d7 100644 --- a/code/particle/ParticleEffect.cpp +++ b/code/particle/ParticleEffect.cpp @@ -3,6 +3,7 @@ #include "particle/ParticleEffect.h" #include "particle/ParticleManager.h" +#include "model/modelrender.h" #include "render/3d.h" #include @@ -23,6 +24,8 @@ ParticleEffect::ParticleEffect(SCP_string name) m_keep_anim_length_if_available(false), m_vel_inherit_absolute(false), m_vel_inherit_from_position_absolute(false), + m_reverseAnimation(false), + m_ignore_velocity_inherit_if_has_parent(false), m_bitmap_list({}), m_bitmap_range(::util::UniformRange(0)), m_delayRange(::util::UniformFloatRange(0.0f)), @@ -43,6 +46,7 @@ ParticleEffect::ParticleEffect(SCP_string name) m_velocityNoise(nullptr), m_spawnNoise(nullptr), m_manual_offset (std::nullopt), + m_manual_velocity_offset(std::nullopt), m_particleTrail(ParticleEffectHandle::invalid()), m_size_lifetime_curve(-1), m_vel_lifetime_curve (-1), @@ -52,6 +56,9 @@ ParticleEffect::ParticleEffect(SCP_string name) ParticleEffect::ParticleEffect(SCP_string name, ::util::ParsedRandomFloatRange particleNum, + Duration duration, + ::util::ParsedRandomFloatRange durationRange, + ::util::ParsedRandomFloatRange particlesPerSecond, ShapeDirection direction, ::util::ParsedRandomFloatRange vel_inherit, bool vel_inherit_absolute, @@ -66,11 +73,17 @@ ParticleEffect::ParticleEffect(SCP_string name, bool affectedByDetail, float distanceCulled, bool disregardAnimationLength, + bool reverseAnimation, + bool parentLocal, + bool ignoreVelocityInheritIfParented, + bool velInheritFromPositionAbsolute, + std::optional velocityOffsetLocal, + std::optional offsetLocal, ::util::ParsedRandomFloatRange lifetime, ::util::ParsedRandomFloatRange radius, int bitmap) : m_name(std::move(name)), - m_duration(Duration::ONETIME), + m_duration(duration), m_rotation_type(RotationType::DEFAULT), m_direction(direction), m_velocity_directional_scaling(velocity_directional_scaling), @@ -78,15 +91,17 @@ ParticleEffect::ParticleEffect(SCP_string name, m_parentLifetime(false), m_parentScale(false), m_hasLifetime(true), - m_parent_local(false), + m_parent_local(parentLocal), m_keep_anim_length_if_available(!disregardAnimationLength), m_vel_inherit_absolute(vel_inherit_absolute), - m_vel_inherit_from_position_absolute(false), + m_vel_inherit_from_position_absolute(velInheritFromPositionAbsolute), + m_reverseAnimation(reverseAnimation), + m_ignore_velocity_inherit_if_has_parent(ignoreVelocityInheritIfParented), m_bitmap_list({bitmap}), m_bitmap_range(::util::UniformRange(0)), m_delayRange(::util::UniformFloatRange(0.0f)), - m_durationRange(::util::UniformFloatRange(0.0f)), - m_particlesPerSecond(::util::UniformFloatRange(-1.f)), + m_durationRange(durationRange), + m_particlesPerSecond(particlesPerSecond), m_particleNum(particleNum), m_radius(radius), m_lifetime(lifetime), @@ -101,13 +116,28 @@ ParticleEffect::ParticleEffect(SCP_string name, m_spawnVolume(std::move(spawnVolume)), m_velocityNoise(nullptr), m_spawnNoise(nullptr), - m_manual_offset (std::nullopt), + m_manual_offset(offsetLocal), + m_manual_velocity_offset(velocityOffsetLocal), m_particleTrail(particleTrail), m_size_lifetime_curve(-1), m_vel_lifetime_curve (-1), m_particleChance(particleChance), m_distanceCulled(distanceCulled) {} +float ParticleEffect::getApproximateVisualSize(const vec3d& pos) const { + float distance_to_eye = vm_vec_dist(&Eye_position, &pos); + + return convert_distance_and_diameter_to_pixel_size( + distance_to_eye, + m_radius.avg() * 2.f, + fl_degrees(g3_get_hfov(Eye_fov)), + gr_screen.max_h); +} + +float ParticleEffect::getCurrentFrequencyMult(decltype(modular_curves_definition)::input_type_t source) const { + return m_modular_curves.get_output(ParticleEffect::ParticleCurvesOutput::PARTICLE_FREQ_MULT, source); +} + matrix ParticleEffect::getNewDirection(const matrix& hostOrientation, const std::optional& normal) const { switch (m_direction) { case ShapeDirection::ALIGNED: @@ -143,11 +173,11 @@ matrix ParticleEffect::getNewDirection(const matrix& hostOrientation, const std: } } -void ParticleEffect::sampleNoise(vec3d& noiseTarget, const matrix* orientation, std::pair& noise, const std::tuple& source, ParticleCurvesOutput noiseMult, ParticleCurvesOutput noiseTimeMult, ParticleCurvesOutput noiseSeed) const { +void ParticleEffect::sampleNoise(vec3d& noiseTarget, const matrix* orientation, std::pair& noise, decltype(modular_curves_definition)::input_type_t source, ParticleCurvesOutput noiseMult, ParticleCurvesOutput noiseTimeMult, ParticleCurvesOutput noiseSeed) const { auto& [kernel, instruction] = noise; anl::CNoiseExecutor executor(kernel); const auto& color = executor.evaluateColor( - ParticleSource::getEffectRunningTime(source) + ParticleSource::getEffectRunningTime(std::forward_as_tuple(std::get<0>(source), std::get<1>(source))) * m_modular_curves.get_output(noiseTimeMult, source) , m_modular_curves.get_output(noiseSeed, source), instruction); @@ -157,18 +187,44 @@ void ParticleEffect::sampleNoise(vec3d& noiseTarget, const matrix* orientation, vm_vec_unrotate(&noiseTarget, &noiseSampleLocal, orientation); } -void ParticleEffect::processSource(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& velParent, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const { +/* + * In persistent mode (should only ever be used by scripting, really), this function returns pointers to the persistent particles + * In non-persistent mode, this function returns the multiplier for the next spawn time. This is because the source cannot know about the curve evaluation that is required to get this factor + * + * */ +template +auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& velParent, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const { + using persistentParticlesList = std::conditional_t, bool>; + persistentParticlesList createdParticles; + + if constexpr (!isPersistent) + SCP_UNUSED(createdParticles); + if (m_affectedByDetail){ if (Detail.num_particles > 0) particle_percent *= (0.5f + (0.25f * static_cast(Detail.num_particles - 1))); - else - return; //Will not emit on current detail settings, but may in the future. + else { + //Will not emit on current detail settings, but may in the future. + if constexpr (isPersistent) + return createdParticles; + else { + const auto& [pos, hostOrientation] = source.m_host->getPositionAndOrientation(m_parent_local, interp, m_manual_offset); + auto modularCurvesInput = std::forward_as_tuple(source, effectNumber, pos); + return getCurrentFrequencyMult(modularCurvesInput); + } + } } - auto modularCurvesInput = std::forward_as_tuple(source, effectNumber); - const auto& [pos, hostOrientation] = source.m_host->getPositionAndOrientation(m_parent_local, interp, m_manual_offset); + vec3d posGlobal = pos; + if (m_parent_local && parent >= 0) { + vm_vec_unrotate(&posGlobal, &posGlobal, &Objects[parent].orient); + vm_vec_add2(&posGlobal, &Objects[parent].pos); + } + + auto modularCurvesInput = std::forward_as_tuple(source, effectNumber, posGlobal); + const auto& orientation = getNewDirection(hostOrientation, source.m_normal); if (m_distanceCulled > 0.f) { @@ -210,8 +266,11 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s } for (uint i = 0; i < num_spawn; ++i) { + float particleFraction = static_cast(i) / static_cast(num_spawn); + particle_info info; + info.reverse = m_reverseAnimation; info.pos = pos; info.vel = velParent; @@ -227,17 +286,21 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s if (m_vel_inherit_absolute) vm_vec_normalize_safe(&info.vel, true); - info.vel *= m_vel_inherit.next() * inheritVelocityMultiplier; + info.vel *= (m_ignore_velocity_inherit_if_has_parent && parent >= 0) ? 0.f : m_vel_inherit.next() * inheritVelocityMultiplier; vec3d localVelocity = velNoise; vec3d localPos = posNoise; if (m_spawnVolume != nullptr) { - localPos += m_spawnVolume->sampleRandomPoint(orientation, modularCurvesInput); + localPos += m_spawnVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction); } if (m_velocityVolume != nullptr) { - localVelocity += m_velocityVolume->sampleRandomPoint(orientation, modularCurvesInput) * (m_velocity_scaling.next() * velocityVolumeMultiplier); + localVelocity += m_velocityVolume->sampleRandomPoint(orientation, modularCurvesInput, particleFraction) * (m_velocity_scaling.next() * velocityVolumeMultiplier); + } + + if (m_manual_velocity_offset.has_value()) { + localVelocity += *m_manual_velocity_offset; } if (m_vel_inherit_from_orientation.has_value()) { @@ -262,8 +325,8 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s m_velocity_directional_scaling == VelocityScaling::DOT ? dot : 1.f / std::max(0.001f, dot)); } - info.pos += localPos; info.vel += localVelocity; + info.pos += localPos + info.vel * (interp * f2fl(Frametime)); info.bitmap = m_bitmap_list[m_bitmap_range.next()]; @@ -302,6 +365,9 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s if (m_particleTrail.isValid()) { auto part = createPersistent(&info); + if constexpr (isPersistent) + createdParticles.push_back(part); + // There are some possibilities where we can get a null pointer back. Those are very rare but we // still shouldn't crash in those circumstances. if (!part.expired()) { @@ -310,10 +376,29 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s trailSource->finishCreation(); } } else { - // We don't have a trail so we don't need a persistent particle - create(&info); + if constexpr (isPersistent){ + auto part = createPersistent(&info); + createdParticles.push_back(part); + } + else { + // We don't have a trail so we don't need a persistent particle + create(&info); + } } } + + if constexpr (isPersistent) + return createdParticles; + else + return getCurrentFrequencyMult(modularCurvesInput); +} + +float ParticleEffect::processSource(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& velParent, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const { + return processSourceInternal(interp, source, effectNumber, velParent, parent, parent_sig, parentLifetime, parentRadius, particle_percent); +} + +SCP_vector ParticleEffect::processSourcePersistent(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& velParent, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const { + return processSourceInternal(interp, source, effectNumber, velParent, parent, parent_sig, parentLifetime, parentRadius, particle_percent); } void ParticleEffect::pageIn() { diff --git a/code/particle/ParticleEffect.h b/code/particle/ParticleEffect.h index 51c07562407..9264988642b 100644 --- a/code/particle/ParticleEffect.h +++ b/code/particle/ParticleEffect.h @@ -15,6 +15,9 @@ class EffectHost; //Due to parsing shenanigans in weapons, this needs a forward-declare here int parse_weapon(int, bool, const char*); +namespace scripting::api { + particle::ParticleEffectHandle getLegacyScriptingParticleEffect(int bitmap, bool reversed); +} namespace anl { class CKernel; @@ -84,6 +87,8 @@ class ParticleEffect { friend int ::parse_weapon(int, bool, const char*); + friend ParticleEffectHandle scripting::api::getLegacyScriptingParticleEffect(int bitmap, bool reversed); + SCP_string m_name; //!< The name of this effect Duration m_duration; @@ -99,6 +104,8 @@ class ParticleEffect { bool m_keep_anim_length_if_available; bool m_vel_inherit_absolute; bool m_vel_inherit_from_position_absolute; + bool m_reverseAnimation; + bool m_ignore_velocity_inherit_if_has_parent; SCP_vector m_bitmap_list; ::util::UniformRange m_bitmap_range; @@ -125,6 +132,7 @@ class ParticleEffect { std::shared_ptr> m_spawnNoise; std::optional m_manual_offset; + std::optional m_manual_velocity_offset; ParticleEffectHandle m_particleTrail; @@ -135,8 +143,10 @@ class ParticleEffect { float m_distanceCulled; //Kinda deprecated. Only used by the oldest of legacy effects. matrix getNewDirection(const matrix& hostOrientation, const std::optional& normal) const; - void sampleNoise(vec3d& noiseTarget, const matrix* orientation, std::pair& noise, const std::tuple& source, ParticleCurvesOutput noiseMult, ParticleCurvesOutput noiseTimeMult, ParticleCurvesOutput noiseSeed) const; - public: + + template + auto processSourceInternal(float interp, const ParticleSource& source, size_t effectNumber, const vec3d& velParent, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const; + public: /** * @brief Initializes the base ParticleEffect * @param name The name this effect should have @@ -149,6 +159,9 @@ class ParticleEffect { // Parsing the deprecated -part.tbm effects uses the simple constructor + parseLegacy() instead! explicit ParticleEffect(SCP_string name, ::util::ParsedRandomFloatRange particleNum, + Duration duration, + ::util::ParsedRandomFloatRange durationRange, + ::util::ParsedRandomFloatRange particlesPerSecond, ShapeDirection direction, ::util::ParsedRandomFloatRange vel_inherit, bool vel_inherit_absolute, @@ -163,12 +176,19 @@ class ParticleEffect { bool affectedByDetail, float distanceCulled, bool disregardAnimationLength, + bool reverseAnimation, + bool parentLocal, + bool ignoreVelocityInheritIfParented, + bool velInheritFromPositionAbsolute, + std::optional velocityOffsetLocal, + std::optional offsetLocal, ::util::ParsedRandomFloatRange lifetime, ::util::ParsedRandomFloatRange radius, int bitmap ); - void processSource(float interp, const ParticleSource& host, size_t effectNumber, const vec3d& vel, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const; + float processSource(float interp, const ParticleSource& host, size_t effectNumber, const vec3d& vel, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const; + SCP_vector processSourcePersistent(float interp, const ParticleSource& host, size_t effectNumber, const vec3d& vel, int parent, int parent_sig, float parentLifetime, float parentRadius, float particle_percent) const; void pageIn(); @@ -180,6 +200,8 @@ class ParticleEffect { bool isOnetime() const { return m_duration == Duration::ONETIME; } + float getApproximateVisualSize(const vec3d& pos) const; + constexpr static auto modular_curves_definition = make_modular_curve_definition( std::array { std::pair {"Particle Number Mult", ParticleCurvesOutput::PARTICLE_NUM_MULT}, @@ -207,12 +229,18 @@ class ParticleEffect { modular_curves_submember_input<&ParticleSource::m_effect_is_running, &decltype(ParticleSource::m_effect_is_running)::count>, modular_curves_submember_input<&ParticleSource::getEffect, &SCP_vector::size>, ModularCurvesMathOperators::division>{}}) - .derive_modular_curves_input_only_subset( + .derive_modular_curves_input_only_subset( //Effect Number std::pair {"Spawntime Left", modular_curves_functional_full_input<&ParticleSource::getEffectRemainingTime>{}}, - std::pair {"Time Running", modular_curves_functional_full_input<&ParticleSource::getEffectRunningTime>{}} + std::pair {"Time Running", modular_curves_functional_full_input<&ParticleSource::getEffectRunningTime>{}}) + .derive_modular_curves_input_only_subset( //Sampled spawn position + std::pair {"Apparent Visual Size At Emitter", modular_curves_functional_full_input<&ParticleSource::getEffectVisualSize>{}} ); MODULAR_CURVE_SET(m_modular_curves, modular_curves_definition); + + private: + float getCurrentFrequencyMult(decltype(modular_curves_definition)::input_type_t source) const; + void sampleNoise(vec3d& noiseTarget, const matrix* orientation, std::pair& noise, decltype(modular_curves_definition)::input_type_t source, ParticleCurvesOutput noiseMult, ParticleCurvesOutput noiseTimeMult, ParticleCurvesOutput noiseSeed) const; }; } diff --git a/code/particle/ParticleParse.cpp b/code/particle/ParticleParse.cpp index 19de33f8b68..f5ba89cad7f 100644 --- a/code/particle/ParticleParse.cpp +++ b/code/particle/ParticleParse.cpp @@ -1,6 +1,8 @@ #include "particle/ParticleManager.h" #include "particle/ParticleEffect.h" #include "particle/volumes/ConeVolume.h" +#include "particle/volumes/PointVolume.h" +#include "particle/volumes/RingVolume.h" #include "particle/volumes/SpheroidVolume.h" #include @@ -19,6 +21,12 @@ namespace particle { } } + static void parseBitmapReversed(ParticleEffect &effect) { + if (optional_string("+Animation Reversed:")) { + stuff_boolean(&effect.m_reverseAnimation); + } + } + template static void parseRadius(ParticleEffect &effect) { if (optional_string(modern ? "+Radius:" : "+Size:")) { effect.m_radius = ::util::ParsedRandomFloatRange::parseRandomRange(); @@ -73,6 +81,10 @@ namespace particle { if (optional_string(modern ? "+Position Offset:" : "+Offset:")) { stuff_vec3d(&effect.m_manual_offset.emplace()); } + + if (optional_string("+Velocity Offset:")) { + stuff_vec3d(&effect.m_manual_velocity_offset.emplace()); + } } static void parseParentLocal(ParticleEffect& effect) { @@ -108,7 +120,7 @@ namespace particle { static std::shared_ptr parseVolume() { - int type = required_string_one_of(2, "Spheroid", "Cone"); //... and future volumes + int type = required_string_one_of(4, "Spheroid", "Cone", "Ring", "Point"); //... and future volumes std::shared_ptr volume; switch (type) { @@ -120,6 +132,14 @@ namespace particle { required_string("Cone"); volume = std::make_shared(); break; + case 2: + required_string("Ring"); + volume = std::make_shared(); + break; + case 3: + required_string("Point"); + volume = std::make_shared(); + break; default: UNREACHABLE("Invalid volume type specified!"); } @@ -135,6 +155,10 @@ namespace particle { effect.m_vel_inherit = ::util::ParsedRandomFloatRange::parseRandomRange(); effect.m_vel_inherit_absolute = true; } + + if (optional_string("+Ignore Velocity Inherit If Parented:")) { + stuff_boolean(&effect.m_ignore_velocity_inherit_if_has_parent); + } } static void parseVelocityVolume(ParticleEffect &effect) { @@ -302,6 +326,7 @@ namespace particle { //Particle Settings parseBitmaps(effect); + parseBitmapReversed(effect); parseRotationType(effect); parseRadius(effect); parseLength(effect); diff --git a/code/particle/ParticleSource.cpp b/code/particle/ParticleSource.cpp index 7d400303e89..a5d92ff9ca1 100644 --- a/code/particle/ParticleSource.cpp +++ b/code/particle/ParticleSource.cpp @@ -62,14 +62,13 @@ bool ParticleSource::process() { //Find "time" in last frame where particle spawned float interp = static_cast(timestamp_since(timing.m_nextCreation)) / (f2fl(Frametime) * 1000.0f); + // Some of these + float freqMult = effect.processSource(interp, *this, i, vel, parent, parent_sig, parent_lifetime, parent_radius, particleMultiplier); + // we need to clamp this to 1 because a spawn delay lower than it takes to spawn the particle in ms means we try to spawn infinite particles - float freqMult = effect.m_modular_curves.get_output(ParticleEffect::ParticleCurvesOutput::PARTICLE_FREQ_MULT, std::pair(*this, i)); auto time_diff_ms = std::max(fl2i(effect.getNextSpawnDelay() / freqMult * MILLISECONDS_PER_SECOND), 1); timing.m_nextCreation = timestamp_delta(timing.m_nextCreation, time_diff_ms); - //Some of these - effect.processSource(interp, *this, i, vel, parent, parent_sig, parent_lifetime, parent_radius, particleMultiplier); - bool isDone = effect.isOnetime() || timestamp_compare(timing.m_endTimestamp, timing.m_nextCreation) < 0; m_effect_is_running[i] = !isDone; @@ -99,10 +98,16 @@ void ParticleSource::setHost(std::unique_ptr host) { } float ParticleSource::getEffectRemainingTime(const std::tuple& source) { - return i2fl(timestamp_until(std::get<0>(source).m_timing[std::get<1>(source)].m_endTimestamp)) / i2fl(MILLISECONDS_PER_SECOND); + const auto& timing = std::get<0>(source).m_timing[std::get<1>(source)]; + return i2fl(timestamp_get_delta(timing.m_nextCreation, timing.m_endTimestamp)) / i2fl(MILLISECONDS_PER_SECOND); } float ParticleSource::getEffectRunningTime(const std::tuple& source) { - return i2fl(timestamp_since(std::get<0>(source).m_timing[std::get<1>(source)].m_startTimestamp)) / i2fl(MILLISECONDS_PER_SECOND); + const auto& timing = std::get<0>(source).m_timing[std::get<1>(source)]; + return i2fl(timestamp_get_delta(timing.m_startTimestamp, timing.m_nextCreation)) / i2fl(MILLISECONDS_PER_SECOND); +} + +float ParticleSource::getEffectVisualSize(const std::tuple& source) { + return std::get<0>(source).getEffect()[std::get<1>(source)].getApproximateVisualSize(std::get<2>(source)); } } diff --git a/code/particle/ParticleSource.h b/code/particle/ParticleSource.h index 9b3d47c7e13..acfade0982a 100644 --- a/code/particle/ParticleSource.h +++ b/code/particle/ParticleSource.h @@ -90,6 +90,8 @@ class ParticleSource { static float getEffectRemainingTime(const std::tuple& source); static float getEffectRunningTime(const std::tuple& source); + + static float getEffectVisualSize(const std::tuple& source); public: ParticleSource(); diff --git a/code/particle/ParticleVolume.h b/code/particle/ParticleVolume.h index 53e6a06bf0f..c03cfaf55b2 100644 --- a/code/particle/ParticleVolume.h +++ b/code/particle/ParticleVolume.h @@ -1,16 +1,50 @@ #pragma once #include "globalincs/pstypes.h" +#include "parse/parselo.h" + +#include namespace particle { class ParticleSource; class ParticleVolume { - public: - virtual vec3d sampleRandomPoint(const matrix &orientation, const std::tuple& source) = 0; + virtual vec3d sampleRandomPoint(const matrix &orientation, const std::tuple& source, float particlesFraction) = 0; virtual void parse() = 0; virtual ~ParticleVolume() = default; + + std::optional posOffset; + std::optional rotOffset; + protected: + void parseCommon() { + if (optional_string("+Volume Position Offset:")) { + stuff_vec3d(&posOffset.emplace()); + } + if (optional_string("+Volume Point Towards:")) { + stuff_vec3d(&rotOffset.emplace()); + } + } + + vec3d pointCompensateForOffsetAndRotOffset(const vec3d& point, const matrix& orientation, float posOffsetRot, float rotOffsetRot) const { + vec3d outpnt = point; + + if (rotOffset.has_value()) { + vec3d rot = *rotOffset; + vm_rot_point_around_line(&rot, &rot, rotOffsetRot, &vmd_zero_vector, &vmd_z_vector); + matrix orientUse; + vm_vector_2_matrix(&orientUse, &rot); + vm_vec_unrotate(&outpnt, &outpnt, &orientUse); + } + if (posOffset.has_value()) { + vec3d pos = *posOffset; + vm_rot_point_around_line(&pos, &pos, posOffsetRot, &vmd_zero_vector, &vmd_z_vector); + vm_vec_unrotate(&pos, &pos, &orientation); + outpnt += pos; + } + + return outpnt; + } }; } \ No newline at end of file diff --git a/code/particle/volumes/ConeVolume.cpp b/code/particle/volumes/ConeVolume.cpp index 4ac782a2c0f..d9468353b6d 100644 --- a/code/particle/volumes/ConeVolume.cpp +++ b/code/particle/volumes/ConeVolume.cpp @@ -3,14 +3,18 @@ namespace particle { ConeVolume::ConeVolume() : m_deviation(::util::UniformFloatRange(0.f)), m_length(::util::UniformFloatRange(1.f)), m_modular_curve_instance(m_modular_curves.create_instance()) { } ConeVolume::ConeVolume(::util::ParsedRandomFloatRange deviation, float length) : m_deviation(deviation), m_length(::util::UniformFloatRange(length)), m_modular_curve_instance(m_modular_curves.create_instance()) { } + ConeVolume::ConeVolume(::util::ParsedRandomFloatRange deviation, ::util::ParsedRandomFloatRange length) : m_deviation(deviation), m_length(length), m_modular_curve_instance(m_modular_curves.create_instance()) { } + + + vec3d ConeVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); - vec3d ConeVolume::sampleRandomPoint(const matrix &orientation, const std::tuple& source) { //It is surely possible to do this more efficiently. angles angs; angs.b = 0.0f; - float deviationMult = m_modular_curves.get_output(VolumeModularCurveOutput::DEVIATION, source, &m_modular_curve_instance); + float deviationMult = m_modular_curves.get_output(VolumeModularCurveOutput::DEVIATION, curveSource, &m_modular_curve_instance); angs.h = m_deviation.next() * deviationMult; angs.p = m_deviation.next() * deviationMult; @@ -21,7 +25,12 @@ namespace particle { matrix rotatedVel; vm_matrix_x_matrix(&rotatedVel, &orientation, &m); - return rotatedVel.vec.fvec * (m_length.next() * m_modular_curves.get_output(VolumeModularCurveOutput::LENGTH, source, &m_modular_curve_instance)); + vec3d point = rotatedVel.vec.fvec * (m_length.next() * m_modular_curves.get_output(VolumeModularCurveOutput::LENGTH, curveSource, &m_modular_curve_instance)); + + //TODO + return pointCompensateForOffsetAndRotOffset(point, orientation, + m_modular_curves.get_output(VolumeModularCurveOutput::OFFSET_ROT, curveSource, &m_modular_curve_instance), + m_modular_curves.get_output(VolumeModularCurveOutput::POINT_TO_ROT, curveSource, &m_modular_curve_instance)); } void ConeVolume::parse() { @@ -48,6 +57,8 @@ namespace particle { m_length = ::util::ParsedRandomFloatRange::parseRandomRange(0); } + ParticleVolume::parseCommon(); + m_modular_curves.parse("$Volume Curve:"); } } \ No newline at end of file diff --git a/code/particle/volumes/ConeVolume.h b/code/particle/volumes/ConeVolume.h index 61e72f7d80f..5fc47de3c37 100644 --- a/code/particle/volumes/ConeVolume.h +++ b/code/particle/volumes/ConeVolume.h @@ -6,22 +6,28 @@ namespace particle { class ConeVolume : public ParticleVolume { + friend int ::parse_weapon(int, bool, const char*); + ::util::ParsedRandomFloatRange m_deviation; ::util::ParsedRandomFloatRange m_length; - enum class VolumeModularCurveOutput : uint8_t {DEVIATION, LENGTH, NUM_VALUES}; - constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_output_only_subset( + enum class VolumeModularCurveOutput : uint8_t {DEVIATION, LENGTH, OFFSET_ROT, POINT_TO_ROT, NUM_VALUES}; + constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_subset( std::array { std::pair { "Deviation Mult", VolumeModularCurveOutput::DEVIATION }, - std::pair { "Length Mult", VolumeModularCurveOutput::LENGTH } - }); + std::pair { "Length Mult", VolumeModularCurveOutput::LENGTH }, + std::pair { "Offset Rotate Around Fvec", VolumeModularCurveOutput::OFFSET_ROT }, + std::pair { "Point To Rotate Around Fvec", VolumeModularCurveOutput::POINT_TO_ROT } + }, + std::pair { "Fraction Particles Spawned", modular_curves_self_input{}}); MODULAR_CURVE_SET(m_modular_curves, modular_curve_definition); modular_curves_entry_instance m_modular_curve_instance; public: explicit ConeVolume(); explicit ConeVolume(::util::ParsedRandomFloatRange deviation, float length); + explicit ConeVolume(::util::ParsedRandomFloatRange deviation, ::util::ParsedRandomFloatRange length); - vec3d sampleRandomPoint(const matrix &orientation, const std::tuple& source) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; void parse() override; }; } \ No newline at end of file diff --git a/code/particle/volumes/LegacyAACuboidVolume.cpp b/code/particle/volumes/LegacyAACuboidVolume.cpp index d2cb729eea3..e48882357fd 100644 --- a/code/particle/volumes/LegacyAACuboidVolume.cpp +++ b/code/particle/volumes/LegacyAACuboidVolume.cpp @@ -5,8 +5,9 @@ namespace particle { LegacyAACuboidVolume::LegacyAACuboidVolume(float normalVariance, float size, bool normalize) : m_normalVariance(normalVariance), m_size(size), m_normalize(normalize), m_modular_curve_instance(m_modular_curves.create_instance()) { } - vec3d LegacyAACuboidVolume::sampleRandomPoint(const matrix &orientation, const std::tuple& source) { - float variance = m_normalVariance * m_modular_curves.get_output(VolumeModularCurveOutput::VARIANCE, source, &m_modular_curve_instance); + vec3d LegacyAACuboidVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); + float variance = m_normalVariance * m_modular_curves.get_output(VolumeModularCurveOutput::VARIANCE, curveSource, &m_modular_curve_instance); vec3d normal; diff --git a/code/particle/volumes/LegacyAACuboidVolume.h b/code/particle/volumes/LegacyAACuboidVolume.h index 95838c99d56..ea98b99c1d0 100644 --- a/code/particle/volumes/LegacyAACuboidVolume.h +++ b/code/particle/volumes/LegacyAACuboidVolume.h @@ -13,10 +13,11 @@ namespace particle { enum class VolumeModularCurveOutput : uint8_t {VARIANCE, NUM_VALUES}; private: - constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_output_only_subset( + constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_subset( std::array { std::pair { "Variance", VolumeModularCurveOutput::VARIANCE } - }); + }, + std::pair { "Fraction Particles Spawned", modular_curves_self_input{}}); public: MODULAR_CURVE_SET(m_modular_curves, modular_curve_definition); @@ -27,7 +28,7 @@ namespace particle { public: explicit LegacyAACuboidVolume(float normalVariance, float size, bool normalize); - vec3d sampleRandomPoint(const matrix &orientation, const std::tuple& source) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; void parse() override { UNREACHABLE("Cannot parse Legacy Particle Volume!"); }; diff --git a/code/particle/volumes/PointVolume.cpp b/code/particle/volumes/PointVolume.cpp new file mode 100644 index 00000000000..05c9b51dcf5 --- /dev/null +++ b/code/particle/volumes/PointVolume.cpp @@ -0,0 +1,21 @@ +#include "PointVolume.h" + +#include "math/vecmat.h" + +namespace particle { + PointVolume::PointVolume() : m_modular_curve_instance(m_modular_curves.create_instance()) { }; + + vec3d PointVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); + + return pointCompensateForOffsetAndRotOffset(ZERO_VECTOR, orientation, + m_modular_curves.get_output(VolumeModularCurveOutput::OFFSET_ROT, curveSource, &m_modular_curve_instance), + m_modular_curves.get_output(VolumeModularCurveOutput::POINT_TO_ROT, curveSource, &m_modular_curve_instance)); + } + + void PointVolume::parse() { + ParticleVolume::parseCommon(); + + m_modular_curves.parse("$Volume Curve:"); + } +} diff --git a/code/particle/volumes/PointVolume.h b/code/particle/volumes/PointVolume.h new file mode 100644 index 00000000000..ccfbd52a7fa --- /dev/null +++ b/code/particle/volumes/PointVolume.h @@ -0,0 +1,31 @@ +#pragma once + +#include "particle/ParticleVolume.h" +#include "particle/ParticleEffect.h" + +namespace particle { + class PointVolume : public ParticleVolume { + public: + enum class VolumeModularCurveOutput : uint8_t {OFFSET_ROT, POINT_TO_ROT, NUM_VALUES}; + + private: + constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_subset( + std::array { + std::pair { "Offset Rotate Around Fvec", VolumeModularCurveOutput::OFFSET_ROT }, + std::pair { "Point To Rotate Around Fvec", VolumeModularCurveOutput::POINT_TO_ROT } + }, + std::pair { "Fraction Particles Spawned", modular_curves_self_input{}}); + + public: + MODULAR_CURVE_SET(m_modular_curves, modular_curve_definition); + + private: + modular_curves_entry_instance m_modular_curve_instance; + + public: + explicit PointVolume(); + + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + void parse() override; + }; +} \ No newline at end of file diff --git a/code/particle/volumes/RingVolume.cpp b/code/particle/volumes/RingVolume.cpp new file mode 100644 index 00000000000..94ab626a20d --- /dev/null +++ b/code/particle/volumes/RingVolume.cpp @@ -0,0 +1,33 @@ +#include "RingVolume.h" + +#include "math/vecmat.h" + +namespace particle { + RingVolume::RingVolume() : m_radius(1.f), m_onEdge(false), m_modular_curve_instance(m_modular_curves.create_instance()) { }; + RingVolume::RingVolume(float radius, bool onEdge) : m_radius(radius), m_onEdge(onEdge), m_modular_curve_instance(m_modular_curves.create_instance()) { }; + + vec3d RingVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); + vec3d pos; + // get an unbiased random point in the sphere + vm_vec_random_in_circle(&pos, &vmd_zero_vector, &orientation, m_radius * m_modular_curves.get_output(VolumeModularCurveOutput::RADIUS, curveSource, &m_modular_curve_instance), false); + + return pointCompensateForOffsetAndRotOffset(pos, orientation, + m_modular_curves.get_output(VolumeModularCurveOutput::OFFSET_ROT, curveSource, &m_modular_curve_instance), + m_modular_curves.get_output(VolumeModularCurveOutput::POINT_TO_ROT, curveSource, &m_modular_curve_instance)); + } + + void RingVolume::parse() { + if (optional_string("+Radius:")) { + stuff_float(&m_radius); + } + + if (optional_string("+On Edge:")) { + stuff_boolean(&m_onEdge); + } + + ParticleVolume::parseCommon(); + + m_modular_curves.parse("$Volume Curve:"); + } +} diff --git a/code/particle/volumes/RingVolume.h b/code/particle/volumes/RingVolume.h new file mode 100644 index 00000000000..bdb000ab212 --- /dev/null +++ b/code/particle/volumes/RingVolume.h @@ -0,0 +1,28 @@ +#pragma once + +#include "particle/ParticleVolume.h" +#include "particle/ParticleEffect.h" + +namespace particle { + class RingVolume : public ParticleVolume { + float m_radius; + bool m_onEdge; + + enum class VolumeModularCurveOutput : uint8_t {RADIUS, OFFSET_ROT, POINT_TO_ROT, NUM_VALUES}; + constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_subset( + std::array { + std::pair { "Radius Mult", VolumeModularCurveOutput::RADIUS }, + std::pair { "Offset Rotate Around Fvec", VolumeModularCurveOutput::OFFSET_ROT }, + std::pair { "Point To Rotate Around Fvec", VolumeModularCurveOutput::POINT_TO_ROT } + }, + std::pair { "Fraction Particles Spawned", modular_curves_self_input{}}); + MODULAR_CURVE_SET(m_modular_curves, modular_curve_definition); + modular_curves_entry_instance m_modular_curve_instance; + public: + explicit RingVolume(); + explicit RingVolume(float radius, bool onEdge); + + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; + void parse() override; + }; +} \ No newline at end of file diff --git a/code/particle/volumes/SpheroidVolume.cpp b/code/particle/volumes/SpheroidVolume.cpp index 083141f561e..f6dc99dd704 100644 --- a/code/particle/volumes/SpheroidVolume.cpp +++ b/code/particle/volumes/SpheroidVolume.cpp @@ -6,32 +6,35 @@ namespace particle { SpheroidVolume::SpheroidVolume() : m_bias(1.f), m_stretch(1.f), m_radius(1.f), m_modular_curve_instance(m_modular_curves.create_instance()) { }; SpheroidVolume::SpheroidVolume(float bias, float stretch, float radius) : m_bias(bias), m_stretch(stretch), m_radius(radius), m_modular_curve_instance(m_modular_curves.create_instance()) { }; - vec3d SpheroidVolume::sampleRandomPoint(const matrix &orientation, const std::tuple& source) { + vec3d SpheroidVolume::sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) { + auto curveSource = std::tuple_cat(source, std::make_tuple(particlesFraction)); vec3d pos; // get an unbiased random point in the sphere vm_vec_random_in_sphere(&pos, &vmd_zero_vector, 1.0f, false); // maybe bias it towards the center or edge - float bias = m_bias * m_modular_curves.get_output(VolumeModularCurveOutput::BIAS, source, &m_modular_curve_instance); + float bias = m_bias * m_modular_curves.get_output(VolumeModularCurveOutput::BIAS, curveSource, &m_modular_curve_instance); if (!fl_equal(bias, 1.f)) { float mag = vm_vec_mag(&pos); pos *= powf(mag, bias) / mag; } // maybe stretch it - float stretch = m_stretch * m_modular_curves.get_output(VolumeModularCurveOutput::STRETCH, source, &m_modular_curve_instance); + float stretch = m_stretch * m_modular_curves.get_output(VolumeModularCurveOutput::STRETCH, curveSource, &m_modular_curve_instance); if (!fl_equal(stretch, 1.f)) { matrix stretch_matrix = vm_stretch_matrix(&orientation.vec.fvec, stretch); vm_vec_rotate(&pos, &pos, &stretch_matrix); } // maybe scale it - float radius = m_radius * m_modular_curves.get_output(VolumeModularCurveOutput::RADIUS, source, &m_modular_curve_instance); + float radius = m_radius * m_modular_curves.get_output(VolumeModularCurveOutput::RADIUS, curveSource, &m_modular_curve_instance); if (!fl_equal(radius, 1.f)) { pos *= radius; } - return pos; + return pointCompensateForOffsetAndRotOffset(pos, orientation, + m_modular_curves.get_output(VolumeModularCurveOutput::OFFSET_ROT, curveSource, &m_modular_curve_instance), + m_modular_curves.get_output(VolumeModularCurveOutput::POINT_TO_ROT, curveSource, &m_modular_curve_instance)); } void SpheroidVolume::parse() { @@ -44,6 +47,9 @@ namespace particle { if (optional_string("+Stretch:")) { stuff_float(&m_stretch); } + + ParticleVolume::parseCommon(); + m_modular_curves.parse("$Volume Curve:"); } } diff --git a/code/particle/volumes/SpheroidVolume.h b/code/particle/volumes/SpheroidVolume.h index 4bee55a11a4..36ea461d232 100644 --- a/code/particle/volumes/SpheroidVolume.h +++ b/code/particle/volumes/SpheroidVolume.h @@ -9,20 +9,31 @@ namespace particle { float m_stretch; float m_radius; - enum class VolumeModularCurveOutput : uint8_t {BIAS, STRETCH, RADIUS, NUM_VALUES}; - constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_output_only_subset( + public: + enum class VolumeModularCurveOutput : uint8_t {BIAS, STRETCH, RADIUS, OFFSET_ROT, POINT_TO_ROT, NUM_VALUES}; + + private: + constexpr static auto modular_curve_definition = ParticleEffect::modular_curves_definition.derive_modular_curves_subset( std::array { std::pair { "Bias Mult", VolumeModularCurveOutput::BIAS }, std::pair { "Stretch Mult", VolumeModularCurveOutput::STRETCH }, - std::pair { "Radius Mult", VolumeModularCurveOutput::RADIUS } - }); + std::pair { "Radius Mult", VolumeModularCurveOutput::RADIUS }, + std::pair { "Offset Rotate Around Fvec", VolumeModularCurveOutput::OFFSET_ROT }, + std::pair { "Point To Rotate Around Fvec", VolumeModularCurveOutput::POINT_TO_ROT } + }, + std::pair { "Fraction Particles Spawned", modular_curves_self_input{}}); + + public: MODULAR_CURVE_SET(m_modular_curves, modular_curve_definition); + + private: modular_curves_entry_instance m_modular_curve_instance; + public: explicit SpheroidVolume(); explicit SpheroidVolume(float bias, float stretch, float radius); - vec3d sampleRandomPoint(const matrix &orientation, const std::tuple& source) override; + vec3d sampleRandomPoint(const matrix &orientation, decltype(ParticleEffect::modular_curves_definition)::input_type_t source, float particlesFraction) override; void parse() override; }; } \ No newline at end of file diff --git a/code/scripting/api/libs/graphics.cpp b/code/scripting/api/libs/graphics.cpp index 8982215f5f2..723cccd62a8 100644 --- a/code/scripting/api/libs/graphics.cpp +++ b/code/scripting/api/libs/graphics.cpp @@ -31,6 +31,8 @@ #include #include #include +#include +#include #include #include #include @@ -2192,23 +2194,63 @@ ADE_FUNC(openMovie, l_Graphics, "string name, [boolean looping = false, boolean return ade_set_args(L, "o", l_MoviePlayer.Set(movie_player_h(std::move(player)))); } -ADE_FUNC(createPersistentParticle, - l_Graphics, - "vector Position, vector Velocity, number Lifetime, number Radius, [enumeration Type=PARTICLE_DEBUG, number TracerLength=-1, " - "boolean Reverse=false, texture Texture=Nil, object AttachedObject=Nil]", - "Creates a persistent particle. Persistent variables are handled specially by the engine so that this " - "function can return a handle to the caller. Only use this if you absolutely need it. Use createParticle if " - "the returned handle is not required. Use PARTICLE_* enumerations for type." - "Reverse reverse animation, if one is specified" - "Attached object specifies object that Position will be (and always be) relative to.", - "particle", - "Handle to the created particle") -{ - particle::particle_info pi; - pi.bitmap = -1; - pi.attached_objnum = -1; - pi.attached_sig = -1; - pi.reverse = false; +particle::ParticleEffectHandle getLegacyScriptingParticleEffect(int bitmap, bool reversed) { + static SCP_map, particle::ParticleEffectHandle> custom_texture_effects; + + bool is_builtin_bitmap = bitmap == particle::Anim_bitmap_id_fire || bitmap == particle::Anim_bitmap_id_smoke || bitmap == particle::Anim_bitmap_id_smoke2; + + if (!is_builtin_bitmap) { + auto it = custom_texture_effects.find(std::make_pair(bitmap, reversed)); + if (it != custom_texture_effects.end()) + return it->second; + } + + particle::ParticleEffect effect( + "", //Name + ::util::UniformFloatRange(1.f), //Particle num + particle::ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only + particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction + ::util::UniformFloatRange(), //Velocity Inherit + false, //Velocity Inherit absolute? + std::make_unique(::util::ParsedRandomFloatRange(), 1.f), //Velocity volume + ::util::UniformFloatRange(1.f), //Velocity volume multiplier + particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling + std::nullopt, //Orientation-based velocity + std::nullopt, //Position-based velocity + nullptr, //Position volume + particle::ParticleEffectHandle::invalid(), //Trail + 1.f, //Chance + false, //Affected by detail + -1.f, //Culling range multiplier + false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + reversed, //Is reversed? + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset + ::util::UniformFloatRange(1.f), //Lifetime + ::util::UniformFloatRange(1.f), //Radius + bitmap); + + effect.m_parent_local = true; + effect.m_parentLifetime = true; + effect.m_parentScale = true; + + particle::ParticleEffectHandle handle = particle::ParticleManager::get()->addEffect(std::move(effect)); + + if (!is_builtin_bitmap) { + custom_texture_effects.emplace(std::make_pair(bitmap, reversed), handle); + } + + return handle; +} + +static int spawnParticles(lua_State *L, bool persistent) { + vec3d pos, vel; + float lifetime, rad; // Need to consume tracer_length parameter but it isn't used anymore float temp; @@ -2217,55 +2259,106 @@ ADE_FUNC(createPersistentParticle, bool rev = false; object_h* objh = nullptr; texture_h* texture = nullptr; - if (!ade_get_args(L, "ooff|ofboo", l_Vector.Get(&pi.pos), l_Vector.Get(&pi.vel), &pi.lifetime, &pi.rad, - l_Enum.GetPtr(&type), &temp, &rev, l_Texture.GetPtr(&texture), l_Object.GetPtr(&objh))) - return ADE_RETURN_NIL; + if (!ade_get_args(L, "ooff|ofboo", l_Vector.Get(&pos), l_Vector.Get(&vel), &lifetime, &rad, + l_Enum.GetPtr(&type), &temp, &rev, l_Texture.GetPtr(&texture), l_Object.GetPtr(&objh))) + return persistent ? ADE_RETURN_NIL : ADE_RETURN_FALSE; + + particle::ParticleEffectHandle handle; if (type != nullptr) { switch (type->index) { case LE_PARTICLE_DEBUG: - LuaError(L, "Debug particles are deprecated as of FSO 25.0.0!"); - return ADE_RETURN_NIL; - case LE_PARTICLE_FIRE: - pi.bitmap = particle::Anim_bitmap_id_fire; - pi.nframes = particle::Anim_num_frames_fire; - break; - case LE_PARTICLE_SMOKE: - pi.bitmap = particle::Anim_bitmap_id_smoke; - pi.nframes = particle::Anim_num_frames_smoke; - break; - case LE_PARTICLE_SMOKE2: - pi.bitmap = particle::Anim_bitmap_id_smoke2; - pi.nframes = particle::Anim_num_frames_smoke2; - break; + LuaError(L, "Debug particles are deprecated as of FSO 25.0.0!"); + return persistent ? ADE_RETURN_NIL : ADE_RETURN_FALSE; + case LE_PARTICLE_FIRE: { + static auto fire_handle = getLegacyScriptingParticleEffect(particle::Anim_bitmap_id_fire, false); + static auto fire_handle_rev = getLegacyScriptingParticleEffect(particle::Anim_bitmap_id_fire, true); + handle = rev ? fire_handle_rev : fire_handle; + break; + } + case LE_PARTICLE_SMOKE: { + static auto smoke_handle = getLegacyScriptingParticleEffect(particle::Anim_bitmap_id_smoke, false); + static auto smoke_handle_rev = getLegacyScriptingParticleEffect(particle::Anim_bitmap_id_smoke, true); + handle = rev ? smoke_handle_rev : smoke_handle; + break; + } + case LE_PARTICLE_SMOKE2: { + static auto smoke2_handle = getLegacyScriptingParticleEffect(particle::Anim_bitmap_id_smoke2, false); + static auto smoke2_handle_rev = getLegacyScriptingParticleEffect(particle::Anim_bitmap_id_smoke2, true); + handle = rev ? smoke2_handle_rev : smoke2_handle; + break; + } case LE_PARTICLE_BITMAP: if (texture == nullptr || !texture->isValid()) { LuaError(L, "Invalid texture specified for createParticle()!"); - return ADE_RETURN_NIL; + return persistent ? ADE_RETURN_NIL : ADE_RETURN_FALSE; } else { - pi.bitmap = texture->handle; + handle = getLegacyScriptingParticleEffect(texture->handle, rev); } break; default: LuaError(L, "Invalid particle enum for createParticle(). Can only support PARTICLE_* enums!"); - return ADE_RETURN_NIL; + return persistent ? ADE_RETURN_NIL : ADE_RETURN_FALSE; } } - if (rev) - pi.reverse = false; - + std::unique_ptr host; if (objh != nullptr && objh->isValid()) { - pi.attached_objnum = objh->objnum; - pi.attached_sig = objh->sig; + host = std::make_unique(objh->objp(), pos); + vel += objh->objp()->phys_info.vel; + } + else { + host = std::make_unique(pos, vmd_identity_matrix, vmd_zero_vector); } - particle::WeakParticlePtr p = particle::createPersistent(&pi); + //Beware that manually creating a particle source like this is REALLY bad. + //The only reason I am doing it here is because of the following three reasons: + // 1. the effect guarantees to finish in a single frame + // 2. we NEED the return particle ptrs for the persistent path + // 3. Scripting gets to set certain values at runtime which are usually encoded as a behaviour in the particle effect and thus tabled statically. - if (!p.expired()) - return ade_set_args(L, "o", l_Particle.Set(particle_h(p))); - else - return ADE_RETURN_NIL; + const auto& [parent, parent_sig] = host->getParentObjAndSig(); + + particle::ParticleSource source; + source.setEffect(handle); + source.setHost(std::move(host)); + source.setTriggerRadius(rad); + source.setTriggerVelocity(lifetime); + + if (persistent) { + auto spawned_particles = particle::ParticleManager::get() + ->getEffect(handle) + .front() + .processSourcePersistent(0, source, 0, vel, parent, parent_sig, lifetime, rad, 1); + + Assertion(spawned_particles.size() == 1, "Did not spawn a single particle in createPersistentParticle"); + + const particle::WeakParticlePtr& p = spawned_particles.front(); + + if (!p.expired()) + return ade_set_args(L, "o", l_Particle.Set(particle_h(p))); + else + return persistent ? ADE_RETURN_NIL : ADE_RETURN_FALSE; + } + else { + particle::ParticleManager::get()->getEffect(handle).front().processSource(0, source, 0, vel, parent, parent_sig, lifetime, rad, 1); + return persistent ? ADE_RETURN_NIL : ADE_RETURN_FALSE; + } +} + +ADE_FUNC(createPersistentParticle, + l_Graphics, + "vector Position, vector Velocity, number Lifetime, number Radius, [enumeration Type=PARTICLE_DEBUG, number TracerLength=-1, " + "boolean Reverse=false, texture Texture=Nil, object AttachedObject=Nil]", + "Creates a persistent particle. Persistent variables are handled specially by the engine so that this " + "function can return a handle to the caller. Only use this if you absolutely need it. Use createParticle if " + "the returned handle is not required. Use PARTICLE_* enumerations for type." + "Reverse reverse animation, if one is specified" + "Attached object specifies object that Position will be (and always be) relative to.", + "particle", + "Handle to the created particle") +{ + return spawnParticles(L, true); } ADE_FUNC(createParticle, @@ -2278,65 +2371,7 @@ ADE_FUNC(createParticle, "boolean", "true if particle was created, false otherwise") { - particle::particle_info pi; - pi.bitmap = -1; - pi.attached_objnum = -1; - pi.attached_sig = -1; - pi.reverse = false; - - // Need to consume tracer_length parameter but it isn't used anymore - float temp; - - enum_h* type = nullptr; - bool rev = false; - object_h* objh = nullptr; - texture_h* texture = nullptr; - if (!ade_get_args(L, "ooff|ofboo", l_Vector.Get(&pi.pos), l_Vector.Get(&pi.vel), &pi.lifetime, &pi.rad, - l_Enum.GetPtr(&type), &temp, &rev, l_Texture.GetPtr(&texture), l_Object.GetPtr(&objh))) - return ADE_RETURN_FALSE; - - if (type != nullptr) { - switch (type->index) { - case LE_PARTICLE_DEBUG: - LuaError(L, "Debug particles are deprecated as of FSO 25.0.0!"); - return ADE_RETURN_NIL; - case LE_PARTICLE_FIRE: - pi.bitmap = particle::Anim_bitmap_id_fire; - pi.nframes = particle::Anim_num_frames_fire; - break; - case LE_PARTICLE_SMOKE: - pi.bitmap = particle::Anim_bitmap_id_smoke; - pi.nframes = particle::Anim_num_frames_smoke; - break; - case LE_PARTICLE_SMOKE2: - pi.bitmap = particle::Anim_bitmap_id_smoke2; - pi.nframes = particle::Anim_num_frames_smoke2; - break; - case LE_PARTICLE_BITMAP: - if (texture == nullptr || !texture->isValid()) { - LuaError(L, "Invalid texture specified for createParticle()!"); - return ADE_RETURN_NIL; - } else { - pi.bitmap = texture->handle; - } - break; - default: - LuaError(L, "Invalid particle enum for createParticle(). Can only support PARTICLE_* enums!"); - return ADE_RETURN_NIL; - } - } - - if (rev) - pi.reverse = false; - - if (objh != nullptr && objh->isValid()) { - pi.attached_objnum = objh->objnum; - pi.attached_sig = objh->sig; - } - - particle::create(&pi); - - return ADE_RETURN_TRUE; + return spawnParticles(L, false); } ADE_FUNC(killAllParticles, l_Graphics, nullptr, "Clears all particles from a mission", nullptr, nullptr) diff --git a/code/scripting/api/libs/testing.cpp b/code/scripting/api/libs/testing.cpp index 6fb3633df5d..37c926006ca 100644 --- a/code/scripting/api/libs/testing.cpp +++ b/code/scripting/api/libs/testing.cpp @@ -125,71 +125,19 @@ ADE_FUNC_DEPRECATED(createParticle, gameversion::version(19, 0, 0, 0), "Not available in the testing library anymore. Use gr.createPersistentParticle instead.") { - particle::particle_info pi; - pi.bitmap = -1; - pi.attached_objnum = -1; - pi.attached_sig = -1; - pi.reverse = 0; - // Need to consume tracer_length parameter but it isn't used anymore + vec3d pos, vel; + float lifetime, rad; float temp; enum_h *type = NULL; bool rev=false; object_h *objh=NULL; texture_h* texture = nullptr; - if (!ade_get_args(L, "ooffo|fboo", l_Vector.Get(&pi.pos), l_Vector.Get(&pi.vel), &pi.lifetime, &pi.rad, - l_Enum.GetPtr(&type), &temp, &rev, l_Texture.GetPtr(&texture), l_Object.GetPtr(&objh))) - return ADE_RETURN_NIL; - - if(type != NULL) - { - switch(type->index) - { - case LE_PARTICLE_DEBUG: - LuaError(L, "Debug particles are deprecated as of FSO 25.0.0!"); - return ADE_RETURN_NIL; - case LE_PARTICLE_FIRE: - pi.bitmap = particle::Anim_bitmap_id_fire; - pi.nframes = particle::Anim_num_frames_fire; - break; - case LE_PARTICLE_SMOKE: - pi.bitmap = particle::Anim_bitmap_id_smoke; - pi.nframes = particle::Anim_num_frames_smoke; - break; - case LE_PARTICLE_SMOKE2: - pi.bitmap = particle::Anim_bitmap_id_smoke2; - pi.nframes = particle::Anim_num_frames_smoke2; - break; - case LE_PARTICLE_BITMAP: - if (texture == nullptr || !texture->isValid()) { - LuaError(L, "Invalid texture specified for createParticle()!"); - return ADE_RETURN_NIL; - } else { - pi.bitmap = texture->handle; - } - break; - default: - LuaError(L, "Invalid particle enum for createParticle(). Can only support PARTICLE_* enums!"); - return ADE_RETURN_NIL; - } - } - - if(rev) - pi.reverse = 0; + ade_get_args(L, "ooffo|fboo", l_Vector.Get(&pos), l_Vector.Get(&vel), &lifetime, &rad, + l_Enum.GetPtr(&type), &temp, &rev, l_Texture.GetPtr(&texture), l_Object.GetPtr(&objh)); - if(objh != NULL && objh->isValid()) - { - pi.attached_objnum = objh->objnum; - pi.attached_sig = objh->sig; - } - - particle::WeakParticlePtr p = particle::createPersistent(&pi); - - if (!p.expired()) - return ade_set_args(L, "o", l_Particle.Set(particle_h(p))); - else - return ADE_RETURN_NIL; + return ADE_RETURN_NIL; } ADE_FUNC(getStack, l_Testing, NULL, "Generates an ADE stackdump", "string", "Current Lua stack") diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index d3d451ce839..c0b3b01366e 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -1075,6 +1075,7 @@ void ship_info::clone(const ship_info& other) split_particles = other.split_particles; knossos_end_particles = other.knossos_end_particles; regular_end_particles = other.regular_end_particles; + debris_flame_particles = other.debris_flame_particles; debris_min_lifetime = other.debris_min_lifetime; debris_max_lifetime = other.debris_max_lifetime; @@ -1431,6 +1432,7 @@ void ship_info::move(ship_info&& other) std::swap(split_particles, other.split_particles); std::swap(knossos_end_particles, other.knossos_end_particles); std::swap(regular_end_particles, other.regular_end_particles); + std::swap(debris_flame_particles, other.debris_flame_particles); debris_min_lifetime = other.debris_min_lifetime; debris_max_lifetime = other.debris_max_lifetime; @@ -1795,6 +1797,8 @@ ship_info::ship_info() static auto default_regular_end_particles = default_ship_particle_effect(LegacyShipParticleType::OTHER, 100, 50, 1.5f, 0.1f, 4.0f, 0.5f, 20.0f, 0.0f, 2.0f, 1.0f, particle::Anim_bitmap_id_smoke2, 1.f, true); regular_end_particles = default_regular_end_particles; + debris_flame_particles = particle::ParticleEffectHandle::invalid(); + debris_min_lifetime = -1.0f; debris_max_lifetime = -1.0f; debris_min_speed = -1.0f; @@ -2046,7 +2050,7 @@ ship_info::ship_info() damage_lightning_type = SLT_DEFAULT; - shield_impact_explosion_anim = -1; + shield_impact_explosion_anim = particle::ParticleEffectHandle::invalid(); hud_gauges.clear(); hud_enabled = false; hud_retail = false; @@ -2529,6 +2533,9 @@ particle::ParticleEffectHandle create_ship_legacy_particle_effect(LegacyShipPart auto effect = particle::ParticleEffect( "", //Name particle_num, //Particle num + particle::ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only useNormal ? particle::ParticleEffect::ShapeDirection::HIT_NORMAL : particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(velocityInherit), //Velocity Inherit false, //Velocity Inherit absolute? @@ -2543,6 +2550,12 @@ particle::ParticleEffectHandle create_ship_legacy_particle_effect(LegacyShipPart true, //Affected by detail range, //Culling range multiplier true, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset lifetime, //Lifetime radius, //Radius bitmap); //Bitmap @@ -3838,6 +3851,11 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool sip->knossos_end_particles = parse_ship_legacy_particle_effect(LegacyShipParticleType::OTHER, sip, "knossos death spew", 50.f, particle::Anim_bitmap_id_smoke2, 1.f, true); } + if(optional_string("$Debris Flame Effect:")) + { + sip->debris_flame_particles = particle::util::parseEffect(sip->name); + } + auto skip_str = "$Skip Death Roll Percent Chance:"; auto vaporize_str = "$Vaporize Percent Chance:"; int which; @@ -4016,12 +4034,56 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool stuff_ubyte(&sip->shield_color[2]); } - if(optional_string("$Shield Impact Explosion:")) { + if(optional_string("$Shield Impact Explosion Effect:")) { + sip->shield_impact_explosion_anim = particle::util::parseEffect(sip->name); + } + else if(optional_string("$Shield Impact Explosion:")) { char fname[MAX_NAME_LEN]; stuff_string(fname, F_NAME, NAME_LENGTH); - if ( VALID_FNAME(fname) ) - sip->shield_impact_explosion_anim = Weapon_explosions.Load(fname); + if ( VALID_FNAME(fname) ) { + auto particle = particle::ParticleEffect( + "", //Name + ::util::UniformFloatRange(1.f), //Particle num + particle::ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only + particle::ParticleEffect::ShapeDirection::HIT_NORMAL, //Particle direction + ::util::UniformFloatRange(0.f), //Velocity Inherit + false, //Velocity Inherit absolute? + nullptr, //Velocity volume + ::util::UniformFloatRange(), //Velocity volume multiplier + particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling + std::nullopt, //Orientation-based velocity + std::nullopt, //Position-based velocity + nullptr, //Position volume + particle::ParticleEffectHandle::invalid(), //Trail + 1.f, //Chance + false, //Affected by detail + -1.f, //Culling range multiplier + false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + true, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset + ::util::UniformFloatRange(0.f), //Lifetime + ::util::UniformFloatRange(1.f), //Radius + bm_load_animation(fname)); //Bitmap + + static const int thruster_particle_curve = []() -> int { + int curve_id = static_cast(Curves.size()); + auto& curve = Curves.emplace_back(";ShipShieldParticles"); + curve.keyframes.emplace_back(curve_keyframe{vec2d{0.f, 0.f}, CurveInterpFunction::Linear, 0.f, 0.f}); + curve.keyframes.emplace_back(curve_keyframe{vec2d{100000.f, 100000.f}, CurveInterpFunction::Linear, 0.f, 0.f}); + return curve_id; + }(); + + particle.m_modular_curves.add_curve("Trigger Radius", particle::ParticleEffect::ParticleCurvesOutput::RADIUS_MULT, modular_curves_entry{thruster_particle_curve}); + + sip->shield_impact_explosion_anim = particle::ParticleManager::get()->addEffect(std::move(particle)); + } } if(optional_string("$Max Shield Recharge:")){ @@ -4842,6 +4904,9 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool auto particle = particle::ParticleEffect( "", //Name ::util::UniformFloatRange(i2fl(min_n), i2fl(max_n)), //Particle num + particle::ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(1.f), //Velocity Inherit true, //Velocity Inherit absolute? @@ -4856,6 +4921,12 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool true, //Affected by detail 1.0f, //Culling range multiplier false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset ::util::UniformFloatRange(0.0f, 1.0f), //Lifetime ::util::UniformFloatRange(min_rad, max_rad), //Radius tpart.thruster_bitmap.first_frame); //Bitmap diff --git a/code/ship/ship.h b/code/ship/ship.h index aece66879a3..24748a0a25c 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1227,6 +1227,7 @@ class ship_info particle::ParticleEffectHandle split_particles; particle::ParticleEffectHandle knossos_end_particles; particle::ParticleEffectHandle regular_end_particles; + particle::ParticleEffectHandle debris_flame_particles; //Debris stuff float debris_min_lifetime; @@ -1471,7 +1472,7 @@ class ship_info float emp_resistance_mod; float piercing_damage_draw_limit; - int shield_impact_explosion_anim; + particle::ParticleEffectHandle shield_impact_explosion_anim; int damage_lightning_type; diff --git a/code/ship/shipfx.cpp b/code/ship/shipfx.cpp index 8610874e68f..0eb934b792c 100644 --- a/code/ship/shipfx.cpp +++ b/code/ship/shipfx.cpp @@ -1083,9 +1083,6 @@ void shipfx_flash_create(object *objp, int model_num, vec3d *gun_pos, vec3d *gun particleSource->setTriggerVelocity(vm_vec_mag_quick(&weapon_objp->phys_info.vel)); particleSource->finishCreation(); - // if there's a muzzle flash entry and no muzzle effect entry, we use the mflash - } else if (Weapon_info[weapon_info_index].muzzle_flash >= 0) { - mflash_create(gun_pos, gun_dir, &objp->phys_info, Weapon_info[weapon_info_index].muzzle_flash, objp); } } diff --git a/code/source_groups.cmake b/code/source_groups.cmake index af16749c9ee..7f6c3efa68a 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -1140,6 +1140,10 @@ add_file_folder("Particle\\\\Volumes" particle/volumes/ConeVolume.h particle/volumes/LegacyAACuboidVolume.cpp particle/volumes/LegacyAACuboidVolume.h + particle/volumes/PointVolume.cpp + particle/volumes/PointVolume.h + particle/volumes/RingVolume.cpp + particle/volumes/RingVolume.h particle/volumes/SpheroidVolume.cpp particle/volumes/SpheroidVolume.h ) diff --git a/code/starfield/supernova.cpp b/code/starfield/supernova.cpp index 28ea0cfaef7..e822e7b9ba6 100644 --- a/code/starfield/supernova.cpp +++ b/code/starfield/supernova.cpp @@ -48,6 +48,9 @@ static particle::ParticleEffectHandle supernova_init_particle() { return particle::ParticleManager::get()->addEffect(particle::ParticleEffect( "", //Name ::util::UniformFloatRange(2.f, 5.f), //Particle num + particle::ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(1.f), //Velocity Inherit false, //Velocity Inherit absolute? @@ -62,6 +65,12 @@ static particle::ParticleEffectHandle supernova_init_particle() { true, //Affected by detail 1.f, //Culling range multiplier true, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset ::util::UniformFloatRange(0.6f, 1.f), //Lifetime ::util::UniformFloatRange(0.5f, 1.25f), //Radius particle::Anim_bitmap_id_fire)); //Bitmap diff --git a/code/utils/modular_curves.h b/code/utils/modular_curves.h index d25b6c2e855..136550aafa0 100644 --- a/code/utils/modular_curves.h +++ b/code/utils/modular_curves.h @@ -20,7 +20,7 @@ template struct modular_curves_submember_input { - private: + protected: template static inline auto grab_part(const input_type& input) { //Pointer to member function @@ -145,6 +145,39 @@ struct modular_curves_submember_input { } }; +//Allows submember grabbers on full inputs by using a reducer-function. +//Mostly useful for submember-like access if you need to combine multiple input components to get the value +template +struct modular_curves_submember_input_full : public modular_curves_submember_input { + public: + template + static inline float grab(const input_type& input) { + const auto& reduced = reducer(input); + const auto& result = modular_curves_submember_input::template grab_internal, grabbers...>(reduced); + if constexpr (is_optional_v>) { + if (result.has_value()) + return modular_curves_submember_input::number_to_float(result->get()); + else + return 1.0f; + } + else if constexpr (is_instance_of_v, std::reference_wrapper>) { + //We could also be returned not a temporary optional from a check, but a true optional stored somewhere, so check for this here + if constexpr (is_optional_v::type>>) { + const auto& inner_result = result.get(); + if (inner_result.has_value()) + return modular_curves_submember_input::number_to_float(*inner_result); + else + return 1.0f; + } + else + return modular_curves_submember_input::number_to_float(result.get()); + } + else { + return modular_curves_submember_input::number_to_float(result); + } + } +}; + template struct modular_curves_functional_input { private: @@ -170,10 +203,23 @@ struct modular_curves_functional_input { template struct modular_curves_functional_full_input { -public: - template + private: + template + static inline auto grab_from_tuple_vararg(const input_type& input, std::integer_sequence) { + return std::forward_as_tuple(std::get(input)...); + } + + template + static inline auto grab_from_tuple(const input_type& input) { + if constexpr(tuple_idx < 0) + return std::cref(input); + else + return grab_from_tuple_vararg(input, std::make_integer_sequence{}); + } + public: + template static inline float grab(const input_type& input) { - return grabber_fnc(input); + return grabber_fnc(grab_from_tuple(input)); } }; @@ -390,6 +436,8 @@ struct modular_curves_definition { } public: + using input_type_t = const input_type&; + template constexpr auto derive_modular_curves_subset(std::array, new_output_size> new_outputs, std::pair... additional_inputs) const { using new_input_type = decltype(unevaluated_maybe_tuple_cat(std::declval(), std::declval())); @@ -459,6 +507,8 @@ struct modular_curves_set { constexpr modular_curves_set() : curves() {} public: + using input_type_t = const input_type&; + // Used to create an instance for any single thing affected by modular curves. Note that having an instance is purely optional [[nodiscard]] modular_curves_entry_instance create_instance() const { return modular_curves_entry_instance{util::Random::next(), util::Random::next()}; diff --git a/code/weapon/beam.cpp b/code/weapon/beam.cpp index 6864d2757f9..770576dc07d 100644 --- a/code/weapon/beam.cpp +++ b/code/weapon/beam.cpp @@ -449,7 +449,6 @@ int beam_fire(beam_fire_info *fire_info) new_item->range = wip->b_info.range; new_item->damage_threshold = wip->b_info.damage_threshold; new_item->bank = fire_info->bank; - new_item->Beam_muzzle_stamp = -1; new_item->beam_glow_frame = 0.0f; new_item->firingpoint = (fire_info->bfi_flags & BFIF_FLOATING_BEAM) ? -1 : fire_info->turret->turret_next_fire_pos; new_item->last_start = fire_info->starting_pos; @@ -559,6 +558,38 @@ int beam_fire(beam_fire_info *fire_info) // start the warmup phase beam_start_warmup(new_item); + //Do particles + if (wip->b_info.beam_muzzle_effect.isValid()) { + auto source = particle::ParticleManager::get()->createSource(wip->b_info.beam_muzzle_effect); + + std::unique_ptr host; + if (new_item->objp == nullptr) { + vec3d beam_dir = new_item->last_shot - new_item->last_start; + matrix orient; + vm_vector_2_matrix(&orient, &beam_dir); + + host = std::make_unique(new_item->last_start, orient, vmd_zero_vector); + } + else if (new_item->subsys == nullptr || new_item->firingpoint < 0) { + vec3d beam_dir = new_item->last_shot - new_item->last_start; + matrix orient; + vm_vector_2_matrix(&orient, &beam_dir); + + vec3d local_pos = new_item->last_start - new_item->objp->pos; + vm_vec_rotate(&local_pos, &local_pos, &new_item->objp->orient); + orient = new_item->objp->orient * orient; + + host = std::make_unique(new_item->objp, local_pos, orient); + } + else { + host = std::make_unique(new_item->objp, new_item->subsys->system_info->turret_gun_sobj, new_item->firingpoint); + } + + source->setHost(std::move(host)); + source->setTriggerRadius(wip->b_info.beam_muzzle_radius); + source->finishCreation(); + } + return objnum; } @@ -1555,82 +1586,6 @@ void beam_render(beam *b, float u_offset) //gr_set_cull(cull); } -// generate particles for the muzzle glow -int hack_time = 100; -DCF(h_time, "Sets the hack time for beam muzzle glow (Default is 100)") -{ - dc_stuff_int(&hack_time); -} - -void beam_generate_muzzle_particles(beam *b) -{ - int particle_count; - int idx; - weapon_info *wip; - vec3d turret_norm, turret_pos, particle_pos, particle_dir; - matrix m; - - // if our hack stamp has expired - if(!((b->Beam_muzzle_stamp == -1) || timestamp_elapsed(b->Beam_muzzle_stamp))){ - return; - } - - // never generate anything past about 1/5 of the beam fire time - if(b->warmup_stamp == -1){ - return; - } - - // get weapon info - wip = &Weapon_info[b->weapon_info_index]; - - // no specified particle for this beam weapon - if (wip->b_info.beam_particle_ani.first_frame < 0) - return; - - - // reset the hack stamp - b->Beam_muzzle_stamp = timestamp(hack_time); - - // randomly generate 10 to 20 particles - particle_count = Random::next(wip->b_info.beam_particle_count+1); - - // get turret info - position and normal - turret_pos = b->last_start; - if (b->subsys != NULL) { - turret_norm = b->subsys->system_info->turret_norm; - } else { - vm_vec_normalized_dir(&turret_norm, &b->last_shot, &b->last_start); - } - - // randomly perturb a vector within a cone around the normal - vm_vector_2_matrix_norm(&m, &turret_norm, nullptr, nullptr); - for(idx=0; idxb_info.beam_particle_angle, &m); - vm_vec_scale_add(&particle_pos, &turret_pos, &particle_dir, wip->b_info.beam_muzzle_radius * frand_range(0.75f, 0.9f)); - - // now generate some interesting values for the particle - float p_time_ref = wip->b_info.beam_life + ((float)wip->b_info.beam_warmup / 1000.0f); - float p_life = frand_range(p_time_ref * 0.5f, p_time_ref * 0.7f); - float p_vel = (wip->b_info.beam_muzzle_radius / p_life) * frand_range(0.85f, 1.2f); - vm_vec_scale(&particle_dir, -p_vel); - if (b->objp != NULL) { - vm_vec_add2(&particle_dir, &b->objp->phys_info.vel); //move along with our parent - } - - particle::particle_info pinfo; - pinfo.pos = particle_pos; - pinfo.vel = particle_dir; - pinfo.lifetime = p_life; - pinfo.attached_objnum = -1; - pinfo.attached_sig = 0; - pinfo.rad = wip->b_info.beam_particle_radius; - pinfo.reverse = 1; - pinfo.bitmap = wip->b_info.beam_particle_ani.first_frame; - particle::create(&pinfo); - } -} - static float get_muzzle_glow_alpha(beam* b) { float dist; @@ -1899,10 +1854,7 @@ void beam_render_all() } // render the muzzle glow - beam_render_muzzle_glow(moveup); - - // maybe generate some muzzle particles - beam_generate_muzzle_particles(moveup); + beam_render_muzzle_glow(moveup); // next item moveup = GET_NEXT(moveup); diff --git a/code/weapon/beam.h b/code/weapon/beam.h index 1eeb9a509f7..56057cd35ea 100644 --- a/code/weapon/beam.h +++ b/code/weapon/beam.h @@ -209,7 +209,6 @@ typedef struct beam { beam_info binfo; int bank; - int Beam_muzzle_stamp; int firingpoint; float beam_collide_width; float beam_light_width; diff --git a/code/weapon/muzzleflash.cpp b/code/weapon/muzzleflash.cpp index a5050dc5bd5..be0d6ea9201 100644 --- a/code/weapon/muzzleflash.cpp +++ b/code/weapon/muzzleflash.cpp @@ -24,20 +24,17 @@ // muzzle flash info - read from a table typedef struct mflash_blob_info { char name[MAX_FILENAME_LEN]; - int anim_id; float offset; float radius; mflash_blob_info( const mflash_blob_info& mbi ) { strcpy_s( name, mbi.name ); - anim_id = mbi.anim_id; offset = mbi.offset; radius = mbi.radius; } mflash_blob_info() : - anim_id( -1 ), offset( 0.0 ), radius( 0.0 ) { @@ -47,7 +44,6 @@ typedef struct mflash_blob_info { mflash_blob_info& operator=( const mflash_blob_info& r ) { strcpy_s( name, r.name ); - anim_id = r.anim_id; offset = r.offset; radius = r.radius; @@ -57,11 +53,9 @@ typedef struct mflash_blob_info { typedef struct mflash_info { char name[MAX_FILENAME_LEN]; - int used_this_level; SCP_vector blobs; - mflash_info() - : used_this_level( 0 ) + mflash_info() { name[ 0 ] = '\0'; } @@ -69,14 +63,12 @@ typedef struct mflash_info { mflash_info( const mflash_info& mi ) { strcpy_s( name, mi.name ); - used_this_level = mi.used_this_level; blobs = mi.blobs; } mflash_info& operator=( const mflash_info& r ) { strcpy_s( name, r.name ); - used_this_level = r.used_this_level; blobs = r.blobs; return *this; @@ -88,7 +80,9 @@ SCP_vector Mflash_info; // --------------------------------------------------------------------------------------------------------------------- // MUZZLE FLASH FUNCTIONS -// +// + +static const SCP_string mflash_particle_prefix = ";MflashParticle;"; void parse_mflash_tbl(const char *filename) { @@ -158,172 +152,73 @@ void parse_mflash_tbl(const char *filename) } } -// initialize muzzle flash stuff for the whole game -void mflash_game_init() -{ - // parse main table first - parse_mflash_tbl("mflash.tbl"); - - // look for any modular tables - parse_modular_table(NOX("*-mfl.tbm"), parse_mflash_tbl); -} - -void mflash_mark_as_used(int index) -{ - if (index < 0) - return; - - Assert( index < (int)Mflash_info.size() ); - - Mflash_info[index].used_this_level++; -} - -void mflash_page_in(bool load_all) -{ - uint i, idx; - int num_frames, fps; - - // load up all anims - for ( i = 0; i < Mflash_info.size(); i++) { - // skip if it's not used - if ( !load_all && !Mflash_info[i].used_this_level ) - continue; - - // blobs - size_t original_num_blobs = Mflash_info[i].blobs.size(); - int original_idx = 1; - for ( idx = 0; idx < Mflash_info[i].blobs.size(); ) { - mflash_blob_info* mfbip = &Mflash_info[i].blobs[idx]; - mfbip->anim_id = bm_load_either(mfbip->name, &num_frames, &fps, NULL, true); - if ( mfbip->anim_id >= 0 ) { - bm_page_in_xparent_texture( mfbip->anim_id ); - ++idx; - } - else { - Warning(LOCATION, "Muzleflash \"%s\", blob [%d/" SIZE_T_ARG "]\nMuzzleflash blob \"%s\" not found! Deleting.", - Mflash_info[i].name, original_idx, original_num_blobs, Mflash_info[i].blobs[idx].name); - Mflash_info[i].blobs.erase( Mflash_info[i].blobs.begin() + idx ); +static void convert_mflash_to_particle() { + Curve new_curve = Curve(";MuzzleFlashMinSizeScalingCurve"); + new_curve.keyframes.push_back(curve_keyframe{vec2d{ -0.00001f , 0.f}, CurveInterpFunction::Polynomial, -1.0f, 1.0f}); //just for numerical safety if we ever get an actual size of 0... + new_curve.keyframes.push_back(curve_keyframe{vec2d{ Min_pizel_size_muzzleflash, 1.f }, CurveInterpFunction::Constant, 0.0f, 1.0f}); + Curves.push_back(new_curve); + modular_curves_entry scaling_curve {(static_cast(Curves.size()) - 1), ::util::UniformFloatRange(1.f), ::util::UniformFloatRange(0.f), false}; + + for (const auto& mflash : Mflash_info) { + SCP_vector subparticles; + + for (const auto& blob : mflash.blobs) { + subparticles.emplace_back( + mflash_particle_prefix + mflash.name, //Name + ::util::UniformFloatRange(1.f), //Particle num + particle::ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only + particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction + ::util::UniformFloatRange(1.f), //Velocity Inherit + false, //Velocity Inherit absolute? + nullptr, //Velocity volume + ::util::UniformFloatRange(), //Velocity volume multiplier + particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling + std::nullopt, //Orientation-based velocity + std::nullopt, //Position-based velocity + nullptr, //Position volume + particle::ParticleEffectHandle::invalid(), //Trail + 1.f, //Chance + false, //Affected by detail + -1.f, //Culling range multiplier + false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + true, //parent local + true, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + vec3d{{{0, 0, blob.offset}}}, //Local offset + ::util::UniformFloatRange(-1.f), //Lifetime + ::util::UniformFloatRange(blob.radius), //Radius + bm_load_animation(blob.name)); + + if (Min_pizel_size_muzzleflash > 0) { + subparticles.back().m_modular_curves.add_curve("Apparent Visual Size At Emitter", particle::ParticleEffect::ParticleCurvesOutput::RADIUS_MULT, scaling_curve); } - ++original_idx; } - } -} - -// initialize muzzle flash stuff for the level -void mflash_level_init() -{ - uint i, idx; - // reset all anim usage for this level - for ( i = 0; i < Mflash_info.size(); i++) { - for ( idx = 0; idx < Mflash_info[i].blobs.size(); idx++) { - Mflash_info[i].used_this_level = 0; - } + particle::ParticleManager::get()->addEffect(std::move(subparticles)); } -} -// shutdown stuff for the level -void mflash_level_close() -{ - uint i, idx; - - // release all anims - for ( i = 0; i < Mflash_info.size(); i++) { - // blobs - for ( idx = 0; idx < Mflash_info[i].blobs.size(); idx++) { - if ( Mflash_info[i].blobs[idx].anim_id < 0 ) - continue; - - bm_release( Mflash_info[i].blobs[idx].anim_id ); - Mflash_info[i].blobs[idx].anim_id = -1; - } - } + //Clean up no longer required data + Mflash_info.clear(); + Mflash_info.shrink_to_fit(); } -// create a muzzle flash on the guy -void mflash_create(const vec3d *gun_pos, const vec3d *gun_dir, const physics_info *pip, int mflash_type, const object *local) -{ - // mflash *mflashp; - mflash_info *mi; - mflash_blob_info *mbi; - uint idx; - - // standalone server should never create trails - if(Game_mode & GM_STANDALONE_SERVER){ - return; - } - - // illegal value - if ( (mflash_type < 0) || (mflash_type >= (int)Mflash_info.size()) ) - return; - - // create the actual animations - mi = &Mflash_info[mflash_type]; - - if (local != NULL) { - int attached_objnum = OBJ_INDEX(local); - - // This muzzle flash is in local space, so its world position must be derived to apply scaling. - vec3d gun_world_pos; - vm_vec_unrotate(&gun_world_pos, gun_pos, &Objects[attached_objnum].orient); - vm_vec_add2(&gun_world_pos, &Objects[attached_objnum].pos); - - for (idx = 0; idx < mi->blobs.size(); idx++) { - mbi = &mi->blobs[idx]; - - // bogus anim - if (mbi->anim_id < 0) - continue; - - // fire it up - particle::particle_info p; - vm_vec_scale_add(&p.pos, gun_pos, gun_dir, mbi->offset); - vm_vec_zero(&p.vel); - //vm_vec_scale_add(&p.vel, &pip->rotvel, &pip->vel, 1.0f); - p.bitmap = mbi->anim_id; - p.attached_objnum = attached_objnum; - p.attached_sig = local->signature; +// initialize muzzle flash stuff for the whole game +void mflash_game_init() +{ + // parse main table first + parse_mflash_tbl("mflash.tbl"); - // Scale the radius of the muzzle flash effect so that it always appears some minimum width in pixels. - p.rad = model_render_get_diameter_clamped_to_min_pixel_size(&gun_world_pos, mbi->radius * 2.0f, Min_pizel_size_muzzleflash) / 2.0f; + // look for any modular tables + parse_modular_table(NOX("*-mfl.tbm"), parse_mflash_tbl); - particle::create(&p); - } - } else { - for (idx = 0; idx < mi->blobs.size(); idx++) { - mbi = &mi->blobs[idx]; - - // bogus anim - if (mbi->anim_id < 0) - continue; - - // fire it up - particle::particle_info p; - vm_vec_scale_add(&p.pos, gun_pos, gun_dir, mbi->offset); - vm_vec_scale_add(&p.vel, &pip->rotvel, &pip->vel, 1.0f); - p.bitmap = mbi->anim_id; - p.attached_objnum = -1; - p.attached_sig = 0; - - // Scale the radius of the muzzle flash effect so that it always appears some minimum width in pixels. - p.rad = model_render_get_diameter_clamped_to_min_pixel_size(&p.pos, mbi->radius * 2.0f, Min_pizel_size_muzzleflash) / 2.0f; - - particle::create(&p); - } - } + //This should really happen at parse time, but that requires modular particle effects which aren't yet a thing + convert_mflash_to_particle(); } -// lookup type by name -int mflash_lookup(const char *name) -{ - uint idx; - - // look it up - for (idx = 0; idx < Mflash_info.size(); idx++) { - if ( !stricmp(name, Mflash_info[idx].name) ) - return idx; - } - - // couldn't find it - return -1; +particle::ParticleEffectHandle mflash_lookup(const char *name) { + return particle::ParticleManager::get()->getEffectByName(mflash_particle_prefix + name); } diff --git a/code/weapon/muzzleflash.h b/code/weapon/muzzleflash.h index e9f189986e0..752fb973f9f 100644 --- a/code/weapon/muzzleflash.h +++ b/code/weapon/muzzleflash.h @@ -13,6 +13,7 @@ #define __FS2_MUZZLEFLASH_HEADER_FILE #include "physics/physics.h" +#include "particle/ParticleManager.h" // --------------------------------------------------------------------------------------------------------------------- // MUZZLE FLASH DEFINES/VARS @@ -29,22 +30,7 @@ struct vec3d; // initialize muzzle flash stuff for the whole game void mflash_game_init(); -// initialize muzzle flash stuff for the level -void mflash_level_init(); - -// shutdown stuff for the level -void mflash_level_close(); - -// create a muzzle flash on the guy -void mflash_create(const vec3d *gun_pos, const vec3d *gun_dir, const physics_info *pip, int mflash_type, const object *local = nullptr); - // lookup type by name -int mflash_lookup(const char *name); - -// mark as used -void mflash_mark_as_used(int index = -1); - -// level page in -void mflash_page_in(bool load_all = false); +particle::ParticleEffectHandle mflash_lookup(const char *name); #endif diff --git a/code/weapon/weapon.h b/code/weapon/weapon.h index edc1392f486..bf5582ce645 100644 --- a/code/weapon/weapon.h +++ b/code/weapon/weapon.h @@ -65,16 +65,6 @@ enum class LR_Objecttypes { LRO_SHIPS, LRO_WEAPONS }; constexpr int BANK_SWITCH_DELAY = 250; // after switching banks, 1/4 second delay until player can fire -//particle names go here -nuke -#define PSPEW_NONE -1 //used to disable a spew, useful for xmts -#define PSPEW_DEFAULT 0 //std fs2 pspew -#define PSPEW_HELIX 1 //q2 style railgun trail -#define PSPEW_SPARKLER 2 //random particles in every direction, can be sperical or ovoid -#define PSPEW_RING 3 //outward expanding ring -#define PSPEW_PLUME 4 //spewers arrayed within a radius for thruster style effects, may converge or scatter - -#define MAX_PARTICLE_SPEWERS 4 //i figure 4 spewers should be enough for now -nuke - // scale factor for supercaps taking damage from weapons which are not "supercap" weapons #define SUPERCAP_DAMAGE_SCALE 0.25f @@ -144,10 +134,6 @@ typedef struct weapon { // corkscrew info (taken out for now) short cscrew_index; // corkscrew info index - // particle spew info - int particle_spew_time[MAX_PARTICLE_SPEWERS]; // time to spew next bunch of particles - float particle_spew_rand; // per weapon randomness value used by some particle spew types -nuke - // flak info short flak_index; // flak info index @@ -238,10 +224,7 @@ typedef struct beam_weapon_info { int beam_warmup; // how long it takes to warmup (in ms) int beam_warmdown; // how long it takes to warmdown (in ms) float beam_muzzle_radius; // muzzle glow radius - int beam_particle_count; // beam spew particle count - float beam_particle_radius; // radius of beam particles - float beam_particle_angle; // angle of beam particle spew cone - generic_anim beam_particle_ani; // particle_ani + particle::ParticleEffectHandle beam_muzzle_effect; SCP_map> beam_iff_miss_factor; // magic # which makes beams miss more. by parent iff and player skill level gamesnd_id beam_loop_sound; // looping beam sound gamesnd_id beam_warmup_sound; // warmup sound @@ -266,22 +249,6 @@ typedef struct beam_weapon_info { type5_beam_info t5info; // type 5 beams only } beam_weapon_info; -typedef struct particle_spew_info { //this will be used for multi spews - // particle spew stuff - int particle_spew_type; //added pspew type field -nuke - int particle_spew_count; - int particle_spew_time; - float particle_spew_vel; - float particle_spew_radius; - float particle_spew_lifetime; - float particle_spew_scale; - float particle_spew_z_scale; //length value for some effects -nuke - float particle_spew_rotation_rate; //rotation rate for some particle effects -nuke - vec3d particle_spew_offset; //offsets and normals, yay! - vec3d particle_spew_velocity; - generic_anim particle_spew_anim; -} particle_spew_info; - typedef struct spawn_weapon_info { short spawn_wep_index; // weapon info index of the child weapon, during parsing instead an index into Spawn_names @@ -572,9 +539,6 @@ struct weapon_info // tag stuff float tag_time; // how long the tag lasts int tag_level; // tag level (1 - 3) - - // muzzle flash - int muzzle_flash; // muzzle flash stuff float field_of_fire; //cone the weapon will fire in, 0 is strait all the time-Bobboau float fof_spread_rate; //How quickly the FOF will spread for each shot (primary weapons only, this doesn't really make sense for turrets) @@ -619,7 +583,7 @@ struct weapon_info beam_weapon_info b_info; // this must be valid if the weapon is a beam weapon WIF_BEAM or WIF_BEAM_SMALL // now using new particle spew struct -nuke - particle_spew_info particle_spewers[MAX_PARTICLE_SPEWERS]; + SCP_vector particle_spewers; // Countermeasure information float cm_aspect_effectiveness; @@ -933,43 +897,6 @@ typedef struct missile_obj { } missile_obj; extern missile_obj Missile_obj_list; -// WEAPON EXPLOSION INFO -#define MAX_WEAPON_EXPL_LOD 4 - -typedef struct weapon_expl_lod { - char filename[MAX_FILENAME_LEN]; - int bitmap_id; - int num_frames; - int fps; - - weapon_expl_lod( ) - : bitmap_id( -1 ), num_frames( 0 ), fps( 0 ) - { - filename[ 0 ] = 0; - } -} weapon_expl_lod; - -typedef struct weapon_expl_info { - int lod_count; - weapon_expl_lod lod[MAX_WEAPON_EXPL_LOD]; -} weapon_expl_info; - -class weapon_explosions -{ -private: - SCP_vector ExplosionInfo; - int GetIndex(const char *filename) const; - -public: - weapon_explosions(); - - int Load(const char *filename = nullptr, int specified_lods = MAX_WEAPON_EXPL_LOD); - int GetAnim(int weapon_expl_index, const vec3d *pos, float size) const; - void PageIn(int idx); -}; - -extern weapon_explosions Weapon_explosions; - extern int Num_weapons; extern int First_secondary_index; extern int Default_cmeasure_index; @@ -1034,9 +961,6 @@ void weapon_set_tracking_info(int weapon_objnum, int parent_objnum, int target_o // src_turret may be null size_t* get_pointer_to_weapon_fire_pattern_index(int weapon_type, int ship_idx, ship_subsys* src_turret); -// for weapons flagged as particle spewers, spew particles. wheee -void weapon_maybe_spew_particle(object *obj); - bool weapon_armed(weapon *wp, bool hit_target); void weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, int quadrant = -1, const vec3d* hitnormal = nullptr, const vec3d* local_hitpos = nullptr, int submodel = -1 ); void spawn_child_weapons( object *objp, int spawn_index_override = -1); @@ -1082,7 +1006,7 @@ void weapon_unpause_sounds(); // Called by hudartillery.cpp after SSMs have been parsed to make sure that $SSM: entries defined in weapons are valid. void validate_SSM_entries(); -void shield_impact_explosion(const vec3d *hitpos, const object *objp, float radius, int idx); +void shield_impact_explosion(const vec3d& hitpos, const vec3d& hitdir, const object *objp, const object *weapon_objp, float radius, particle::ParticleEffectHandle handle); // Swifty - return number of max simultaneous locks int weapon_get_max_missile_seekers(weapon_info *wip); diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index e606986c867..59c3adbe39e 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -56,8 +56,11 @@ #include "weapon/muzzleflash.h" #include "weapon/swarm.h" #include "particle/ParticleEffect.h" +#include "particle/volumes/ConeVolume.h" #include "particle/volumes/LegacyAACuboidVolume.h" #include "particle/volumes/SpheroidVolume.h" +#include "particle/volumes/RingVolume.h" +#include "particle/volumes/PointVolume.h" #include "tracing/Monitor.h" #include "tracing/tracing.h" #include "weapon.h" @@ -126,10 +129,6 @@ flag_def_list_new Beam_info_flags[] = { const size_t Num_beam_info_flags = sizeof(Beam_info_flags) / sizeof(flag_def_list_new); -weapon_explosions Weapon_explosions; - -SCP_vector LOD_checker; - special_flag_def_list_new&> Weapon_Info_Flags[] = { { "spawn", Weapon::Info_Flags::Spawn, true, [](const SCP_string& spawn, weapon_info* weaponp, flagset& flags) { if (weaponp->num_spawn_weapons_defined < MAX_SPAWN_TYPES_PER_WEAPON) @@ -317,239 +316,7 @@ extern int compute_num_homing_objects(const object *target_objp); void weapon_spew_stats(WeaponSpewType type); - -weapon_explosions::weapon_explosions() -{ - ExplosionInfo.clear(); -} - -int weapon_explosions::GetIndex(const char *filename) const -{ - if ( filename == NULL ) { - Int3(); - return -1; - } - - for (size_t i = 0; i < ExplosionInfo.size(); i++) { - if ( !stricmp(ExplosionInfo[i].lod[0].filename, filename)) { - return (int)i; - } - } - - return -1; -} - -int weapon_explosions::Load(const char *filename, int expected_lods) -{ - char name_tmp[MAX_FILENAME_LEN] = ""; - int bitmap_id = -1; - int nframes, nfps; - weapon_expl_info new_wei; - - Assert( expected_lods <= MAX_WEAPON_EXPL_LOD ); - - //Check if it exists - int idx = GetIndex(filename); - - if (idx != -1) - return idx; - - new_wei.lod_count = 1; - - strcpy_s(new_wei.lod[0].filename, filename); - new_wei.lod[0].bitmap_id = bm_load_animation(filename, &new_wei.lod[0].num_frames, &new_wei.lod[0].fps, nullptr, nullptr, true); - - if (new_wei.lod[0].bitmap_id < 0) { - Warning(LOCATION, "Weapon explosion '%s' does not have an LOD0 anim!", filename); - - // if we don't have the first then it's only safe to assume that the rest are missing or not usable - return -1; - } - - // 2 chars for the lod, 4 for the extension that gets added automatically - if ( (MAX_FILENAME_LEN - strlen(filename)) > 6 ) { - for (idx = 1; idx < expected_lods; idx++) { - sprintf(name_tmp, "%s_%d", filename, idx); - - bitmap_id = bm_load_animation(name_tmp, &nframes, &nfps, nullptr, nullptr, true); - - if (bitmap_id > 0) { - strcpy_s(new_wei.lod[idx].filename, name_tmp); - new_wei.lod[idx].bitmap_id = bitmap_id; - new_wei.lod[idx].num_frames = nframes; - new_wei.lod[idx].fps = nfps; - - new_wei.lod_count++; - } else { - break; - } - } - - if (new_wei.lod_count != expected_lods) - Warning(LOCATION, "For '%s', %i of %i LODs are missing!", filename, expected_lods - new_wei.lod_count, expected_lods); - } - else { - Warning(LOCATION, "Filename '%s' is too long to have any LODs.", filename); - } - - ExplosionInfo.push_back( new_wei ); - - return (int)(ExplosionInfo.size() - 1); -} - -void weapon_explosions::PageIn(int idx) -{ - int i; - - if ( (idx < 0) || (idx >= (int)ExplosionInfo.size()) ) - return; - - weapon_expl_info *wei = &ExplosionInfo[idx]; - - for ( i = 0; i < wei->lod_count; i++ ) { - if ( wei->lod[i].bitmap_id >= 0 ) { - bm_page_in_xparent_texture( wei->lod[i].bitmap_id, wei->lod[i].num_frames ); - } - } -} - -int weapon_explosions::GetAnim(int weapon_expl_index, const vec3d *pos, float size) const -{ - if ( (weapon_expl_index < 0) || (weapon_expl_index >= (int)ExplosionInfo.size()) ) - return -1; - - //Get our weapon expl for the day - auto wei = &ExplosionInfo[weapon_expl_index]; - - if (wei->lod_count == 1) - return wei->lod[0].bitmap_id; - - // now we have to do some work - vertex v; - int x, y, w, h, bm_size; - int must_stop = 0; - int best_lod = 1; - int behind = 0; - - // start the frame - extern int G3_count; - - if(!G3_count){ - g3_start_frame(1); - must_stop = 1; - } - g3_set_view_matrix(&Eye_position, &Eye_matrix, Eye_fov); - - // get extents of the rotated bitmap - g3_rotate_vertex(&v, pos); - - // if vertex is behind, find size if in front, then drop down 1 LOD - if (v.codes & CC_BEHIND) { - float dist = vm_vec_dist_quick(&Eye_position, pos); - vec3d temp; - - behind = 1; - vm_vec_scale_add(&temp, &Eye_position, &Eye_matrix.vec.fvec, dist); - g3_rotate_vertex(&v, &temp); - - // if still behind, bail and go with default - if (v.codes & CC_BEHIND) { - behind = 0; - } - } - - if (!g3_get_bitmap_dims(wei->lod[0].bitmap_id, &v, size, &x, &y, &w, &h, &bm_size)) { - if (Detail.hardware_textures == 4) { - // straight LOD - if(w <= bm_size/8){ - best_lod = 3; - } else if(w <= bm_size/2){ - best_lod = 2; - } else if(w <= 1.3f*bm_size){ - best_lod = 1; - } else { - best_lod = 0; - } - } else { - // less aggressive LOD for lower detail settings - if(w <= bm_size/8){ - best_lod = 3; - } else if(w <= bm_size/3){ - best_lod = 2; - } else if(w <= (1.15f*bm_size)){ - best_lod = 1; - } else { - best_lod = 0; - } - } - } - - // if it's behind, bump up LOD by 1 - if (behind) - best_lod++; - - // end the frame - if (must_stop) - g3_end_frame(); - - best_lod = MIN(best_lod, wei->lod_count - 1); - Assert( (best_lod >= 0) && (best_lod < MAX_WEAPON_EXPL_LOD) ); - - return wei->lod[best_lod].bitmap_id; -} - - -void parse_weapon_expl_tbl(const char *filename) -{ - uint i; - lod_checker lod_check; - - try - { - read_file_text(filename, CF_TYPE_TABLES); - reset_parse(); - - required_string("#Start"); - while (required_string_either("#End", "$Name:")) - { - memset(&lod_check, 0, sizeof(lod_checker)); - - // base filename - required_string("$Name:"); - stuff_string(lod_check.filename, F_NAME, MAX_FILENAME_LEN); - - //Do we have an LOD num - if (optional_string("$LOD:")) - { - stuff_int(&lod_check.num_lods); - } - - // only bother with this if we have 1 or more lods and less than max lods, - // otherwise the stardard level loading will take care of the different effects - if ((lod_check.num_lods > 0) && (lod_check.num_lods < MAX_WEAPON_EXPL_LOD)) { - // name check, update lod count if it already exists - for (i = 0; i < LOD_checker.size(); i++) { - if (!stricmp(LOD_checker[i].filename, lod_check.filename)) { - LOD_checker[i].num_lods = lod_check.num_lods; - } - } - - // old entry not found, add new entry - if (i == LOD_checker.size()) { - LOD_checker.push_back(lod_check); - } - } - } - required_string("#End"); - } - catch (const parse::ParseException& e) - { - mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", filename, e.what())); - return; - } -} - -/** +/* * Clear out the Missile_obj_list */ void missile_obj_list_init() @@ -866,6 +633,143 @@ void parse_shockwave_info(shockwave_create_info *sci, const char *pre_char) static SCP_vector Removed_weapons; +enum Pspew_legacy_type { + PSPEW_NONE, //used to disable a spew, useful for xmts + PSPEW_DEFAULT, //std fs2 pspew + PSPEW_HELIX, //q2 style railgun trail + PSPEW_SPARKLER, //random particles in every direction, can be sperical or ovoid + PSPEW_RING, //outward expanding ring + PSPEW_PLUME, //spewers arrayed within a radius for thruster style effects, may converge or scatter +}; + +struct pspew_legacy_parse_data { + // particle spew stuff + Pspew_legacy_type particle_spew_type; //added pspew type field -nuke + int particle_spew_count; + int particle_spew_time; + float particle_spew_vel; + float particle_spew_radius; + float particle_spew_lifetime; + float particle_spew_scale; + float particle_spew_z_scale; //length value for some effects -nuke + float particle_spew_rotation_rate; //rotation rate for some particle effects -nuke + vec3d particle_spew_offset; //offsets and normals, yay! + vec3d particle_spew_velocity; + SCP_string particle_spew_anim; +}; + +static SCP_unordered_map> pspew_legacy_parse_data_buffer; + +static particle::ParticleEffectHandle convertLegacyPspewBuffer(const pspew_legacy_parse_data& pspew_buffer, const weapon_info* wip) { + auto particle_spew_count = static_cast(pspew_buffer.particle_spew_count); + float particle_spew_spawns_per_second = 1000.f / static_cast(pspew_buffer.particle_spew_time); + + if (particle_spew_spawns_per_second > 60.f) { + error_display(0, "PSPEW requested with a spawn frequency of over 60FPS. This used to be capped to spawn once a frame. It will now be artificially capped at 60 spawns per second."); + particle_spew_spawns_per_second = 60.f; + } + + bool hasAnim = !pspew_buffer.particle_spew_anim.empty() && bm_validate_filename(pspew_buffer.particle_spew_anim, true, true); + + std::unique_ptr velocity_vol, position_vol; + bool absolutePositionVelocityInherit = false; + std::optional<::util::ParsedRandomFloatRange> positionBasedVelocity = std::nullopt; + particle::ParticleEffect::ShapeDirection direction = particle::ParticleEffect::ShapeDirection::ALIGNED; + + switch (pspew_buffer.particle_spew_type) { + case PSPEW_DEFAULT: + position_vol = std::make_unique(::util::UniformFloatRange(-PI_2, PI_2), 3.f * pspew_buffer.particle_spew_scale); + direction = particle::ParticleEffect::ShapeDirection::REVERSE; + break; + case PSPEW_HELIX: { + particle_spew_count = 1.f; + particle_spew_spawns_per_second *= pspew_buffer.particle_spew_count; + + int curve_id = static_cast(Curves.size()); + auto& curve = Curves.emplace_back(SCP_string(";PSPEWHelixCurve;") + wip->name); + curve.keyframes.emplace_back(curve_keyframe{vec2d{0.f, 0.f}, CurveInterpFunction::Linear, 0.f, 0.f}); + curve.keyframes.emplace_back(curve_keyframe{vec2d{1.f / pspew_buffer.particle_spew_rotation_rate, PI2}, CurveInterpFunction::Linear, 0.f, 0.f}); + + auto vel_vol_temp = std::make_unique(); + vel_vol_temp->posOffset = vec3d {{{pspew_buffer.particle_spew_scale, 0.f, 0.f}}}; + vel_vol_temp->m_modular_curves.add_curve("Time Running", particle::PointVolume::VolumeModularCurveOutput::OFFSET_ROT, modular_curves_entry{curve_id, ::util::UniformFloatRange(1.f), ::util::UniformFloatRange(0.f, 1.f / pspew_buffer.particle_spew_rotation_rate), true}); + velocity_vol = std::move(vel_vol_temp); + } + break; + case PSPEW_SPARKLER: { + //This is really strange behaviour cause the old sparklers (likely accidentally) cumulated the random velocity for each particle. + //This does not do this, but at least tries to emulate the resulting velocity magnitudes in similar chaotic fashion. + int curve_id_dist = static_cast(Curves.size()); + auto& curve_dist = Curves.emplace_back(SCP_string(";PSPEWSparklerCurveDist;") + wip->name); + curve_dist.keyframes.emplace_back(curve_keyframe{vec2d{0.f, 1.f / particle_spew_count}, CurveInterpFunction::Linear, 0.f, 0.f}); + curve_dist.keyframes.emplace_back(curve_keyframe{vec2d{1.f, 1.f}, CurveInterpFunction::Linear, 0.f, 0.f}); + + int curve_id_bias = static_cast(Curves.size()); + auto& curve_bias = Curves.emplace_back(SCP_string(";PSPEWSparklerCurveBias;") + wip->name); + curve_bias.keyframes.emplace_back(curve_keyframe{vec2d{0.f, 0.f}, CurveInterpFunction::Linear, 0.f, 0.f}); + curve_bias.keyframes.emplace_back(curve_keyframe{vec2d{1.f, particle_spew_count}, CurveInterpFunction::Linear, 0.f, 0.f}); + + auto vel_vol_temp = std::make_unique(1.f, pspew_buffer.particle_spew_z_scale, pspew_buffer.particle_spew_scale * particle_spew_count); + vel_vol_temp->m_modular_curves.add_curve("Fraction Particles Spawned", particle::SpheroidVolume::VolumeModularCurveOutput::RADIUS, modular_curves_entry{curve_id_dist}); + vel_vol_temp->m_modular_curves.add_curve("Fraction Particles Spawned", particle::SpheroidVolume::VolumeModularCurveOutput::BIAS, modular_curves_entry{curve_id_bias}); + velocity_vol = std::move(vel_vol_temp); + } + break; + case PSPEW_RING: { + static const int ring_pspew_rot = []() -> int { + int curve_id = static_cast(Curves.size()); + auto& curve = Curves.emplace_back(";PSPEWRingCurve"); + curve.keyframes.emplace_back(curve_keyframe{vec2d{0.f, 0.f}, CurveInterpFunction::Linear, 0.f, 0.f}); + curve.keyframes.emplace_back(curve_keyframe{vec2d{1.f, PI2}, CurveInterpFunction::Linear, 0.f, 0.f}); + return curve_id; + }(); + + auto vel_vol_temp = std::make_unique(); + vel_vol_temp->posOffset = vec3d {{{pspew_buffer.particle_spew_scale, 0.f, 0.f}}}; + vel_vol_temp->m_modular_curves.add_curve("Fraction Particles Spawned", particle::PointVolume::VolumeModularCurveOutput::OFFSET_ROT, modular_curves_entry{ring_pspew_rot}); + velocity_vol = std::move(vel_vol_temp); + } + break; + case PSPEW_PLUME: + position_vol = std::make_unique(pspew_buffer.particle_spew_scale, false); + positionBasedVelocity = ::util::UniformFloatRange(pspew_buffer.particle_spew_z_scale); + absolutePositionVelocityInherit = true; + break; + default: + UNREACHABLE("Invalid PSPEW legacy type!"); + } + + return particle::ParticleManager::get()->addEffect(particle::ParticleEffect( + "", //Name + ::util::UniformFloatRange(particle_spew_count), //Particle num + particle::ParticleEffect::Duration::ALWAYS, //permanent Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (particle_spew_spawns_per_second), //Single particle only + direction, //Particle direction + ::util::UniformFloatRange(pspew_buffer.particle_spew_vel), //Velocity Inherit + false, //Velocity Inherit absolute? + std::move(velocity_vol), //Velocity volume + ::util::UniformFloatRange(1.f), //Velocity volume multiplier + particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling + std::nullopt, //Orientation-based velocity + positionBasedVelocity, //Position-based velocity + std::move(position_vol), //Position volume + particle::ParticleEffectHandle::invalid(), //Trail + 1.f, //Chance + false, //Affected by detail + -1.f, //Culling range multiplier + !hasAnim, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + absolutePositionVelocityInherit, //position velocity inherit absolute? + IS_VEC_NULL(&pspew_buffer.particle_spew_velocity) ? std::nullopt : std::optional(pspew_buffer.particle_spew_velocity), //Local velocity offset + IS_VEC_NULL(&pspew_buffer.particle_spew_offset) ? std::nullopt : std::optional(pspew_buffer.particle_spew_offset), //Local offset + ::util::UniformFloatRange(pspew_buffer.particle_spew_lifetime), //Lifetime + ::util::UniformFloatRange(pspew_buffer.particle_spew_radius), //Radius + hasAnim ? bm_load_animation(pspew_buffer.particle_spew_anim.c_str()) : particle::Anim_bitmap_id_smoke)); //Bitmap +} + /** * Parse the information for a specific ship type. * Return weapon index if successful, otherwise return -1 @@ -2222,6 +2126,9 @@ int parse_weapon(int subtype, bool replace, const char *filename) wip->impact_weapon_expl_effect = ParticleManager::get()->addEffect(ParticleEffect( "", //Name ::util::UniformFloatRange(1.f), //Particle num + ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(0.f), //Velocity Inherit false, //Velocity Inherit absolute? @@ -2236,6 +2143,12 @@ int parse_weapon(int subtype, bool replace, const char *filename) false, //Affected by detail -1.f, //Culling range multiplier false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset ::util::UniformFloatRange(-1.f), //Lifetime radius, //Radius bitmapIndex)); //Bitmap @@ -2303,6 +2216,9 @@ int parse_weapon(int subtype, bool replace, const char *filename) wip->dinky_impact_weapon_expl_effect = ParticleManager::get()->addEffect(ParticleEffect( "", //Name ::util::UniformFloatRange(1.f), //Particle num + ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(0.f), //Velocity Inherit false, //Velocity Inherit absolute? @@ -2317,6 +2233,12 @@ int parse_weapon(int subtype, bool replace, const char *filename) false, //Affected by detail -1.f, //Culling range multiplier false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset ::util::UniformFloatRange(-1.f), //Lifetime radius, //Radius bitmapID)); //Bitmap @@ -2393,6 +2315,9 @@ int parse_weapon(int subtype, bool replace, const char *filename) wip->piercing_impact_effect = ParticleManager::get()->addEffect(ParticleEffect( "", //Name ::util::UniformFloatRange(count / 2.f, 2.f * count), //Particle num + ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(1.f), //Velocity Inherit false, //Velocity Inherit absolute? @@ -2407,6 +2332,12 @@ int parse_weapon(int subtype, bool replace, const char *filename) true, //Affected by detail 10.f, //Culling range multiplier false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset ::util::UniformFloatRange(0.25f * life, 2.0f * life), //Lifetime ::util::UniformFloatRange(0.5f * radius, 2.0f * radius), //Radius effectIndex)); //Bitmap @@ -2416,6 +2347,9 @@ int parse_weapon(int subtype, bool replace, const char *filename) wip->piercing_impact_secondary_effect = ParticleManager::get()->addEffect(ParticleEffect( "", //Name ::util::UniformFloatRange(count / 4.f, i2fl(count)), //Particle num + ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(1.f), //Velocity Inherit false, //Velocity Inherit absolute? @@ -2430,6 +2364,12 @@ int parse_weapon(int subtype, bool replace, const char *filename) true, //Affected by detail 10.f, //Culling range multiplier false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset ::util::UniformFloatRange(0.25f * life, 2.0f * life), //Lifetime ::util::UniformFloatRange(0.5f * radius, 2.0f * radius), //Radius effectIndex)); //Bitmap @@ -2503,14 +2443,11 @@ int parse_weapon(int subtype, bool replace, const char *filename) if (optional_string("$Muzzle Effect:")) { wip->muzzle_effect = particle::util::parseEffect(wip->name); - } else { - // muzzle flash - if (optional_string("$Muzzleflash:")) { - stuff_string(fname, F_NAME, NAME_LENGTH); + } else if (optional_string("$Muzzleflash:")) { + stuff_string(fname, F_NAME, NAME_LENGTH); - // look it up - wip->muzzle_flash = mflash_lookup(fname); - } + // look it up + wip->muzzle_effect = mflash_lookup(fname); } // EMP optional stuff (if WIF_EMP is not set, none of this matters, anyway) @@ -2900,25 +2837,83 @@ int parse_weapon(int subtype, bool replace, const char *filename) stuff_float(&wip->b_info.beam_muzzle_radius); } - // particle spew count - if(optional_string("+PCount:")) { - stuff_int(&wip->b_info.beam_particle_count); + if (optional_string("+Muzzle Particle Effect:")) { + wip->b_info.beam_muzzle_effect = particle::util::parseEffect(wip->name); } + else { + int pcount = 0; + float pradius = 1.f, pangle = 0.f; + SCP_string pani; - // particle radius - if(optional_string("+PRadius:")) { - stuff_float(&wip->b_info.beam_particle_radius); - } + if (wip->b_info.beam_muzzle_effect.isValid()) { + //We're modifying existing data. Restore old values... Ugly, but oh well. + const auto& oldEffect = particle::ParticleManager::get()->getEffect(wip->b_info.beam_muzzle_effect).front(); + pcount = static_cast(oldEffect.m_particleNum.max()); + pradius = oldEffect.m_radius.max(); + auto cone_volume = dynamic_cast(oldEffect.m_spawnVolume.get()); + if (cone_volume != nullptr) { + pangle = cone_volume->m_deviation.max(); + } + pani = bm_get_filename(oldEffect.m_bitmap_list.front()); + } - // angle off turret normal - if(optional_string("+PAngle:")) { - stuff_float(&wip->b_info.beam_particle_angle); - } + // particle spew count + if (optional_string("+PCount:")) { + stuff_int(&pcount); + } - // particle bitmap/ani - if ( optional_string("+PAni:") ) { - stuff_string(fname, F_NAME, NAME_LENGTH); - generic_anim_init(&wip->b_info.beam_particle_ani, fname); + // particle radius + if (optional_string("+PRadius:")) { + stuff_float(&pradius); + } + + // angle off turret normal + if (optional_string("+PAngle:")) { + stuff_float(&pangle); + pangle = fl_radians(pangle); + } + + // particle bitmap/ani + if (optional_string("+PAni:")) { + stuff_string(pani, F_NAME); + } + + if (pcount > 0 && !pani.empty()) { + float p_time_ref = wip->b_info.beam_life + ((float)wip->b_info.beam_warmup / 1000.0f); + float p_vel = wip->b_info.beam_muzzle_radius / (0.6f * p_time_ref); + + auto effect = particle::ParticleEffect( + "", //Name + ::util::UniformFloatRange(0.f, static_cast(pcount)), //Particle num + particle::ParticleEffect::Duration::RANGE, + ::util::UniformFloatRange((float)wip->b_info.beam_warmup / 1000.0f), //Emit for beam warmup time + ::util::UniformFloatRange (10.f), //One particle every 100ms + particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction + ::util::UniformFloatRange(1.f), //Velocity Inherit + false, //Velocity Inherit absolute? + nullptr, //Velocity volume + ::util::UniformFloatRange(), //Velocity volume multiplier + particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling + std::nullopt, //Orientation-based velocity + ::util::UniformFloatRange(p_vel * -1.2f, p_vel * -0.85f), //Position-based velocity + std::make_unique(::util::UniformFloatRange(-pangle, pangle), ::util::UniformFloatRange(wip->b_info.beam_muzzle_radius * 0.75f, wip->b_info.beam_muzzle_radius * 0.9f)), //Position volume + particle::ParticleEffectHandle::invalid(), //Trail + 1.f, //Chance + false, //Affected by detail + -1.f, //Culling range multiplier + true, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X and for most pspews + true, //reverse animation, for whatever reason + true, //parent local + true, //ignore velocity inherit if parented + true, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset + ::util::UniformFloatRange(0.5f * p_time_ref, 0.7f * p_time_ref), // Lifetime + ::util::UniformFloatRange(pradius), //Radius + bm_load_animation(pani.c_str())); //Bitmap + + wip->b_info.beam_muzzle_effect = particle::ParticleManager::get()->addEffect(std::move(effect)); + } } // magic miss # @@ -3085,6 +3080,9 @@ int parse_weapon(int subtype, bool replace, const char *filename) wip->flash_impact_weapon_expl_effect = ParticleManager::get()->addEffect(ParticleEffect( "", //Name ::util::UniformFloatRange(1.f), //Particle num + ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(0.f), //Velocity Inherit false, //Velocity Inherit absolute? @@ -3099,6 +3097,12 @@ int parse_weapon(int subtype, bool replace, const char *filename) false, //Affected by detail -1.f, //Culling range multiplier false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset lifetime, //Lifetime ::util::UniformFloatRange(size * 1.2f, size * 1.9f), //Radius bitmapIndex)); //Bitmap @@ -3158,6 +3162,9 @@ int parse_weapon(int subtype, bool replace, const char *filename) piercingEffect.emplace_back( particle_effect_name, //Name ::util::UniformFloatRange(1.f), //Particle num + ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(0.f), //Velocity Inherit false, //Velocity Inherit absolute? @@ -3172,6 +3179,12 @@ int parse_weapon(int subtype, bool replace, const char *filename) false, //Affected by detail -1.f, //Culling range multiplier false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset ::util::UniformFloatRange(-1.f), //Lifetime ::util::UniformFloatRange(radius * 0.5f, radius * 2.f), //Radius bitmapIndex); @@ -3180,6 +3193,9 @@ int parse_weapon(int subtype, bool replace, const char *filename) piercingEffect.emplace_back( "", //Name, empty as it's a non-findable part of a composite ::util::UniformFloatRange(1.f), //Particle num + ParticleEffect::Duration::ONETIME, //Single Particle Emission + ::util::UniformFloatRange(), //No duration + ::util::UniformFloatRange (-1.f), //Single particle only ParticleEffect::ShapeDirection::ALIGNED, //Particle direction ::util::UniformFloatRange(0.f), //Velocity Inherit false, //Velocity Inherit absolute? @@ -3194,6 +3210,12 @@ int parse_weapon(int subtype, bool replace, const char *filename) false, //Affected by detail -1.f, //Culling range multiplier false, //Disregard Animation Length. Must be true for everything using particle::Anim_bitmap_X + false, //Don't reverse animation + false, //parent local + false, //ignore velocity inherit if parented + false, //position velocity inherit absolute? + std::nullopt, //Local velocity offset + std::nullopt, //Local offset ::util::UniformFloatRange(-1.f), //Lifetime ::util::UniformFloatRange(radius * 0.5f, radius * 2.f), //Radius bitmapIndex); @@ -3530,34 +3552,26 @@ int parse_weapon(int subtype, bool replace, const char *filename) // index for xmt edit, replace and remove support if (optional_string("+Index:")) { stuff_int(&spew_index); - if (spew_index < 0 || spew_index >= MAX_PARTICLE_SPEWERS) { - Warning(LOCATION, "+Index in particle spewer out of range. It must be between 0 and %i. Tag will be ignored.", MAX_PARTICLE_SPEWERS); + if (spew_index < 0) { + Warning(LOCATION, "+Index in particle spewer out of range. It must be positive."); spew_index = -1; } + else if (spew_index >= static_cast(wip->particle_spewers.size())) { + wip->particle_spewers.resize(spew_index + 1, particle::ParticleEffectHandle::invalid()); + } } // check for remove flag if (optional_string("+Remove")) { if (spew_index < 0) { Warning(LOCATION, "+Index not specified or is out of range, can not remove spewer."); } else { // restore defaults - wip->particle_spewers[spew_index].particle_spew_type = PSPEW_NONE; - wip->particle_spewers[spew_index].particle_spew_count = 1; - wip->particle_spewers[spew_index].particle_spew_time = 25; - wip->particle_spewers[spew_index].particle_spew_vel = 0.4f; - wip->particle_spewers[spew_index].particle_spew_radius = 2.0f; - wip->particle_spewers[spew_index].particle_spew_lifetime = 0.15f; - wip->particle_spewers[spew_index].particle_spew_scale = 0.8f; - wip->particle_spewers[spew_index].particle_spew_z_scale = 1.0f; - wip->particle_spewers[spew_index].particle_spew_rotation_rate = 10.0f; - wip->particle_spewers[spew_index].particle_spew_offset = vmd_zero_vector; - wip->particle_spewers[spew_index].particle_spew_velocity = vmd_zero_vector; - generic_anim_init(&wip->particle_spewers[spew_index].particle_spew_anim, NULL); + wip->particle_spewers[spew_index] = particle::ParticleEffectHandle::invalid(); } } else { // were not removing the spewer if (spew_index < 0) { // index us ether not used or is invalid, so figure out where to put things //find a free slot in the pspew info array - for (size_t s = 0; s < MAX_PARTICLE_SPEWERS; s++) { - if (wip->particle_spewers[s].particle_spew_type == PSPEW_NONE) { + for (size_t s = 0; s < wip->particle_spewers.size(); s++) { + if (!wip->particle_spewers[s].isValid()) { spew_index = (int)s; break; } @@ -3565,86 +3579,130 @@ int parse_weapon(int subtype, bool replace, const char *filename) } // no empty spot found, the modder tried to define too many spewers, or screwed up the xmts, or my code sucks if ( spew_index < 0 ) { - Warning(LOCATION, "Too many particle spewers, max number of spewers is %i.", MAX_PARTICLE_SPEWERS); - } else { // we have a valid index, now parse the spewer already + spew_index = static_cast(wip->particle_spewers.size()); + wip->particle_spewers.emplace_back(particle::ParticleEffectHandle::invalid()); + } + + if (optional_string("+Effect:")) { + wip->particle_spewers[spew_index] = particle::util::parseEffect(wip->name); + } + else { // we have a valid index, now parse the spewer already + auto& pspew_buffer = pspew_legacy_parse_data_buffer[weapon_info_get_index(wip)][spew_index]; + if (pspew_buffer.particle_spew_type == PSPEW_NONE) { + //This must be an uninitialized effect, store defaults. + pspew_buffer.particle_spew_count = 1; + pspew_buffer.particle_spew_time = 25; + pspew_buffer.particle_spew_vel = 0.4f; + pspew_buffer.particle_spew_radius = 2.0f; + pspew_buffer.particle_spew_lifetime = 0.15f; + pspew_buffer.particle_spew_scale = 0.8f; + pspew_buffer.particle_spew_z_scale = 1.0f; + pspew_buffer.particle_spew_rotation_rate = 10.0f; + pspew_buffer.particle_spew_offset = vmd_zero_vector; + pspew_buffer.particle_spew_velocity = vmd_zero_vector; + } + if (optional_string("+Type:")) { // added type field for pspew types, 0 is the default for reverse compatability -nuke char temp_pspew_type[NAME_LENGTH]; stuff_string(temp_pspew_type, F_NAME, NAME_LENGTH); if (!stricmp(temp_pspew_type, NOX("DEFAULT"))) { - wip->particle_spewers[spew_index].particle_spew_type = PSPEW_DEFAULT; + pspew_buffer.particle_spew_type = PSPEW_DEFAULT; } else if (!stricmp(temp_pspew_type, NOX("HELIX"))) { - wip->particle_spewers[spew_index].particle_spew_type = PSPEW_HELIX; + pspew_buffer.particle_spew_type = PSPEW_HELIX; } else if (!stricmp(temp_pspew_type, NOX("SPARKLER"))) { // new types can be added here - wip->particle_spewers[spew_index].particle_spew_type = PSPEW_SPARKLER; + pspew_buffer.particle_spew_type = PSPEW_SPARKLER; } else if (!stricmp(temp_pspew_type, NOX("RING"))) { - wip->particle_spewers[spew_index].particle_spew_type = PSPEW_RING; + pspew_buffer.particle_spew_type = PSPEW_RING; } else if (!stricmp(temp_pspew_type, NOX("PLUME"))) { - wip->particle_spewers[spew_index].particle_spew_type = PSPEW_PLUME; + pspew_buffer.particle_spew_type = PSPEW_PLUME; } else { - wip->particle_spewers[spew_index].particle_spew_type = PSPEW_DEFAULT; + pspew_buffer.particle_spew_type = PSPEW_DEFAULT; } // for compatibility with existing tables that don't have a type tag - } else if (wip->particle_spewers[spew_index].particle_spew_type == PSPEW_NONE) { // make sure the omission of type wasn't to edit an existing entry - wip->particle_spewers[spew_index].particle_spew_type = PSPEW_DEFAULT; + } else if (pspew_buffer.particle_spew_type == PSPEW_NONE) { // make sure the omission of type wasn't to edit an existing entry + pspew_buffer.particle_spew_type = PSPEW_DEFAULT; } if (optional_string("+Count:")) { - stuff_int(&wip->particle_spewers[spew_index].particle_spew_count); + stuff_int(&pspew_buffer.particle_spew_count); } if (optional_string("+Time:")) { - stuff_int(&wip->particle_spewers[spew_index].particle_spew_time); + stuff_int(&pspew_buffer.particle_spew_time); } if (optional_string("+Vel:")) { - stuff_float(&wip->particle_spewers[spew_index].particle_spew_vel); + stuff_float(&pspew_buffer.particle_spew_vel); } if (optional_string("+Radius:")) { - stuff_float(&wip->particle_spewers[spew_index].particle_spew_radius); + stuff_float(&pspew_buffer.particle_spew_radius); } if (optional_string("+Life:")) { - stuff_float(&wip->particle_spewers[spew_index].particle_spew_lifetime); + stuff_float(&pspew_buffer.particle_spew_lifetime); } if (optional_string("+Scale:")) { - stuff_float(&wip->particle_spewers[spew_index].particle_spew_scale); + stuff_float(&pspew_buffer.particle_spew_scale); } if (optional_string("+Z Scale:")) { - stuff_float(&wip->particle_spewers[spew_index].particle_spew_z_scale); + stuff_float(&pspew_buffer.particle_spew_z_scale); } if (optional_string("+Rotation Rate:")) { - stuff_float(&wip->particle_spewers[spew_index].particle_spew_rotation_rate); + stuff_float(&pspew_buffer.particle_spew_rotation_rate); } if (optional_string("+Offset:")) { - stuff_vec3d(&wip->particle_spewers[spew_index].particle_spew_offset); + stuff_vec3d(&pspew_buffer.particle_spew_offset); } if (optional_string("+Initial Velocity:")) { - stuff_vec3d(&wip->particle_spewers[spew_index].particle_spew_velocity); + stuff_vec3d(&pspew_buffer.particle_spew_velocity); } if (optional_string("+Bitmap:")) { - stuff_string(fname, F_NAME, MAX_FILENAME_LEN); - generic_anim_init(&wip->particle_spewers[spew_index].particle_spew_anim, fname); + stuff_string(pspew_buffer.particle_spew_anim, F_NAME); } + + //if (wip->particle_spewers[spew_index].isValid()) { + //We had a previous particle effect set here, so we could clear it if we have too much overhead from memory waste. + //But as clearing a particle requires significant memory movement as we erase from a vector, don't for now. + //} + + wip->particle_spewers[spew_index] = convertLegacyPspewBuffer(pspew_buffer, wip); } } } // check to see if the pspew flag was enabled but no pspew tags were given, for compatability with retail tables if (wip->wi_flags[Weapon::Info_Flags::Particle_spew]) { bool nospew = true; - for (size_t s = 0; s < MAX_PARTICLE_SPEWERS; s++) - if (wip->particle_spewers[s].particle_spew_type != PSPEW_NONE) { + for (const auto& spewer : wip->particle_spewers) { + if (spewer.isValid()) { nospew = false; + break; } + } if (nospew) { // set first spewer to default - wip->particle_spewers[0].particle_spew_type = PSPEW_DEFAULT; + if (wip->particle_spewers.empty()) { + wip->particle_spewers.emplace_back(particle::ParticleEffectHandle::invalid()); + } + auto& pspew_buffer = pspew_legacy_parse_data_buffer[weapon_info_get_index(wip)][0]; + pspew_buffer.particle_spew_count = 1; + pspew_buffer.particle_spew_time = 25; + pspew_buffer.particle_spew_vel = 0.4f; + pspew_buffer.particle_spew_radius = 2.0f; + pspew_buffer.particle_spew_lifetime = 0.15f; + pspew_buffer.particle_spew_scale = 0.8f; + pspew_buffer.particle_spew_z_scale = 1.0f; + pspew_buffer.particle_spew_rotation_rate = 10.0f; + pspew_buffer.particle_spew_offset = vmd_zero_vector; + pspew_buffer.particle_spew_velocity = vmd_zero_vector; + pspew_buffer.particle_spew_type = PSPEW_DEFAULT; + wip->particle_spewers[0] = convertLegacyPspewBuffer(pspew_buffer, wip); } } @@ -4106,6 +4164,8 @@ void parse_weaponstbl(const char *filename) required_string("#End"); } + pspew_legacy_parse_data_buffer.clear(); + // Read in a list of weapon_info indicies that are an ordering of the player weapon precedence. // This list is used to select an alternate weapon when a particular weapon is not available // during weapon selection. @@ -4377,12 +4437,6 @@ void weapon_release_bitmaps() } if (wip->wi_flags[Weapon::Info_Flags::Beam]) { - // particle animation - if (wip->b_info.beam_particle_ani.first_frame >= 0) { - bm_release(wip->b_info.beam_particle_ani.first_frame); - wip->b_info.beam_particle_ani.first_frame = -1; - } - // muzzle glow if (wip->b_info.beam_glow.first_frame >= 0) { bm_release(wip->b_info.beam_glow.first_frame); @@ -4407,17 +4461,6 @@ void weapon_release_bitmaps() } } - if (wip->wi_flags[Weapon::Info_Flags::Particle_spew]) { // tweaked for multiple particle spews -nuke - for (size_t s = 0; s < MAX_PARTICLE_SPEWERS; s++) { // just bitmaps that got loaded - if (wip->particle_spewers[s].particle_spew_type != PSPEW_NONE){ - if (wip->particle_spewers[s].particle_spew_anim.first_frame >= 0) { - bm_release(wip->particle_spewers[s].particle_spew_anim.first_frame); - wip->particle_spewers[s].particle_spew_anim.first_frame = -1; - } - } - } - } - if (wip->thruster_flame.first_frame >= 0) { bm_release(wip->thruster_flame.first_frame); wip->thruster_flame.first_frame = -1; @@ -4507,10 +4550,6 @@ void weapon_load_bitmaps(int weapon_index) } if (wip->wi_flags[Weapon::Info_Flags::Beam]) { - // particle animation - if ( (wip->b_info.beam_particle_ani.first_frame < 0) && strlen(wip->b_info.beam_particle_ani.filename) ) - generic_anim_load(&wip->b_info.beam_particle_ani); - // muzzle glow if ( (wip->b_info.beam_glow.first_frame < 0) && strlen(wip->b_info.beam_glow.filename) ) { if ( generic_anim_load(&wip->b_info.beam_glow) ) { @@ -4556,30 +4595,6 @@ void weapon_load_bitmaps(int weapon_index) } } - //WMC - Don't try to load an anim if no anim is specified, Mmkay? - if (wip->wi_flags[Weapon::Info_Flags::Particle_spew]) { - for (size_t s = 0; s < MAX_PARTICLE_SPEWERS; s++) { // looperfied for multiple pspewers -nuke - if (wip->particle_spewers[s].particle_spew_type != PSPEW_NONE){ - - if ((wip->particle_spewers[s].particle_spew_anim.first_frame < 0) - && (wip->particle_spewers[s].particle_spew_anim.filename[0] != '\0') ) { - - wip->particle_spewers[s].particle_spew_anim.first_frame = bm_load(wip->particle_spewers[s].particle_spew_anim.filename); - - if (wip->particle_spewers[s].particle_spew_anim.first_frame >= 0) { - wip->particle_spewers[s].particle_spew_anim.num_frames = 1; - wip->particle_spewers[s].particle_spew_anim.total_time = 1; - } - // fall back to an animated type - else if ( generic_anim_load(&wip->particle_spewers[s].particle_spew_anim) ) { - mprintf(("Could not find a usable particle spew bitmap for '%s'!\n", wip->name)); - Warning(LOCATION, "Could not find a usable particle spew bitmap (%s) for weapon '%s'!\n", wip->particle_spewers[s].particle_spew_anim.filename, wip->name); - } - } - } - } - } - // load alternate thruster textures if (strlen(wip->thruster_flame.filename)) { generic_anim_load(&wip->thruster_flame); @@ -4804,33 +4819,12 @@ void weapon_do_post_parse() translate_spawn_types(); } -void weapon_expl_info_init() -{ - int i; - - parse_weapon_expl_tbl("weapon_expl.tbl"); - - // check for, and load, modular tables - parse_modular_table(NOX("*-wxp.tbm"), parse_weapon_expl_tbl); - - // we've got our list so pass it off for final checking and loading - for (i = 0; i < (int)LOD_checker.size(); i++) { - Weapon_explosions.Load( LOD_checker[i].filename, LOD_checker[i].num_lods ); - } - - // done - LOD_checker.clear(); -} - /** * This will get called once at game startup */ void weapon_init() { if ( !Weapons_inited ) { - //Init weapon explosion info - weapon_expl_info_init(); - Num_spawn_types = 0; // parse weapons.tbl @@ -6215,10 +6209,6 @@ void weapon_process_post(object * obj, float frame_time) weapon_maybe_play_flyby_sound(obj, wp); #endif - if (wip->wi_flags[Weapon::Info_Flags::Particle_spew] && wp->lssm_stage != 3) { - weapon_maybe_spew_particle(obj); - } - // If this flag is true this is evaluated in weapon_process_pre() if (!Framerate_independent_turning) { weapon_do_homing_behavior(obj, frame_time); @@ -6779,16 +6769,6 @@ int weapon_create( const vec3d *pos, const matrix *porient, int weapon_type, int swarm_create(objp, wp->swarm_info_ptr.get()); } - // if this is a particle spewing weapon, setup some stuff - if (wip->wi_flags[Weapon::Info_Flags::Particle_spew]) { - for (size_t s = 0; s < MAX_PARTICLE_SPEWERS; s++) { // allow for multiple time values - if (wip->particle_spewers[s].particle_spew_type != PSPEW_NONE) { - wp->particle_spew_time[s] = -1; - wp->particle_spew_rand = frand_range(0, PI2); // per weapon randomness - } - } - } - // assign the network signature. The starting sig is sent to all clients, so this call should // result in the same net signature numbers getting assigned to every player in the game if ( Game_mode & GM_MULTIPLAYER ) { @@ -7063,6 +7043,19 @@ int weapon_create( const vec3d *pos, const matrix *porient, int weapon_type, int obj_snd_assign(objnum, wip->ambient_snd, &vmd_zero_vector , OS_MAIN); } + // if this is a particle spewing weapon, setup some stuff + if (wip->wi_flags[Weapon::Info_Flags::Particle_spew]) { + for (const auto& effect : wip->particle_spewers) { + if(!effect.isValid()) + continue; + + auto source = particle::ParticleManager::get()->createSource(effect); + auto host = std::make_unique(objp, vmd_zero_vector); + source->setHost(std::move(host)); + source->finishCreation(); + } + } + //Only try and play animations on POF Weapons if (wip->render_type == WRT_POF && wp->model_instance_num > -1) { wip->animations.getAll(model_get_instance(wp->model_instance_num), animation::ModelAnimationTriggerType::OnSpawn).start(animation::ModelAnimationDirection::FWD); @@ -8235,23 +8228,8 @@ void weapons_page_in() // muzzle glow bm_page_in_texture(wip->b_info.beam_glow.first_frame); - - // particle ani - bm_page_in_texture(wip->b_info.beam_particle_ani.first_frame); - } - - if (wip->wi_flags[Weapon::Info_Flags::Particle_spew]) { - for (size_t s = 0; s < MAX_PARTICLE_SPEWERS; s++) { // looped, multi particle spew -nuke - if (wip->particle_spewers[s].particle_spew_type != PSPEW_NONE) { - bm_page_in_texture(wip->particle_spewers[s].particle_spew_anim.first_frame); - } - } } - // muzzle flashes - if (wip->muzzle_flash >= 0) - mflash_mark_as_used(wip->muzzle_flash); - bm_page_in_texture(wip->thruster_flame.first_frame); bm_page_in_texture(wip->thruster_glow.first_frame); @@ -8276,9 +8254,6 @@ void weapons_page_in_cheats() Assert( used_weapons != NULL ); - // force a page in of all muzzle flashes - mflash_page_in(true); - // page in models for all weapon types that aren't already loaded for (i = 0; i < weapon_info_size(); i++) { // skip over anything that's already loaded @@ -8426,23 +8401,8 @@ bool weapon_page_in(int weapon_type) // muzzle glow bm_page_in_texture(wip->b_info.beam_glow.first_frame); - - // particle ani - bm_page_in_texture(wip->b_info.beam_particle_ani.first_frame); - } - - if (wip->wi_flags[Weapon::Info_Flags::Particle_spew]) { - for (size_t s = 0; s < MAX_PARTICLE_SPEWERS; s++) { // looped, multi particle spew -nuke - if (wip->particle_spewers[s].particle_spew_type != PSPEW_NONE) { - bm_page_in_texture(wip->particle_spewers[s].particle_spew_anim.first_frame); - } - } } - // muzzle flashes - if (wip->muzzle_flash >= 0) - mflash_mark_as_used(wip->muzzle_flash); - bm_page_in_texture(wip->thruster_flame.first_frame); bm_page_in_texture(wip->thruster_glow.first_frame); @@ -8510,443 +8470,6 @@ void weapon_get_laser_color(color *c, object *objp) gr_init_color( c, r, g, b ); } -// default weapon particle spew data - -int Weapon_particle_spew_count = 1; -int Weapon_particle_spew_time = 25; -float Weapon_particle_spew_vel = 0.4f; -float Weapon_particle_spew_radius = 2.0f; -float Weapon_particle_spew_lifetime = 0.15f; -float Weapon_particle_spew_scale = 0.8f; - -/** - * For weapons flagged as particle spewers, spew particles. wheee - */ -void weapon_maybe_spew_particle(object *obj) -{ - weapon *wp; - weapon_info *wip; - int idx; - - // check some stuff - Assert(obj->type == OBJ_WEAPON); - Assert(obj->instance >= 0); - Assert(Weapons[obj->instance].weapon_info_index >= 0); - Assert(Weapon_info[Weapons[obj->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Particle_spew]); - - wp = &Weapons[obj->instance]; - wip = &Weapon_info[wp->weapon_info_index]; - vec3d spawn_pos, spawn_vel, output_pos, output_vel, input_pos, input_vel; - - for (int psi = 0; psi < MAX_PARTICLE_SPEWERS; psi++) { // iterate through spewers -nuke - if (wip->particle_spewers[psi].particle_spew_type != PSPEW_NONE) { - // if the weapon's particle timestamp has elapsed - if ((wp->particle_spew_time[psi] == -1) || timestamp_elapsed(wp->particle_spew_time[psi])) { - // reset the timestamp - wp->particle_spew_time[psi] = timestamp(wip->particle_spewers[0].particle_spew_time); - - // turn normals and origins to world space if we need to - if (!vm_vec_same(&wip->particle_spewers[psi].particle_spew_offset, &vmd_zero_vector)) { // don't xform unused vectors - vm_vec_unrotate(&spawn_pos, &wip->particle_spewers[psi].particle_spew_offset, &obj->orient); - } else { - spawn_pos = vmd_zero_vector; - } - - if (!vm_vec_same(&wip->particle_spewers[psi].particle_spew_velocity, &vmd_zero_vector)) { - vm_vec_unrotate(&spawn_vel, &wip->particle_spewers[psi].particle_spew_velocity, &obj->orient); - } else { - spawn_vel = vmd_zero_vector; - } - - // spew some particles - if (wip->particle_spewers[psi].particle_spew_type == PSPEW_DEFAULT) // default pspew type - { // do the default pspew - vec3d direct, direct_temp, particle_pos; - vec3d null_vec = ZERO_VECTOR; - vec3d vel; - float ang; - - for (idx = 0; idx < wip->particle_spewers[psi].particle_spew_count; idx++) { - // get the backward vector of the weapon - direct = obj->orient.vec.fvec; - vm_vec_negate(&direct); - - // randomly perturb x, y and z - - // uvec - ang = frand_range(-PI_2,PI_2); // fl_radian(frand_range(-90.0f, 90.0f)); -optimized by nuke - vm_rot_point_around_line(&direct_temp, &direct, ang, &null_vec, &obj->orient.vec.fvec); - direct = direct_temp; - vm_vec_scale(&direct, wip->particle_spewers[psi].particle_spew_scale); - - // rvec - ang = frand_range(-PI_2,PI_2); // fl_radian(frand_range(-90.0f, 90.0f)); -optimized by nuke - vm_rot_point_around_line(&direct_temp, &direct, ang, &null_vec, &obj->orient.vec.rvec); - direct = direct_temp; - vm_vec_scale(&direct, wip->particle_spewers[psi].particle_spew_scale); - - // fvec - ang = frand_range(-PI_2,PI_2); // fl_radian(frand_range(-90.0f, 90.0f)); -optimized by nuke - vm_rot_point_around_line(&direct_temp, &direct, ang, &null_vec, &obj->orient.vec.uvec); - direct = direct_temp; - vm_vec_scale(&direct, wip->particle_spewers[psi].particle_spew_scale); - - // get a velocity vector of some percentage of the weapon's velocity - vel = obj->phys_info.vel; - vm_vec_scale(&vel, wip->particle_spewers[psi].particle_spew_vel); - - // maybe add in offset and initial velocity - if (!vm_vec_same(&spawn_vel, &vmd_zero_vector)) { // add in particle velocity if its available - vm_vec_add2(&vel, &spawn_vel); - } - if (!vm_vec_same(&spawn_pos, &vmd_zero_vector)) { // add offset if available - vm_vec_add2(&direct, &spawn_pos); - } - - if (wip->wi_flags[Weapon::Info_Flags::Corkscrew]) { - vm_vec_add(&particle_pos, &obj->last_pos, &direct); - } else { - vm_vec_add(&particle_pos, &obj->pos, &direct); - } - - // emit the particle - if (wip->particle_spewers[psi].particle_spew_anim.first_frame < 0) { - particle::particle_info pi; - pi.bitmap = particle::Anim_bitmap_id_smoke; - pi.nframes = particle::Anim_num_frames_smoke; - pi.pos = particle_pos; - pi.vel = vel; - pi.lifetime = wip->particle_spewers[psi].particle_spew_lifetime; - pi.rad = wip->particle_spewers[psi].particle_spew_radius; - - particle::create(&pi); - } else { - particle::create(&particle_pos, - &vel, - wip->particle_spewers[psi].particle_spew_lifetime, - wip->particle_spewers[psi].particle_spew_radius, - wip->particle_spewers[psi].particle_spew_anim.first_frame); - } - } - } else if (wip->particle_spewers[psi].particle_spew_type == PSPEW_HELIX) { // helix - float segment_length = wip->max_speed * flFrametime; // determine how long the segment is - float segment_angular_length = PI2 * wip->particle_spewers[psi].particle_spew_rotation_rate * flFrametime; // determine how much the segment rotates - float rotation_value = (wp->lifeleft * PI2 * wip->particle_spewers[psi].particle_spew_rotation_rate) + wp->particle_spew_rand; // calculate a rotational start point based on remaining life - float inc = 1.0f / wip->particle_spewers[psi].particle_spew_count; // determine our incriment - float particle_rot; - vec3d input_pos_l = ZERO_VECTOR; - - for (float is = 0; is < 1; is += inc ) { // use iterator as a scaler - particle_rot = rotation_value + (segment_angular_length * is); // find what point of the rotation were at - input_vel.xyz.x = sinf(particle_rot) * wip->particle_spewers[psi].particle_spew_scale; // determine x/y velocity based on scale and rotation - input_vel.xyz.y = cosf(particle_rot) * wip->particle_spewers[psi].particle_spew_scale; - input_vel.xyz.z = wip->max_speed * wip->particle_spewers[psi].particle_spew_vel; // velocity inheritance - vm_vec_unrotate(&output_vel, &input_vel, &obj->orient); // orient velocity to weapon - input_pos_l.xyz.x = input_vel.xyz.x * flFrametime * (1.0f - is); // interpolate particle motion - input_pos_l.xyz.y = input_vel.xyz.y * flFrametime * (1.0f - is); - input_pos_l.xyz.z = segment_length * is; // position particle correctly on the z axis - vm_vec_unrotate(&input_pos, &input_pos_l, &obj->orient); // orient to weapon - vm_vec_sub(&output_pos, &obj->pos, &input_pos); // translate to world space - - //maybe add in offset and initial velocity - if (!vm_vec_same(&spawn_vel, &vmd_zero_vector)) { // add particle velocity if needed - vm_vec_add2(&output_vel, &spawn_vel); - } - if (!vm_vec_same(&spawn_pos, &vmd_zero_vector)) { // add offset if needed - vm_vec_add2(&output_pos, &spawn_pos); - } - - //emit particles - if (wip->particle_spewers[psi].particle_spew_anim.first_frame < 0) { - particle::particle_info pi; - pi.bitmap = particle::Anim_bitmap_id_smoke; - pi.nframes = particle::Anim_num_frames_smoke; - pi.pos = output_pos; - pi.vel = output_vel; - pi.lifetime = wip->particle_spewers[psi].particle_spew_lifetime; - pi.rad = wip->particle_spewers[psi].particle_spew_radius; - - particle::create(&pi); - } else { - particle::create(&output_pos, - &output_vel, - wip->particle_spewers[psi].particle_spew_lifetime, - wip->particle_spewers[psi].particle_spew_radius, - wip->particle_spewers[psi].particle_spew_anim.first_frame); - } - } - } else if (wip->particle_spewers[psi].particle_spew_type == PSPEW_SPARKLER) { // sparkler - vec3d temp_vel; - output_vel = obj->phys_info.vel; - vm_vec_scale(&output_vel, wip->particle_spewers[psi].particle_spew_vel); - - for (idx = 0; idx < wip->particle_spewers[psi].particle_spew_count; idx++) { - // create a random unit vector and scale it - vm_vec_rand_vec_quick(&input_vel); - vm_vec_scale(&input_vel, wip->particle_spewers[psi].particle_spew_scale); - - if (wip->particle_spewers[psi].particle_spew_z_scale != 1.0f) { // don't do the extra math for spherical effect - temp_vel = input_vel; - temp_vel.xyz.z *= wip->particle_spewers[psi].particle_spew_z_scale; // for an ovoid particle effect to better combine with laser effects - vm_vec_unrotate(&input_vel, &temp_vel, &obj->orient); // so it has to be rotated - } - - vm_vec_add2(&output_vel, &input_vel); // add to weapon velocity - output_pos = obj->pos; - - // maybe add in offset and initial velocity - if (!vm_vec_same(&spawn_vel, &vmd_zero_vector)) { // add particle velocity if needed - vm_vec_add2(&output_vel, &spawn_vel); - } - if (!vm_vec_same(&spawn_pos, &vmd_zero_vector)) { // add offset if needed - vm_vec_add2(&output_pos, &spawn_pos); - } - - // emit particles - if (wip->particle_spewers[psi].particle_spew_anim.first_frame < 0) { - particle::particle_info pi; - pi.bitmap = particle::Anim_bitmap_id_smoke; - pi.nframes = particle::Anim_num_frames_smoke; - pi.pos = output_pos; - pi.vel = output_vel; - pi.lifetime = wip->particle_spewers[psi].particle_spew_lifetime; - pi.rad = wip->particle_spewers[psi].particle_spew_radius; - - particle::create(&pi); - } else { - particle::create(&output_pos, - &output_vel, - wip->particle_spewers[psi].particle_spew_lifetime, - wip->particle_spewers[psi].particle_spew_radius, - wip->particle_spewers[psi].particle_spew_anim.first_frame); - } - } - } else if (wip->particle_spewers[psi].particle_spew_type == PSPEW_RING) { - float inc = PI2 / wip->particle_spewers[psi].particle_spew_count; - - for (float ir = 0; ir < PI2; ir += inc) { // use iterator for rotation - input_vel.xyz.x = sinf(ir) * wip->particle_spewers[psi].particle_spew_scale; // generate velocity from rotation data - input_vel.xyz.y = cosf(ir) * wip->particle_spewers[psi].particle_spew_scale; - input_vel.xyz.z = obj->phys_info.fspeed * wip->particle_spewers[psi].particle_spew_vel; - vm_vec_unrotate(&output_vel, &input_vel, &obj->orient); // rotate it to model - - output_pos = obj->pos; - - // maybe add in offset amd iitial velocity - if (!vm_vec_same(&spawn_vel, &vmd_zero_vector)) { // add particle velocity if needed - vm_vec_add2(&output_vel, &spawn_vel); - } - if (!vm_vec_same(&spawn_pos, &vmd_zero_vector)) { // add offset if needed - vm_vec_add2(&output_pos, &spawn_pos); - } - - // emit particles - if (wip->particle_spewers[psi].particle_spew_anim.first_frame < 0) { - particle::particle_info pi; - pi.bitmap = particle::Anim_bitmap_id_smoke; - pi.nframes = particle::Anim_num_frames_smoke; - pi.pos = output_pos; - pi.vel = output_vel; - pi.lifetime = wip->particle_spewers[psi].particle_spew_lifetime; - pi.rad = wip->particle_spewers[psi].particle_spew_radius; - } else { - particle::create(&output_pos, - &output_vel, - wip->particle_spewers[psi].particle_spew_lifetime, - wip->particle_spewers[psi].particle_spew_radius, - wip->particle_spewers[psi].particle_spew_anim.first_frame); - } - } - } else if (wip->particle_spewers[psi].particle_spew_type == PSPEW_PLUME) { - float ang_rand, len_rand, sin_ang, cos_ang; - vec3d input_pos_l = ZERO_VECTOR; - - for (int i = 0; i < wip->particle_spewers[psi].particle_spew_count; i++) { - // use polar coordinates to ensure a disk shaped spew plane - ang_rand = frand_range(-PI,PI); - len_rand = frand() * wip->particle_spewers[psi].particle_spew_scale; - sin_ang = sinf(ang_rand); - cos_ang = cosf(ang_rand); - // compute velocity - input_vel.xyz.x = wip->particle_spewers[psi].particle_spew_z_scale * -sin_ang; - input_vel.xyz.y = wip->particle_spewers[psi].particle_spew_z_scale * -cos_ang; - input_vel.xyz.z = obj->phys_info.fspeed * wip->particle_spewers[psi].particle_spew_vel; - vm_vec_unrotate(&output_vel, &input_vel, &obj->orient); // rotate it to model - // place particle on a disk prependicular to the weapon normal and rotate to model space - input_pos_l.xyz.x = sin_ang * len_rand; - input_pos_l.xyz.y = cos_ang * len_rand; - vm_vec_unrotate(&input_pos, &input_pos_l, &obj->orient); // rotate to world - vm_vec_sub(&output_pos, &obj->pos, &input_pos); // translate to world - - // maybe add in offset amd iitial velocity - if (!vm_vec_same(&spawn_vel, &vmd_zero_vector)) { // add particle velocity if needed - vm_vec_add2(&output_vel, &spawn_vel); - } - if (!vm_vec_same(&spawn_pos, &vmd_zero_vector)) { // add offset if needed - vm_vec_add2(&output_pos, &spawn_pos); - } - - //emit particles - if (wip->particle_spewers[psi].particle_spew_anim.first_frame < 0) { - particle::particle_info pi; - pi.bitmap = particle::Anim_bitmap_id_smoke; - pi.nframes = particle::Anim_num_frames_smoke; - pi.pos = output_pos; - pi.vel = output_vel; - pi.lifetime = wip->particle_spewers[psi].particle_spew_lifetime; - pi.rad = wip->particle_spewers[psi].particle_spew_radius; - - particle::create(&pi); - } else { - particle::create(&output_pos, - &output_vel, - wip->particle_spewers[psi].particle_spew_lifetime, - wip->particle_spewers[psi].particle_spew_radius, - wip->particle_spewers[psi].particle_spew_anim.first_frame); - } - } - } - } - } - } -} - -/** - * Debug console functionality - */ -void dcf_pspew(); -DCF(pspew_count, "Number of particles spewed at a time") -{ - if (dc_optional_string_either("help", "--help")) { - dcf_pspew(); - return; - } - - if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) { - dc_printf("Partical count is %i\n", Weapon_particle_spew_count); - return; - } - - dc_stuff_int(&Weapon_particle_spew_count); - - dc_printf("Partical count set to %i\n", Weapon_particle_spew_count); -} - -DCF(pspew_time, "Time between particle spews") -{ - if (dc_optional_string_either("help", "--help")) { - dcf_pspew(); - return; - } - - if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) { - dc_printf("Particle spawn period is %i\n", Weapon_particle_spew_time); - return; - } - - dc_stuff_int(&Weapon_particle_spew_time); - - dc_printf("Particle spawn period set to %i\n", Weapon_particle_spew_time); -} - -DCF(pspew_vel, "Relative velocity of particles (0.0 - 1.0)") -{ - if (dc_optional_string_either("help", "--help")) { - dcf_pspew(); - return; - } - - if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) { - dc_printf("Particle relative velocity is %f\n", Weapon_particle_spew_vel); - return; - } - - dc_stuff_float(&Weapon_particle_spew_vel); - - dc_printf("Particle relative velocity set to %f\n", Weapon_particle_spew_vel); -} - -DCF(pspew_size, "Size of spewed particles") -{ - if (dc_optional_string_either("help", "--help")) { - dcf_pspew(); - return; - } - - if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) { - dc_printf("Particle size is %f\n", Weapon_particle_spew_radius); - return; - } - - dc_stuff_float(&Weapon_particle_spew_radius); - - dc_printf("Particle size set to %f\n", Weapon_particle_spew_radius); -} - -DCF(pspew_life, "Lifetime of spewed particles") -{ - if (dc_optional_string_either("help", "--help")) { - dcf_pspew(); - return; - } - - if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) { - dc_printf("Particle lifetime is %f\n", Weapon_particle_spew_lifetime); - return; - } - - dc_stuff_float(&Weapon_particle_spew_lifetime); - - dc_printf("Particle lifetime set to %f\n", Weapon_particle_spew_lifetime); -} - -DCF(pspew_scale, "How far away particles are from the weapon path") -{ - if (dc_optional_string_either("help", "--help")) { - dcf_pspew(); - return; - } - - if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) { - dc_printf("Particle scale is %f\n", Weapon_particle_spew_scale); - } - - dc_stuff_float(&Weapon_particle_spew_scale); - - dc_printf("Particle scale set to %f\n", Weapon_particle_spew_scale); -} - -// Help and Status provider -DCF(pspew, "Particle spew help and status provider") -{ - if (dc_optional_string_either("status", "--status") || dc_optional_string_either("?", "--?")) { - dc_printf("Particle spew settings\n\n"); - - dc_printf(" Count (pspew_count) : %d\n", Weapon_particle_spew_count); - dc_printf(" Time (pspew_time) : %d\n", Weapon_particle_spew_time); - dc_printf(" Velocity (pspew_vel) : %f\n", Weapon_particle_spew_vel); - dc_printf(" Size (pspew_size) : %f\n", Weapon_particle_spew_radius); - dc_printf(" Lifetime (pspew_life) : %f\n", Weapon_particle_spew_lifetime); - dc_printf(" Scale (psnew_scale) : %f\n", Weapon_particle_spew_scale); - return; - } - - dc_printf("Available particlar spew commands:\n"); - dc_printf("pspew_count : %s\n", dcmd_pspew_count.help); - dc_printf("pspew_time : %s\n", dcmd_pspew_time.help); - dc_printf("pspew_vel : %s\n", dcmd_pspew_vel.help); - dc_printf("pspew_size : %s\n", dcmd_pspew_size.help); - dc_printf("pspew_life : %s\n", dcmd_pspew_life.help); - dc_printf("pspew_scale : %s\n\n", dcmd_pspew_scale.help); - - dc_printf("To view status of all pspew settings, type in 'pspew --status'.\n"); - dc_printf("Passing '--status' as an argument to any of the individual spew commands will show the status of that variable only.\n\n"); - - dc_printf("These commands adjust the various properties of the particle spew system, which is used by weapons when they are fired, are in-flight, and die (either by impact or by end of life time.\n"); - dc_printf("Generally, a large particle count with small size and scale will result in a nice dense particle spew.\n"); - dc_printf("Be advised, this effect is applied to _ALL_ weapons, and as such may drastically reduce framerates on lower powered platforms.\n"); -} - /** * Return a scale factor for damage which should be applied for 2 collisions */ @@ -9075,14 +8598,18 @@ void weapon_unpause_sounds() beam_unpause_sounds(); } -void shield_impact_explosion(const vec3d *hitpos, const object *objp, float radius, int idx) { - int expl_ani_handle = Weapon_explosions.GetAnim(idx, hitpos, radius); - particle::create(hitpos, - &vmd_zero_vector, - 0.0f, - radius, - expl_ani_handle, - objp); +void shield_impact_explosion(const vec3d& hitpos, const vec3d& hitdir, const object *objp, const object *weapon_objp, float radius, particle::ParticleEffectHandle handle) { + matrix localorient = objp->orient * weapon_objp->orient; + vec3d hitdir_global; + vm_vec_unrotate(&hitdir_global, &hitdir, &objp->orient); + + auto particleSource = particle::ParticleManager::get()->createSource(handle); + particleSource->setHost(make_unique(objp, hitpos, localorient)); + particleSource->setNormal(hitdir_global); + particleSource->setTriggerRadius(radius); + particleSource->setTriggerVelocity(vm_vec_mag_quick(&weapon_objp->phys_info.vel)); + + particleSource->finishCreation(); } // renders another laser bitmap on top of the regular bitmap based on the angle of the camera to the front of the laser @@ -9855,8 +9382,6 @@ void weapon_info::reset() this->tag_time = -1.0f; this->tag_level = -1; - this->muzzle_flash = -1; - this->field_of_fire = 0.0f; this->fof_spread_rate = 0.0f; this->fof_reset_rate = 0.0f; @@ -9896,9 +9421,6 @@ void weapon_info::reset() this->b_info.beam_warmup = -1; this->b_info.beam_warmdown = -1; this->b_info.beam_muzzle_radius = 0.0f; - this->b_info.beam_particle_count = -1; - this->b_info.beam_particle_radius = 0.0f; - this->b_info.beam_particle_angle = 0.0f; this->b_info.beam_loop_sound = gamesnd_id(); this->b_info.beam_warmup_sound = gamesnd_id(); this->b_info.beam_warmdown_sound = gamesnd_id(); @@ -9938,7 +9460,8 @@ void weapon_info::reset() this->b_info.t5info.burst_rot_axis = Type5BeamRotAxis::UNSPECIFIED; generic_anim_init(&this->b_info.beam_glow, NULL); - generic_anim_init(&this->b_info.beam_particle_ani, NULL); + + this->b_info.beam_muzzle_effect = particle::ParticleEffectHandle::invalid(); for (i = 0; i < (int)Iff_info.size(); i++) for (j = 0; j < NUM_SKILL_LEVELS; j++) @@ -9959,20 +9482,7 @@ void weapon_info::reset() bsip->translation = 0.0f; } - for (size_t s = 0; s < MAX_PARTICLE_SPEWERS; s++) { // default values for everything -nuke - this->particle_spewers[s].particle_spew_type = PSPEW_NONE; // added by nuke - this->particle_spewers[s].particle_spew_count = 1; - this->particle_spewers[s].particle_spew_time = 25; - this->particle_spewers[s].particle_spew_vel = 0.4f; - this->particle_spewers[s].particle_spew_radius = 2.0f; - this->particle_spewers[s].particle_spew_lifetime = 0.15f; - this->particle_spewers[s].particle_spew_scale = 0.8f; - this->particle_spewers[s].particle_spew_z_scale = 1.0f; // added by nuke - this->particle_spewers[s].particle_spew_rotation_rate = 10.0f; - this->particle_spewers[s].particle_spew_offset = vmd_zero_vector; - this->particle_spewers[s].particle_spew_velocity = vmd_zero_vector; - generic_anim_init(&this->particle_spewers[s].particle_spew_anim, NULL); - } + this->particle_spewers.clear(); // added by nuke this->cm_aspect_effectiveness = 1.0f; this->cm_heat_effectiveness = 1.0f; diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index a04782ae390..cb11df5ec2d 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -941,7 +941,6 @@ void game_level_close() volumetrics_level_close(); ct_level_close(); beam_level_close(); - mflash_level_close(); mission_brief_common_reset(); // close out parsed briefing/mission stuff cam_close(); subtitles_close(); @@ -1094,7 +1093,6 @@ void game_level_init() ct_level_init(); // initialize ships contrails, etc awacs_level_init(); // initialize AWACS beam_level_init(); // initialize beam weapons - mflash_level_init(); ssm_level_init(); supernova_level_init(); cam_init(); diff --git a/freespace2/levelpaging.cpp b/freespace2/levelpaging.cpp index 6582b44ce18..d82efbcf026 100644 --- a/freespace2/levelpaging.cpp +++ b/freespace2/levelpaging.cpp @@ -28,7 +28,6 @@ extern void asteroid_page_in(); extern void neb2_page_in(); extern void message_pagein_mission_messages(); extern void model_page_in_stop(); -extern void mflash_page_in(bool); namespace particle { @@ -57,7 +56,6 @@ void level_page_in() shield_hit_page_in(); asteroid_page_in(); neb2_page_in(); - mflash_page_in(false); // just so long as it happens after weapons_page_in() // preload mission messages if NOT running low-memory (greater than 48MB) if (game_using_low_mem() == false) { From 62c8260d263152a3d98e5e88843c5956935e3269 Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Wed, 9 Jul 2025 09:08:53 -0700 Subject: [PATCH 232/466] add flag (#6815) --- code/ai/aiturret.cpp | 6 ++++-- code/model/model_flags.h | 1 + code/ship/ship.cpp | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/code/ai/aiturret.cpp b/code/ai/aiturret.cpp index ae259cf1f09..f949ec56e71 100644 --- a/code/ai/aiturret.cpp +++ b/code/ai/aiturret.cpp @@ -1559,7 +1559,9 @@ void turret_set_next_fire_timestamp(int weapon_num, const weapon_info *wip, ship float burst_shots_mult = wip->weapon_launch_curves.get_output(weapon_info::WeaponLaunchCurveOutputs::BURST_SHOTS_MULT, launch_curve_data); int burst_shots = MAX(fl2i(i2fl(base_burst_shots) * burst_shots_mult) - 1, 0); - if (burst_shots > turret->weapons.burst_counter[weapon_num]) { + bool burst = burst_shots > turret->weapons.burst_counter[weapon_num]; + + if (burst) { wait *= wip->burst_delay; wait *= wip->weapon_launch_curves.get_output(weapon_info::WeaponLaunchCurveOutputs::BURST_DELAY_MULT, launch_curve_data); turret->weapons.burst_counter[weapon_num]++; @@ -1659,7 +1661,7 @@ void turret_set_next_fire_timestamp(int weapon_num, const weapon_info *wip, ship wait *= frand_range(0.9f, 1.1f); } - if(turret->rof_scaler != 1.0f) + if(turret->rof_scaler != 1.0f && !(burst && turret->system_info->flags[Model::Subsystem_Flags::Burst_ignores_RoF_Mult])) wait /= get_adjusted_turret_rof(turret); (*fs_dest) = timestamp((int)wait); diff --git a/code/model/model_flags.h b/code/model/model_flags.h index c61c909eabd..2559e73bd96 100644 --- a/code/model/model_flags.h +++ b/code/model/model_flags.h @@ -72,6 +72,7 @@ namespace Model { Hide_turret_from_loadout_stats, // Turret is not accounted for in auto-generated "Turrets" line in the ship loadout window --wookieejedi Turret_distant_firepoint, //Turret barrel is very long and should be taken into account when aiming -- Kiloku Override_submodel_impact, // if a weapon impacted a submodel, but this subsystem is within range, the subsystem takes priority -- Goober5000 + Burst_ignores_RoF_Mult, // The turret's fire rate multiplier won't affect burst delay. NUM_VALUES }; diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index c0b3b01366e..238626cbba2 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -357,6 +357,7 @@ flag_def_list_new Subsystem_flags[] = { { "hide turret from loadout stats", Model::Subsystem_Flags::Hide_turret_from_loadout_stats, true, false }, { "turret has distant firepoint", Model::Subsystem_Flags::Turret_distant_firepoint, true, false }, { "override submodel impact", Model::Subsystem_Flags::Override_submodel_impact, true, false }, + { "burst ignores rof mult", Model::Subsystem_Flags::Burst_ignores_RoF_Mult, true, false }, }; const size_t Num_subsystem_flags = sizeof(Subsystem_flags)/sizeof(flag_def_list_new); From a52f31f748206271ed8913b24fbaca9beb7010e1 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Thu, 10 Jul 2025 00:51:04 +0200 Subject: [PATCH 233/466] Instanced Decals (#6813) * prepare instancing * Render single decal with instance-like buffer * encode per-instance data in the instance matrix * Actually render decals instanced * Update todo commit * actually add stubs * Gate decal preparation behind GPU capability flag * Fix MSVC warnigns * remove explicit layout * Fix MSVC warnigns 2 * Clang Tidy --- code/decals/decals.cpp | 18 +++- code/def_files/data/effects/decal-f.sdr | 23 ++--- code/def_files/data/effects/decal-v.sdr | 32 ++++--- code/graphics/2d.cpp | 14 +-- code/graphics/2d.h | 20 +++-- code/graphics/decal_draw_list.cpp | 115 +++++++----------------- code/graphics/decal_draw_list.h | 27 +++--- code/graphics/grstub.cpp | 22 +++++ code/graphics/opengl/gropengl.cpp | 3 + code/graphics/opengl/gropengldraw.cpp | 10 ++- code/graphics/opengl/gropengldraw.h | 4 +- code/graphics/opengl/gropenglshader.cpp | 3 +- code/graphics/opengl/gropenglshader.h | 1 + code/graphics/opengl/gropengltnl.cpp | 52 +++++++++-- code/graphics/opengl/gropengltnl.h | 1 + code/graphics/util/uniform_structs.h | 13 +-- code/graphics/vulkan/vulkan_stubs.cpp | 14 +++ 17 files changed, 215 insertions(+), 157 deletions(-) diff --git a/code/decals/decals.cpp b/code/decals/decals.cpp index 7e386e595b5..20d5a5c2da1 100644 --- a/code/decals/decals.cpp +++ b/code/decals/decals.cpp @@ -370,7 +370,11 @@ void initializeMission() { active_decals.clear(); } -matrix4 getDecalTransform(Decal& decal) { +// Discard any fragments where the angle to the direction to greater than 45° +const float DECAL_ANGLE_CUTOFF = fl_radians(45.f); +const float DECAL_ANGLE_FADE_START = fl_radians(30.f); + +static matrix4 getDecalTransform(Decal& decal, float alpha) { Assertion(decal.object.objp()->type == OBJ_SHIP, "Only ships are currently supported for decals!"); auto objp = decal.object.objp(); @@ -405,11 +409,17 @@ matrix4 getDecalTransform(Decal& decal) { matrix4 mat4; vm_matrix4_set_transform(&mat4, &worldOrient, &worldPos); + // This is currently a constant but in the future this may be configurable by the decals table + mat4.a2d[0][3] = DECAL_ANGLE_CUTOFF; + mat4.a2d[1][3] = DECAL_ANGLE_FADE_START; + + mat4.a2d[2][3] = alpha; + return mat4; } void renderAll() { - if (!Decal_system_active || !Decal_option_active) { + if (!Decal_system_active || !Decal_option_active || !gr_is_capable(gr_capability::CAPABILITY_INSTANCED_RENDERING)) { return; } @@ -438,7 +448,7 @@ void renderAll() { auto mission_time = f2fl(Missiontime); - graphics::decal_draw_list draw_list(active_decals.size()); + graphics::decal_draw_list draw_list; for (auto& decal : active_decals) { Assertion(decal.definition_handle >= 0 && decal.definition_handle < (int)DecalDefinitions.size(), @@ -473,7 +483,7 @@ void renderAll() { + bm_get_anim_frame(decalDef.getNormalBitmap(), decal_time, 0.0f, decalDef.isNormalLooping()); } - draw_list.add_decal(diffuse_bm, glow_bm, normal_bm, decal_time, getDecalTransform(decal), alpha); + draw_list.add_decal(diffuse_bm, glow_bm, normal_bm, decal_time, getDecalTransform(decal, alpha)); } draw_list.render(); diff --git a/code/def_files/data/effects/decal-f.sdr b/code/def_files/data/effects/decal-f.sdr index 26ec0952d65..852e1d2b5bc 100644 --- a/code/def_files/data/effects/decal-f.sdr +++ b/code/def_files/data/effects/decal-f.sdr @@ -10,6 +10,12 @@ out vec4 fragOut0; // Diffuse buffer out vec4 fragOut1; // Normal buffer out vec4 fragOut2; // Emissive buffer +in flat mat4 invModelMatrix; +in flat vec3 decalDirection; +in flat float normal_angle_cutoff; +in flat float angle_fade_start; +in flat float alpha_scale; + uniform sampler2D gDepthBuffer; uniform sampler2D gNormalBuffer; @@ -24,23 +30,17 @@ layout (std140) uniform decalGlobalData { mat4 invProjMatrix; vec3 ambientLight; + float pad0; vec2 viewportSize; }; -layout (std140) uniform decalInfoData { - mat4 model_matrix; - mat4 inv_model_matrix; - - vec3 decal_direction; - float normal_angle_cutoff; +layout (std140) uniform decalInfoData { int diffuse_index; int glow_index; int normal_index; - float angle_fade_start; - - float alpha_scale; int diffuse_blend_mode; + int glow_blend_mode; }; @@ -77,7 +77,7 @@ vec3 getPixelNormal(vec3 frag_position, vec2 tex_coord, inout float alpha, out v #endif //Calculate angle between surface normal and decal direction - float angle = acos(dot(normal, decal_direction)); + float angle = acos(dot(normal, decalDirection)); if (angle > normal_angle_cutoff) { // The angle between surface normal and decal direction is too big @@ -91,7 +91,7 @@ vec3 getPixelNormal(vec3 frag_position, vec2 tex_coord, inout float alpha, out v } vec2 getDecalTexCoord(vec3 view_pos, inout float alpha) { - vec4 object_pos = inv_model_matrix * invViewMatrix * vec4(view_pos, 1.0); + vec4 object_pos = invModelMatrix * invViewMatrix * vec4(view_pos, 1.0); bvec3 invalidComponents = greaterThan(abs(object_pos.xyz), vec3(0.5)); bvec4 nanComponents = isnan(object_pos); // nan can happen some times if we have an infinite depth value @@ -111,6 +111,7 @@ void main() { vec3 frag_position = computeViewPosition(gl_FragCoord.xy); float alpha = alpha_scale; + vec2 tex_coord = getDecalTexCoord(frag_position, alpha); vec3 binormal; diff --git a/code/def_files/data/effects/decal-v.sdr b/code/def_files/data/effects/decal-v.sdr index 4cfa3b36b87..cebe64844c6 100644 --- a/code/def_files/data/effects/decal-v.sdr +++ b/code/def_files/data/effects/decal-v.sdr @@ -1,5 +1,12 @@ in vec4 vertPosition; +in mat4 vertModelMatrix; + +out flat mat4 invModelMatrix; +out flat vec3 decalDirection; +out flat float normal_angle_cutoff; +out flat float angle_fade_start; +out flat float alpha_scale; layout (std140) uniform decalGlobalData { mat4 viewMatrix; @@ -8,26 +15,31 @@ layout (std140) uniform decalGlobalData { mat4 invProjMatrix; vec3 ambientLight; + float pad0; vec2 viewportSize; }; -layout (std140) uniform decalInfoData { - mat4 model_matrix; - mat4 inv_model_matrix; - - vec3 decal_direction; - float normal_angle_cutoff; +layout (std140) uniform decalInfoData { int diffuse_index; int glow_index; int normal_index; - float angle_fade_start; - - float alpha_scale; int diffuse_blend_mode; + int glow_blend_mode; }; void main() { - gl_Position = projMatrix * viewMatrix * model_matrix * vertPosition; + normal_angle_cutoff = vertModelMatrix[0][3]; + angle_fade_start = vertModelMatrix[1][3]; + alpha_scale = vertModelMatrix[2][3]; + + mat4 modelMatrix = vertModelMatrix; + modelMatrix[0][3] = 0.0; + modelMatrix[1][3] = 0.0; + modelMatrix[2][3] = 0.0; + + invModelMatrix = inverse(modelMatrix); + decalDirection = mat3(viewMatrix) * vec3(modelMatrix[0][2], modelMatrix[1][2], modelMatrix[2][2]); + gl_Position = projMatrix * viewMatrix * modelMatrix * vertPosition; } diff --git a/code/graphics/2d.cpp b/code/graphics/2d.cpp index 937d01b6909..5f411d6a336 100644 --- a/code/graphics/2d.cpp +++ b/code/graphics/2d.cpp @@ -79,7 +79,8 @@ gr_capability_def gr_capabilities[] = { GR_CAPABILITY_ENTRY(SEPARATE_BLEND_FUNCTIONS), GR_CAPABILITY_ENTRY(PERSISTENT_BUFFER_MAPPING), gr_capability_def {gr_capability::CAPABILITY_BPTC, "BPTC Texture Compression"}, //This one had a different parse string already! - GR_CAPABILITY_ENTRY(LARGE_SHADER) + GR_CAPABILITY_ENTRY(LARGE_SHADER), + GR_CAPABILITY_ENTRY(INSTANCED_RENDERING), }; const size_t gr_capabilities_num = sizeof(gr_capabilities) / sizeof(gr_capabilities[0]); @@ -3072,7 +3073,7 @@ size_t hash::operator()(const vertex_layout& data) const { bool vertex_layout::resident_vertex_format(vertex_format_data::vertex_format format_type) const { return ( Vertex_mask & vertex_format_data::mask(format_type) ) ? true : false; } -void vertex_layout::add_vertex_component(vertex_format_data::vertex_format format_type, size_t stride, size_t offset) { +void vertex_layout::add_vertex_component(vertex_format_data::vertex_format format_type, size_t stride, size_t offset, size_t divisor, size_t buffer_number ) { // A stride value of 0 is not handled consistently by the graphics API so we must enforce that that does not happen Assertion(stride != 0, "The stride of a vertex component may not be zero!"); @@ -3081,15 +3082,16 @@ void vertex_layout::add_vertex_component(vertex_format_data::vertex_format forma return; } - if (Vertex_mask == 0) { + auto stride_it = Vertex_stride.find(buffer_number); + if (stride_it == Vertex_stride.end()) { // This is the first element so we need to initialize the global stride here - Vertex_stride = stride; + stride_it = Vertex_stride.emplace(buffer_number, stride).first; } - Assertion(Vertex_stride == stride, "The strides of all elements must be the same in a vertex layout!"); + Assertion(stride_it->second == stride, "The strides of all elements must be the same in a vertex layout!"); Vertex_mask |= (1 << format_type); - Vertex_components.push_back(vertex_format_data(format_type, stride, offset)); + Vertex_components.emplace_back(format_type, stride, offset, divisor, buffer_number); } bool vertex_layout::operator==(const vertex_layout& other) const { if (Vertex_mask != other.Vertex_mask) { diff --git a/code/graphics/2d.h b/code/graphics/2d.h index bb65b01f27c..7158fb8075e 100644 --- a/code/graphics/2d.h +++ b/code/graphics/2d.h @@ -271,14 +271,17 @@ struct vertex_format_data MODEL_ID, RADIUS, UVEC, + MATRIX4, }; vertex_format format_type; size_t stride; size_t offset; + size_t divisor; + size_t buffer_number; - vertex_format_data(vertex_format i_format_type, size_t i_stride, size_t i_offset) : - format_type(i_format_type), stride(i_stride), offset(i_offset) {} + vertex_format_data(vertex_format i_format_type, size_t i_stride, size_t i_offset, size_t i_divisor, size_t i_buffer_number) : + format_type(i_format_type), stride(i_stride), offset(i_offset), divisor(i_divisor), buffer_number(i_buffer_number) {} static inline uint mask(vertex_format v_format) { return 1 << v_format; } @@ -291,7 +294,7 @@ class vertex_layout SCP_vector Vertex_components; uint Vertex_mask = 0; - size_t Vertex_stride = 0; + SCP_unordered_map Vertex_stride; public: vertex_layout() {} @@ -301,9 +304,9 @@ class vertex_layout bool resident_vertex_format(vertex_format_data::vertex_format format_type) const; - void add_vertex_component(vertex_format_data::vertex_format format_type, size_t stride, size_t offset); + void add_vertex_component(vertex_format_data::vertex_format format_type, size_t stride, size_t offset, size_t divisor = 0, size_t buffer_number = 0); - size_t get_vertex_stride() const { return Vertex_stride; } + size_t get_vertex_stride(size_t buffer_number = 0) const { return Vertex_stride.at(buffer_number); } bool operator==(const vertex_layout& other) const; @@ -333,7 +336,8 @@ enum class gr_capability { CAPABILITY_SEPARATE_BLEND_FUNCTIONS, CAPABILITY_PERSISTENT_BUFFER_MAPPING, CAPABILITY_BPTC, - CAPABILITY_LARGE_SHADER + CAPABILITY_LARGE_SHADER, + CAPABILITY_INSTANCED_RENDERING }; struct gr_capability_def { @@ -891,7 +895,9 @@ typedef struct screen { primitive_type prim_type, vertex_layout* layout, int num_elements, - const indexed_vertex_source& buffers)> + const indexed_vertex_source& buffers, + const gr_buffer_handle& instance_buffer, + int num_instances)> gf_render_decals; void (*gf_render_rocket_primitives)(interface_material* material_info, primitive_type prim_type, diff --git a/code/graphics/decal_draw_list.cpp b/code/graphics/decal_draw_list.cpp index 8306c847925..bcb84f70fab 100644 --- a/code/graphics/decal_draw_list.cpp +++ b/code/graphics/decal_draw_list.cpp @@ -1,6 +1,5 @@ #include "graphics/decal_draw_list.h" -#include "graphics/util/uniform_structs.h" #include "graphics/matrix.h" #include "render/3d.h" @@ -9,10 +8,6 @@ namespace { -// Discard any fragments where the angle to the direction to greater than 45° -const float DECAL_ANGLE_CUTOFF = fl_radians(45.f); -const float DECAL_ANGLE_FADE_START = fl_radians(30.f); - vec3d BOX_VERTS[] = {{{{ -0.5f, -0.5f, -0.5f }}}, {{{ -0.5f, 0.5f, -0.5f }}}, {{{ 0.5f, 0.5f, -0.5f }}}, @@ -29,6 +24,7 @@ const size_t BOX_NUM_FACES = sizeof(BOX_FACES) / sizeof(BOX_FACES[0]); gr_buffer_handle box_vertex_buffer; gr_buffer_handle box_index_buffer; +gr_buffer_handle decal_instance_buffer; void init_buffers() { box_vertex_buffer = gr_create_buffer(BufferType::Vertex, BufferUsageHint::Static); @@ -36,6 +32,8 @@ void init_buffers() { box_index_buffer = gr_create_buffer(BufferType::Index, BufferUsageHint::Static); gr_update_buffer_data(box_index_buffer, sizeof(BOX_FACES), BOX_FACES); + + decal_instance_buffer = gr_create_buffer(BufferType::Vertex, BufferUsageHint::Streaming); } bool check_box_in_view(const matrix4& transform) { @@ -56,35 +54,6 @@ bool check_box_in_view(const matrix4& transform) { namespace graphics { -/** - * @brief Sorts Decals so that as many decals can be batched together as possible - * - * This uses the bitmaps in the definitions to determine if two decals can be rendered at the same time. Since we use - * texture arrays we can use the base frame for batching which increases the number of draw calls that can be batched together. - * - * @param left - * @param right - * @return - */ -bool decal_draw_list::sort_draws(const decal_draw_info& left, const decal_draw_info& right) { - auto left_diffuse_base = bm_get_base_frame(left.draw_mat.get_texture_map(TM_BASE_TYPE)); - auto right_diffuse_base = bm_get_base_frame(right.draw_mat.get_texture_map(TM_BASE_TYPE)); - - if (left_diffuse_base != right_diffuse_base) { - return left_diffuse_base < right_diffuse_base; - } - auto left_glow_base = bm_get_base_frame(left.draw_mat.get_texture_map(TM_GLOW_TYPE)); - auto right_glow_base = bm_get_base_frame(right.draw_mat.get_texture_map(TM_GLOW_TYPE)); - - if (left_glow_base != right_glow_base) { - return left_glow_base < right_glow_base; - } - - auto left_normal_base = bm_get_base_frame(left.draw_mat.get_texture_map(TM_NORMAL_TYPE)); - auto right_normal_base = bm_get_base_frame(left.draw_mat.get_texture_map(TM_NORMAL_TYPE)); - - return left_normal_base < right_normal_base; -} void decal_draw_list::globalInit() { init_buffers(); @@ -96,9 +65,8 @@ void decal_draw_list::globalShutdown() { gr_delete_buffer(box_index_buffer); } -decal_draw_list::decal_draw_list(size_t num_decals) -{ - _buffer = gr_get_uniform_buffer(uniform_block_type::DecalInfo, num_decals); +void decal_draw_list::prepare_global_data() { + _buffer = gr_get_uniform_buffer(uniform_block_type::DecalInfo, _draws.size()); auto& aligner = _buffer.aligner(); // Initialize header data @@ -124,19 +92,35 @@ decal_draw_list::decal_draw_list(size_t num_decals) header->ambientLight.xyz.x += gr_light_emission[0]; header->ambientLight.xyz.y += gr_light_emission[1]; header->ambientLight.xyz.z += gr_light_emission[2]; + + for (auto& [batch_info, draw_info] : _draws) { + auto info = aligner.addTypedElement(); + info->diffuse_index = batch_info.diffuse < 0 ? -1 : bm_get_array_index(batch_info.diffuse); + info->glow_index = batch_info.glow < 0 ? -1 : bm_get_array_index(batch_info.glow); + info->normal_index = batch_info.normal < 0 ? -1 : bm_get_array_index(batch_info.normal); + + draw_info.first.uniform_offset = _buffer.getBufferOffset(aligner.getCurrentOffset()); + + material_set_decal(&draw_info.first.material, + bm_get_base_frame(batch_info.diffuse), + bm_get_base_frame(batch_info.glow), + bm_get_base_frame(batch_info.normal)); + info->diffuse_blend_mode = draw_info.first.material.get_blend_mode(0) == ALPHA_BLEND_ADDITIVE ? 1 : 0; + info->glow_blend_mode = draw_info.first.material.get_blend_mode(2) == ALPHA_BLEND_ADDITIVE ? 1 : 0; + } } -decal_draw_list::~decal_draw_list() { -} + void decal_draw_list::render() { GR_DEBUG_SCOPE("Render decals"); TRACE_SCOPE(tracing::RenderDecals); - _buffer.submitData(); + prepare_global_data(); - std::sort(_draws.begin(), _draws.end(), decal_draw_list::sort_draws); + _buffer.submitData(); vertex_layout layout; layout.add_vertex_component(vertex_format_data::POSITION3, sizeof(vec3d), 0); + layout.add_vertex_component(vertex_format_data::MATRIX4, sizeof(matrix4), 0, 1, 1); indexed_vertex_source source; source.Vbuffer_handle = box_vertex_buffer; @@ -149,14 +133,13 @@ void decal_draw_list::render() { _buffer.bufferHandle()); gr_screen.gf_start_decal_pass(); - for (auto& draw : _draws) { - GR_DEBUG_SCOPE("Draw single decal"); + for (auto& [textures, decal_list] : _draws) { + GR_DEBUG_SCOPE("Draw decal type"); TRACE_SCOPE(tracing::RenderSingleDecal); - gr_bind_uniform_buffer(uniform_block_type::DecalInfo, draw.uniform_offset, sizeof(graphics::decal_info), - _buffer.bufferHandle()); - - gr_screen.gf_render_decals(&draw.draw_mat, PRIM_TYPE_TRIS, &layout, BOX_NUM_FACES, source); + gr_update_buffer_data(decal_instance_buffer, sizeof(matrix4) * decal_list.second.size(), decal_list.second.data()); + gr_bind_uniform_buffer(uniform_block_type::DecalInfo, decal_list.first.uniform_offset, sizeof(graphics::decal_info), _buffer.bufferHandle()); + gr_screen.gf_render_decals(&decal_list.first.material, PRIM_TYPE_TRIS, &layout, BOX_NUM_FACES, source, decal_instance_buffer, static_cast(decal_list.second.size())); } gr_screen.gf_stop_decal_pass(); @@ -165,45 +148,13 @@ void decal_draw_list::add_decal(int diffuse_bitmap, int glow_bitmap, int normal_bitmap, float /*decal_timer*/, - const matrix4& transform, - float base_alpha) { - if (!check_box_in_view(transform)) { + const matrix4& instancedata) { + if (!check_box_in_view(instancedata)) { // The decal box is not in view so we don't need to render it return; } - auto& aligner = _buffer.aligner(); - - auto info = aligner.addTypedElement(); - info->model_matrix = transform; - // This is currently a constant but in the future this may be configurable by the decals table - info->normal_angle_cutoff = DECAL_ANGLE_CUTOFF; - info->angle_fade_start = DECAL_ANGLE_FADE_START; - info->alpha_scale = base_alpha; - - matrix transform_rot; - vm_matrix4_get_orientation(&transform_rot, &transform); - - // The decal shader works in view-space so the direction also has to be transformed into that space - vm_vec_transform(&info->decal_direction, &transform_rot.vec.fvec, &gr_view_matrix, false); - - vm_inverse_matrix4(&info->inv_model_matrix, &info->model_matrix); - - info->diffuse_index = diffuse_bitmap < 0 ? -1 : bm_get_array_index(diffuse_bitmap); - info->glow_index = glow_bitmap < 0 ? -1 : bm_get_array_index(glow_bitmap); - info->normal_index = normal_bitmap < 0 ? -1 : bm_get_array_index(normal_bitmap); - - decal_draw_info current_draw; - current_draw.uniform_offset = _buffer.getBufferOffset(aligner.getCurrentOffset()); - - material_set_decal(¤t_draw.draw_mat, - bm_get_base_frame(diffuse_bitmap), - bm_get_base_frame(glow_bitmap), - bm_get_base_frame(normal_bitmap)); - info->diffuse_blend_mode = current_draw.draw_mat.get_blend_mode(0) == ALPHA_BLEND_ADDITIVE ? 1 : 0; - info->glow_blend_mode = current_draw.draw_mat.get_blend_mode(2) == ALPHA_BLEND_ADDITIVE ? 1 : 0; - - _draws.push_back(current_draw); + _draws[decal_batch_info{diffuse_bitmap, glow_bitmap, normal_bitmap}].second.emplace_back(instancedata); } } diff --git a/code/graphics/decal_draw_list.h b/code/graphics/decal_draw_list.h index 534357d9b13..710304acd2a 100644 --- a/code/graphics/decal_draw_list.h +++ b/code/graphics/decal_draw_list.h @@ -3,36 +3,41 @@ #include "globalincs/pstypes.h" #include "graphics/material.h" +#include "graphics/util/uniform_structs.h" #include "graphics/util/UniformBuffer.h" namespace graphics { class decal_draw_list { struct decal_draw_info { - decal_material draw_mat; - + decal_material material; size_t uniform_offset; }; - SCP_vector _draws; - - util::UniformBuffer _buffer; + struct decal_batch_info { + int diffuse, glow, normal; + bool operator< (const decal_batch_info& r) const { + return std::tie(diffuse, glow, normal) < std::tie(r.diffuse, r.glow, r.normal); + } + }; - static bool sort_draws(const decal_draw_info& left, const decal_draw_info& right); + SCP_map>> _draws; + util::UniformBuffer _buffer; public: - explicit decal_draw_list(size_t num_decals); - ~decal_draw_list(); - decal_draw_list(const decal_draw_list&) = delete; decal_draw_list& operator=(const decal_draw_list&) = delete; - void add_decal(int diffuse_bitmap, int glow_bitmap, int normal_bitmap, float decal_timer, const matrix4& transform, - float base_alpha); + void add_decal(int diffuse_bitmap, int glow_bitmap, int normal_bitmap, float decal_timer, const matrix4& instancedata); void render(); static void globalInit(); static void globalShutdown(); + + decal_draw_list() = default; + +private: + void prepare_global_data(); }; } diff --git a/code/graphics/grstub.cpp b/code/graphics/grstub.cpp index e088a693178..d3155560e2b 100644 --- a/code/graphics/grstub.cpp +++ b/code/graphics/grstub.cpp @@ -307,6 +307,24 @@ void gr_stub_shadow_map_end() { } +void gr_stub_start_decal_pass() +{ +} + +void gr_stub_stop_decal_pass() +{ +} + +void gr_stub_render_decals(decal_material* /*material_info*/, + primitive_type /*prim_type*/, + vertex_layout* /*layout*/, + int /*num_elements*/, + const indexed_vertex_source& /*buffers*/, + const gr_buffer_handle& /*instance_buffer*/, + int /*num_instances*/) +{ +} + void gr_stub_render_shield_impact(shield_material* /*material_info*/, primitive_type /*prim_type*/, vertex_layout* /*layout*/, @@ -569,6 +587,10 @@ void gr_stub_init_function_pointers() { gr_screen.gf_shadow_map_start = gr_stub_shadow_map_start; gr_screen.gf_shadow_map_end = gr_stub_shadow_map_end; + gr_screen.gf_start_decal_pass = gr_stub_start_decal_pass; + gr_screen.gf_stop_decal_pass = gr_stub_stop_decal_pass; + gr_screen.gf_render_decals = gr_stub_render_decals; + gr_screen.gf_render_shield_impact = gr_stub_render_shield_impact; gr_screen.gf_maybe_create_shader = gr_stub_maybe_create_shader; diff --git a/code/graphics/opengl/gropengl.cpp b/code/graphics/opengl/gropengl.cpp index 4756d276874..d13a48bb4ce 100644 --- a/code/graphics/opengl/gropengl.cpp +++ b/code/graphics/opengl/gropengl.cpp @@ -1489,8 +1489,11 @@ bool gr_opengl_is_capable(gr_capability capability) return GLAD_GL_ARB_texture_compression_bptc != 0; case gr_capability::CAPABILITY_LARGE_SHADER: return !Cmdline_no_large_shaders; + case gr_capability::CAPABILITY_INSTANCED_RENDERING: + return GLAD_GL_ARB_vertex_attrib_binding; } + return false; } diff --git a/code/graphics/opengl/gropengldraw.cpp b/code/graphics/opengl/gropengldraw.cpp index 34183963b35..2f7da228f0f 100644 --- a/code/graphics/opengl/gropengldraw.cpp +++ b/code/graphics/opengl/gropengldraw.cpp @@ -1147,14 +1147,16 @@ void gr_opengl_render_decals(decal_material* material_info, primitive_type prim_type, vertex_layout* layout, int num_elements, - const indexed_vertex_source& binding) { + const indexed_vertex_source& binding, + const gr_buffer_handle& instance_buffer, + int num_instances) { opengl_tnl_set_material_decal(material_info); - opengl_bind_vertex_layout(*layout, - opengl_buffer_get_id(GL_ARRAY_BUFFER, binding.Vbuffer_handle), + opengl_bind_vertex_layout_multiple(*layout, + SCP_vector { opengl_buffer_get_id(GL_ARRAY_BUFFER, binding.Vbuffer_handle), opengl_buffer_get_id(GL_ARRAY_BUFFER, instance_buffer) }, opengl_buffer_get_id(GL_ELEMENT_ARRAY_BUFFER, binding.Ibuffer_handle)); - glDrawElements(opengl_primitive_type(prim_type), num_elements, GL_UNSIGNED_INT, nullptr); + glDrawElementsInstanced(opengl_primitive_type(prim_type), num_elements, GL_UNSIGNED_INT, nullptr, num_instances); } void gr_opengl_start_decal_pass() { diff --git a/code/graphics/opengl/gropengldraw.h b/code/graphics/opengl/gropengldraw.h index 17634cd7a37..332dcd5de12 100644 --- a/code/graphics/opengl/gropengldraw.h +++ b/code/graphics/opengl/gropengldraw.h @@ -112,7 +112,9 @@ void gr_opengl_render_decals(decal_material* material_info, primitive_type prim_type, vertex_layout* layout, int num_elements, - const indexed_vertex_source& binding); + const indexed_vertex_source& binding, + const gr_buffer_handle& instance_buffer, + int num_instances); void gr_opengl_render_rocket_primitives(interface_material* material_info, primitive_type prim_type, vertex_layout* layout, diff --git a/code/graphics/opengl/gropenglshader.cpp b/code/graphics/opengl/gropenglshader.cpp index daed462a172..9a30ea855dd 100644 --- a/code/graphics/opengl/gropenglshader.cpp +++ b/code/graphics/opengl/gropenglshader.cpp @@ -58,6 +58,7 @@ SCP_vector GL_vertex_attrib_info = { opengl_vert_attrib::RADIUS, "vertRadius", {{{ 1.0f, 0.0f, 0.0f, 0.0f }}} }, { opengl_vert_attrib::UVEC, "vertUvec", {{{ 0.0f, 1.0f, 0.0f, 0.0f }}} }, { opengl_vert_attrib::WORLD_MATRIX, "vertWorldMatrix", {{{ 1.0f, 0.0f, 0.0f, 0.0f }}} }, + { opengl_vert_attrib::MODEL_MATRIX, "vertModelMatrix", {{{ 1.0f, 0.0f, 0.0f, 0.0f }}} }, }; struct opengl_uniform_block_binding { @@ -141,7 +142,7 @@ static opengl_shader_type_t GL_shader_types[] = { { opengl_vert_attrib::POSITION, opengl_vert_attrib::TEXCOORD }, "NanoVG shader", false }, { SDR_TYPE_DECAL, "decal-v.sdr", "decal-f.sdr", nullptr, - { opengl_vert_attrib::POSITION, opengl_vert_attrib::WORLD_MATRIX }, "Decal rendering", false }, + { opengl_vert_attrib::POSITION, opengl_vert_attrib::MODEL_MATRIX }, "Decal rendering", false }, { SDR_TYPE_SCENE_FOG, "post-v.sdr", "fog-f.sdr", nullptr, { opengl_vert_attrib::POSITION, opengl_vert_attrib::TEXCOORD }, "Scene fogging", false }, diff --git a/code/graphics/opengl/gropenglshader.h b/code/graphics/opengl/gropenglshader.h index 151f0d43e4f..8b61e9041b7 100644 --- a/code/graphics/opengl/gropenglshader.h +++ b/code/graphics/opengl/gropenglshader.h @@ -36,6 +36,7 @@ struct opengl_vert_attrib { RADIUS, UVEC, WORLD_MATRIX, + MODEL_MATRIX, NUM_ATTRIBS, }; diff --git a/code/graphics/opengl/gropengltnl.cpp b/code/graphics/opengl/gropengltnl.cpp index fe680cd189e..691249cb6a7 100644 --- a/code/graphics/opengl/gropengltnl.cpp +++ b/code/graphics/opengl/gropengltnl.cpp @@ -85,6 +85,7 @@ static opengl_vertex_bind GL_array_binding_data[] = { vertex_format_data::MODEL_ID, 1, GL_FLOAT, GL_FALSE, opengl_vert_attrib::MODEL_ID }, { vertex_format_data::RADIUS, 1, GL_FLOAT, GL_FALSE, opengl_vert_attrib::RADIUS }, { vertex_format_data::UVEC, 3, GL_FLOAT, GL_FALSE, opengl_vert_attrib::UVEC }, + { vertex_format_data::MATRIX4, 16, GL_FLOAT, GL_FALSE, opengl_vert_attrib::MODEL_MATRIX }, }; struct opengl_buffer_object { @@ -1231,17 +1232,25 @@ void opengl_bind_vertex_array(const vertex_layout& layout) { auto& bind_info = GL_array_binding_data[component->format_type]; auto& attrib_info = GL_vertex_attrib_info[bind_info.attribute_id]; - auto attribIndex = attrib_info.attribute_id; + auto attribIndex = static_cast(opengl_shader_get_attribute(attrib_info.attribute_id)); - glEnableVertexAttribArray(attribIndex); - glVertexAttribFormat(attribIndex, - bind_info.size, - bind_info.data_type, - bind_info.normalized, - static_cast(component->offset)); + GLuint add_val_index = 0; + for (GLint size = bind_info.size; size > 0; size -=4) { + glEnableVertexAttribArray(attribIndex + add_val_index); + glVertexAttribFormat(attribIndex + add_val_index, + std::min(size, 4), + bind_info.data_type, + bind_info.normalized, + static_cast(component->offset) + add_val_index * 16); - // Currently, all vertex data comes from one buffer. - glVertexAttribBinding(attribIndex, 0); + glVertexAttribBinding(attribIndex + add_val_index, static_cast(component->buffer_number)); + + add_val_index++; + } + + if (component->divisor != 0) { + glVertexBindingDivisor(static_cast(component->buffer_number), static_cast(component->divisor)); + } } Stored_vertex_arrays.insert(std::make_pair(layout, vao)); @@ -1267,3 +1276,28 @@ void opengl_bind_vertex_layout(vertex_layout &layout, GLuint vertexBuffer, GLuin static_cast(layout.get_vertex_stride())); GL_state.Array.BindElementBuffer(indexBuffer); } + +void opengl_bind_vertex_layout_multiple(vertex_layout &layout, const SCP_vector& vertexBuffer, GLuint indexBuffer, size_t base_offset) { + GR_DEBUG_SCOPE("Bind vertex layout"); + if (!GLAD_GL_ARB_vertex_attrib_binding) { + /* + * This will mean that decals don't render. + * It's possible, but way too much effort to support non-instanced fallback rendering here. + * By my estimation, every GPU you might still run FSO run supports this. + * per mesamatrix.net, even the least-extension supporting mesa drivers all support this extension + * */ + return; + } + + opengl_bind_vertex_array(layout); + + GLuint i = 0; + for(const auto& buffer : vertexBuffer) { + GL_state.Array.BindVertexBuffer(i, + buffer, + static_cast(base_offset), + static_cast(layout.get_vertex_stride(i))); + i++; + } + GL_state.Array.BindElementBuffer(indexBuffer); +} \ No newline at end of file diff --git a/code/graphics/opengl/gropengltnl.h b/code/graphics/opengl/gropengltnl.h index c3f590399e7..ea83618da35 100644 --- a/code/graphics/opengl/gropengltnl.h +++ b/code/graphics/opengl/gropengltnl.h @@ -83,5 +83,6 @@ void opengl_tnl_set_model_material(model_material *material_info); void gr_opengl_set_viewport(int x, int y, int width, int height); void opengl_bind_vertex_layout(vertex_layout &layout, GLuint vertexBuffer, GLuint indexBuffer, size_t base_offset = 0); +void opengl_bind_vertex_layout_multiple(vertex_layout &layout, const SCP_vector& vertexBuffer, GLuint indexBuffer, size_t base_offset = 0); #endif //_GROPENGLTNL_H diff --git a/code/graphics/util/uniform_structs.h b/code/graphics/util/uniform_structs.h index 4bf861d44ab..455a207c6c5 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -189,22 +189,13 @@ struct decal_globals { }; struct decal_info { - matrix4 model_matrix; - matrix4 inv_model_matrix; - - vec3d decal_direction; - float normal_angle_cutoff; - int diffuse_index; int glow_index; int normal_index; - float angle_fade_start; - - float alpha_scale; int diffuse_blend_mode; - int glow_blend_mode; - float pad; + int glow_blend_mode; + float pad[3]; }; struct matrix_uniforms { diff --git a/code/graphics/vulkan/vulkan_stubs.cpp b/code/graphics/vulkan/vulkan_stubs.cpp index 4027a4a2e19..a757ed553ef 100644 --- a/code/graphics/vulkan/vulkan_stubs.cpp +++ b/code/graphics/vulkan/vulkan_stubs.cpp @@ -131,6 +131,16 @@ void stub_shadow_map_start(matrix4* /*shadow_view_matrix*/, const matrix* /*ligh void stub_shadow_map_end() {} +void stub_start_decal_pass() {} +void stub_stop_decal_pass() {} +void stub_render_decals(decal_material* /*material_info*/, + primitive_type /*prim_type*/, + vertex_layout* /*layout*/, + int /*num_elements*/, + const indexed_vertex_source& /*buffers*/, + const gr_buffer_handle& /*instance_buffer*/, + int /*num_instances*/) {} + void stub_render_shield_impact(shield_material* /*material_info*/, primitive_type /*prim_type*/, vertex_layout* /*layout*/, @@ -328,6 +338,10 @@ void init_stub_pointers() gr_screen.gf_shadow_map_start = stub_shadow_map_start; gr_screen.gf_shadow_map_end = stub_shadow_map_end; + gr_screen.gf_start_decal_pass = stub_start_decal_pass; + gr_screen.gf_stop_decal_pass = stub_stop_decal_pass; + gr_screen.gf_render_decals = stub_render_decals; + gr_screen.gf_render_shield_impact = stub_render_shield_impact; gr_screen.gf_maybe_create_shader = stub_maybe_create_shader; From 44ed242aeeda2c9fccd707a7b97d5da8761b1b4e Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Thu, 10 Jul 2025 14:26:12 +0200 Subject: [PATCH 234/466] Make pspew warning a bit less annyoing (#6819) --- code/weapon/weapons.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 59c3adbe39e..71463067a01 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -659,14 +659,16 @@ struct pspew_legacy_parse_data { }; static SCP_unordered_map> pspew_legacy_parse_data_buffer; +static bool pspew_do_warning = false; static particle::ParticleEffectHandle convertLegacyPspewBuffer(const pspew_legacy_parse_data& pspew_buffer, const weapon_info* wip) { auto particle_spew_count = static_cast(pspew_buffer.particle_spew_count); float particle_spew_spawns_per_second = 1000.f / static_cast(pspew_buffer.particle_spew_time); if (particle_spew_spawns_per_second > 60.f) { - error_display(0, "PSPEW requested with a spawn frequency of over 60FPS. This used to be capped to spawn once a frame. It will now be artificially capped at 60 spawns per second."); + mprintf(("Warning: %s(line %i): PSPEW requested with a spawn frequency of over 60FPS. This used to be capped to spawn once a frame. It will now be artificially capped at 60 spawns per second.\n", Current_filename, get_line_num())); particle_spew_spawns_per_second = 60.f; + pspew_do_warning = true; } bool hasAnim = !pspew_buffer.particle_spew_anim.empty() && bm_validate_filename(pspew_buffer.particle_spew_anim, true, true); @@ -4164,8 +4166,6 @@ void parse_weaponstbl(const char *filename) required_string("#End"); } - pspew_legacy_parse_data_buffer.clear(); - // Read in a list of weapon_info indicies that are an ordering of the player weapon precedence. // This list is used to select an alternate weapon when a particular weapon is not available // during weapon selection. @@ -4815,6 +4815,11 @@ void weapon_do_post_parse() } } + pspew_legacy_parse_data_buffer.clear(); + if (pspew_do_warning) { + Warning(LOCATION, "At least one legacy PSPEW was requested with a spawn frequency of over 60FPS. See the log for details."); + } + // translate all spawn type weapons to referrnce the appropriate spawned weapon entry translate_spawn_types(); } From a565c8e76d943e1a154396f9869788bc87d3de43 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 8 Jul 2025 22:27:24 -0400 Subject: [PATCH 235/466] memory safety for model parsing Add assertions to the model parsing macros to guard against accessing out-of-bounds memory. Patch the model parsing routines to avoid going out of bounds, even if the out of bounds situations are temporary and benign. --- code/globalincs/pstypes.h | 14 ++++++++++++++ code/model/model.h | 2 +- code/model/modelcollide.cpp | 6 +++--- code/model/modelinterp.cpp | 22 +++++++++++++++++----- code/model/modelread.cpp | 10 ++++++++-- code/model/modelsinc.h | 22 ++++++++++++---------- code/render/3d.h | 2 +- code/render/3ddraw.cpp | 7 +++++-- 8 files changed, 61 insertions(+), 24 deletions(-) diff --git a/code/globalincs/pstypes.h b/code/globalincs/pstypes.h index 2f8973d78f8..90b80c7a8fb 100644 --- a/code/globalincs/pstypes.h +++ b/code/globalincs/pstypes.h @@ -308,6 +308,20 @@ constexpr bool LoggingEnabled = false; ASSUME( expr );\ } while (false) #endif + +template +bool CallAssert(bool val, const char *msg, const char *filename, int linenum, T assertMsgFunc) +{ + if (!val) + assertMsgFunc(msg, filename, linenum, nullptr); + ASSUME(val); + return true; +} +#if defined(NDEBUG) +# define AssertExpr(expr) (true) +#else +# define AssertExpr(expr) CallAssert(expr, #expr, __FILE__, __LINE__, os::dialogs::AssertMessage) +#endif /*******************NEVER COMMENT Assert ************************************************/ // Goober5000 - define Verify for use in both release and debug mode diff --git a/code/model/model.h b/code/model/model.h index 846b3f656a3..65a6c092bfe 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -1366,7 +1366,7 @@ typedef struct mc_info { */ int model_collide(mc_info *mc_info_obj); -void model_collide_parse_bsp(bsp_collision_tree *tree, void *model_ptr, int version); +void model_collide_parse_bsp(bsp_collision_tree *tree, ubyte *bsp_data, int version); bsp_collision_tree *model_get_bsp_collision_tree(int tree_index); void model_remove_bsp_collision_tree(int tree_index); diff --git a/code/model/modelcollide.cpp b/code/model/modelcollide.cpp index 7984135de2f..e95e938414f 100644 --- a/code/model/modelcollide.cpp +++ b/code/model/modelcollide.cpp @@ -577,11 +577,11 @@ void model_collide_parse_bsp_flatpoly(bsp_collision_leaf *leaf, SCP_vectorbsp_data); + auto bsp_polies = new bsp_polygon_data(model->bsp_data, model->bsp_data_size); auto textureReplace = deferredTasks.texture_replacements.find(mn); if (textureReplace != deferredTasks.texture_replacements.end()) @@ -2878,7 +2884,7 @@ void texture_map::ResetToOriginal() this->textures[i].ResetTexture(); } -bsp_polygon_data::bsp_polygon_data(ubyte* bsp_data) +bsp_polygon_data::bsp_polygon_data(ubyte* _bsp_data, int _bsp_data_size) { Polygon_vertices.clear(); Polygons.clear(); @@ -2891,7 +2897,12 @@ bsp_polygon_data::bsp_polygon_data(ubyte* bsp_data) Num_flat_verts = 0; Num_flat_polies = 0; - process_bsp(0, bsp_data); + bsp_data_size = _bsp_data_size; + + Macro_ubyte_bounds = _bsp_data + _bsp_data_size; + process_bsp(0, _bsp_data); + Macro_ubyte_bounds = nullptr; + } void bsp_polygon_data::process_bsp(int offset, ubyte* bsp_data) @@ -2935,6 +2946,7 @@ void bsp_polygon_data::process_bsp(int offset, ubyte* bsp_data) default: return; } + if (end) break; offset += size; id = w(bsp_data + offset); @@ -3291,5 +3303,5 @@ void bsp_polygon_data::replace_textures_used(const SCP_map& replacemen } SCP_set model_get_textures_used(const polymodel* pm, int submodel) { - return bsp_polygon_data{ pm->submodel[submodel].bsp_data }.get_textures_used(); + return bsp_polygon_data{ pm->submodel[submodel].bsp_data, pm->submodel[submodel].bsp_data_size }.get_textures_used(); } diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 61deb09a7f3..bc26c7c40e1 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -70,6 +70,8 @@ SCP_vector Polygon_model_instances; SCP_vector Bsp_collision_tree_list; +const ubyte* Macro_ubyte_bounds = nullptr; + static int model_initted = 0; #ifndef NDEBUG @@ -2110,7 +2112,7 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename, { sm->bsp_data_size = cfread_int(fp); if (sm->bsp_data_size > 0) { - sm->bsp_data = (ubyte*)vm_malloc(sm->bsp_data_size); + sm->bsp_data = reinterpret_cast(vm_malloc(sm->bsp_data_size)); cfread(sm->bsp_data, 1, sm->bsp_data_size, fp); swap_bsp_data(pm, sm->bsp_data); } @@ -2123,7 +2125,7 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename, sm->bsp_data_size = cfread_int(fp); if (sm->bsp_data_size > 0) { - auto bsp_data = reinterpret_cast(vm_malloc(sm->bsp_data_size)); + auto bsp_data = reinterpret_cast(vm_malloc(sm->bsp_data_size)); cfread(bsp_data, 1, sm->bsp_data_size, fp); @@ -3555,7 +3557,10 @@ int model_load(const char* filename, ship_info* sip, ErrorType error_type, bool for (i = 0; i < pm->n_models; ++i) { pm->submodel[i].collision_tree_index = model_create_bsp_collision_tree(); bsp_collision_tree* tree = model_get_bsp_collision_tree(pm->submodel[i].collision_tree_index); + + Macro_ubyte_bounds = pm->submodel[i].bsp_data + pm->submodel[i].bsp_data_size; model_collide_parse_bsp(tree, pm->submodel[i].bsp_data, pm->version); + Macro_ubyte_bounds = nullptr; } // Find the core_radius... the minimum of @@ -5725,6 +5730,7 @@ void swap_bsp_data( polymodel * pm, void * model_ptr ) Int3(); // Bad chunk type! return; } + if (end) break; p += chunk_size; chunk_type = INTEL_INT( w(p)); //tigital diff --git a/code/model/modelsinc.h b/code/model/modelsinc.h index 5d6c68c9a54..94a3c290c1f 100644 --- a/code/model/modelsinc.h +++ b/code/model/modelsinc.h @@ -60,16 +60,18 @@ class polymodel; #define ID_SLDC 0x43444c53 // CDLS (SLDC): Shield Collision Tree #define ID_SLC2 0x32434c53 // 2CLS (SLC2): Shield Collision Tree with ints instead of char - ShivanSpS -#define us(p) (*reinterpret_cast(p)) -#define cus(p) (*reinterpret_cast(p)) -#define uw(p) (*reinterpret_cast(p)) -#define cuw(p) (*reinterpret_cast(p)) -#define w(p) (*reinterpret_cast(p)) -#define cw(p) (*reinterpret_cast(p)) -#define wp(p) (reinterpret_cast(p) -#define vp(p) (reinterpret_cast(p)) -#define fl(p) (*reinterpret_cast(p)) -#define cfl(p) (*reinterpret_cast(p)) +extern const ubyte* Macro_ubyte_bounds; + +#define us(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) +#define cus(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) +#define uw(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) +#define cuw(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) +#define w(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) +#define cw(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) +#define wp(p) (AssertExpr(p < Macro_ubyte_bounds), reinterpret_cast(p) +#define vp(p) (AssertExpr(p < Macro_ubyte_bounds), reinterpret_cast(p)) +#define fl(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) +#define cfl(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) void model_calc_bound_box(vec3d *box, const vec3d *big_mn, const vec3d *big_mx); diff --git a/code/render/3d.h b/code/render/3d.h index 99a7809e11c..f0fafe87ed7 100644 --- a/code/render/3d.h +++ b/code/render/3d.h @@ -287,7 +287,7 @@ class flash_ball{ } void initialize(uint number, float min_ray_width, float max_ray_width = 0, const vec3d* dir = &vmd_zero_vector, const vec3d* pcenter = &vmd_zero_vector, float outer = PI2, float inner = 0.0f, ubyte max_r = 255, ubyte max_g = 255, ubyte max_b = 255, ubyte min_r = 255, ubyte min_g = 255, ubyte min_b = 255); - void initialize(ubyte *bsp_data, float min_ray_width, float max_ray_width = 0, const vec3d* dir = &vmd_zero_vector, const vec3d* pcenter = &vmd_zero_vector, float outer = PI2, float inner = 0.0f, ubyte max_r = 255, ubyte max_g = 255, ubyte max_b = 255, ubyte min_r = 255, ubyte min_g = 255, ubyte min_b = 255); + void initialize(ubyte *bsp_data, int bsp_data_size, float min_ray_width, float max_ray_width = 0, const vec3d* dir = &vmd_zero_vector, const vec3d* pcenter = &vmd_zero_vector, float outer = PI2, float inner = 0.0f, ubyte max_r = 255, ubyte max_g = 255, ubyte max_b = 255, ubyte min_r = 255, ubyte min_g = 255, ubyte min_b = 255); void render(int texture, float rad, float intinsity, float life); }; #endif diff --git a/code/render/3ddraw.cpp b/code/render/3ddraw.cpp index 7dd6baf40da..08364688a31 100644 --- a/code/render/3ddraw.cpp +++ b/code/render/3ddraw.cpp @@ -1333,12 +1333,15 @@ void flash_ball::parse_bsp(int offset, ubyte *bsp_data){ } } +extern const ubyte* Macro_ubyte_bounds; -void flash_ball::initialize(ubyte *bsp_data, float min_ray_width, float max_ray_width, const vec3d* dir, const vec3d* pcenter, float outer, float inner, ubyte max_r, ubyte max_g, ubyte max_b, ubyte min_r, ubyte min_g, ubyte min_b) +void flash_ball::initialize(ubyte *bsp_data, int bsp_data_size, float min_ray_width, float max_ray_width, const vec3d* dir, const vec3d* pcenter, float outer, float inner, ubyte max_r, ubyte max_g, ubyte max_b, ubyte min_r, ubyte min_g, ubyte min_b) { center = *pcenter; vm_vec_negate(¢er); - parse_bsp(0,bsp_data); + Macro_ubyte_bounds = bsp_data + bsp_data_size; + parse_bsp(0, bsp_data); + Macro_ubyte_bounds = nullptr; center = vmd_zero_vector; uint i; From 86851fe0d2f6f1c2c5cc90fb18a659c207566008 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 9 Jul 2025 20:29:52 -0400 Subject: [PATCH 236/466] make other compilers happy by only using the comma trick on debug mode --- code/model/modelsinc.h | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/code/model/modelsinc.h b/code/model/modelsinc.h index 94a3c290c1f..80bc4a4ee63 100644 --- a/code/model/modelsinc.h +++ b/code/model/modelsinc.h @@ -62,6 +62,7 @@ class polymodel; extern const ubyte* Macro_ubyte_bounds; +#ifndef NDEBUG #define us(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) #define cus(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) #define uw(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) @@ -72,6 +73,18 @@ extern const ubyte* Macro_ubyte_bounds; #define vp(p) (AssertExpr(p < Macro_ubyte_bounds), reinterpret_cast(p)) #define fl(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) #define cfl(p) (AssertExpr(p < Macro_ubyte_bounds), *reinterpret_cast(p)) +#else +#define us(p) (*reinterpret_cast(p)) +#define cus(p) (*reinterpret_cast(p)) +#define uw(p) (*reinterpret_cast(p)) +#define cuw(p) (*reinterpret_cast(p)) +#define w(p) (*reinterpret_cast(p)) +#define cw(p) (*reinterpret_cast(p)) +#define wp(p) (reinterpret_cast(p) +#define vp(p) (reinterpret_cast(p)) +#define fl(p) (*reinterpret_cast(p)) +#define cfl(p) (*reinterpret_cast(p)) +#endif void model_calc_bound_box(vec3d *box, const vec3d *big_mn, const vec3d *big_mx); From 32fcb8ccba67f6a6e87f112b85b811e2d0725a4d Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 10 Jul 2025 21:42:32 -0400 Subject: [PATCH 237/466] fix broken newline behavior when FRED saves missions Ever since retail, when encountering a comment, FRED would add an extra newline and mix `\r` with `\r\n`, often causing other bugs. The reason for this is subtle. C and C++ library functions will automatically convert `\n` to `\r\n` and back again when reading and writing in text mode, but FRED mission files are read in binary mode and written in text mode. Normally the parsing code can accommodate reading raw newlines of any type, but comments are copied verbatim from what is read to what is written. A comment ending in `\r\n` will therefore be written as `\r\r\n` which will add an extra incompatible newline. At one point (in commit a988f5eab22b2c2316bce389e24c5eee4aad7903) a fix was made for this, but it only applied to multiline comments, not all comments. Therefore, this PR moves the fix to a common function and uses it for all comment styles. --- fred2/missionsave.cpp | 41 +++++++++++++++++------------- fred2/missionsave.h | 11 +++++--- qtfred/src/mission/missionsave.cpp | 41 +++++++++++++++++------------- qtfred/src/mission/missionsave.h | 5 ++++ 4 files changed, 61 insertions(+), 37 deletions(-) diff --git a/fred2/missionsave.cpp b/fred2/missionsave.cpp index 1afcbd6fd96..efe6c8dd8a2 100644 --- a/fred2/missionsave.cpp +++ b/fred2/missionsave.cpp @@ -398,6 +398,28 @@ int CFred_mission_save::fout_version(char *format, ...) return 0; } +void CFred_mission_save::fout_raw_comment(const char *comment_start) +{ + Assertion(comment_start <= raw_ptr, "This function assumes the beginning of the comment precedes the current raw pointer!"); + + // the current character is \n, so either set it to 0, or set the preceding \r (if there is one) to 0 + if (*(raw_ptr - 1) == '\r') { + *(raw_ptr - 1) = '\0'; + } else { + *raw_ptr = '\0'; + } + + // save the comment, which will write all characters up to the 0 we just set + fout("%s\n", comment_start); + + // restore the overwritten character + if (*(raw_ptr - 1) == '\0') { + *(raw_ptr - 1) = '\r'; + } else { + *raw_ptr = '\n'; + } +} + void CFred_mission_save::parse_comments(int newlines) { char *comment_start = NULL; @@ -497,29 +519,14 @@ void CFred_mission_save::parse_comments(int newlines) if (state == 2) { if (first_comment && !flag) fout("\t\t"); + fout_raw_comment(comment_start); - *raw_ptr = 0; - fout("%s\n", comment_start); - *raw_ptr = '\n'; state = first_comment = same_line = flag = 0; } else if (state == 4) { same_line = newlines - 2 + same_line; while (same_line-- > 0) fout("\n"); - - if (*(raw_ptr - 1) == '\r') { - *(raw_ptr - 1) = '\0'; - } else { - *raw_ptr = 0; - } - - fout("%s\n", comment_start); - - if (*(raw_ptr - 1) == '\0') { - *(raw_ptr - 1) = '\r'; - } else { - *raw_ptr = '\n'; - } + fout_raw_comment(comment_start); state = first_comment = same_line = flag = 0; } diff --git a/fred2/missionsave.h b/fred2/missionsave.h index 8b5e5e578c0..9cdcde7a4fe 100644 --- a/fred2/missionsave.h +++ b/fred2/missionsave.h @@ -505,10 +505,15 @@ class CFred_mission_save */ int save_wings(); - char *raw_ptr; + /** + * @brief Utility function to save a raw comment, the start of which precedes the current raw_ptr, to a file while handling newlines properly + */ + void fout_raw_comment(const char *comment_start); + + char *raw_ptr = nullptr; SCP_vector fso_ver_comment; - int err; - CFILE *fp; + int err = 0; + CFILE *fp = nullptr; }; #endif // _MISSION_SAVE_H diff --git a/qtfred/src/mission/missionsave.cpp b/qtfred/src/mission/missionsave.cpp index 223530f3be3..dae061742b6 100644 --- a/qtfred/src/mission/missionsave.cpp +++ b/qtfred/src/mission/missionsave.cpp @@ -387,6 +387,28 @@ int CFred_mission_save::fout_version(const char* format, ...) return 0; } +void CFred_mission_save::fout_raw_comment(const char *comment_start) +{ + Assertion(comment_start <= raw_ptr, "This function assumes the beginning of the comment precedes the current raw pointer!"); + + // the current character is \n, so either set it to 0, or set the preceding \r (if there is one) to 0 + if (*(raw_ptr - 1) == '\r') { + *(raw_ptr - 1) = '\0'; + } else { + *raw_ptr = '\0'; + } + + // save the comment, which will write all characters up to the 0 we just set + fout("%s\n", comment_start); + + // restore the overwritten character + if (*(raw_ptr - 1) == '\0') { + *(raw_ptr - 1) = '\r'; + } else { + *raw_ptr = '\n'; + } +} + void CFred_mission_save::parse_comments(int newlines) { char* comment_start = NULL; @@ -499,30 +521,15 @@ void CFred_mission_save::parse_comments(int newlines) if (first_comment && !flag) { fout("\t\t"); } + fout_raw_comment(comment_start); - *raw_ptr = 0; - fout("%s\n", comment_start); - *raw_ptr = '\n'; state = first_comment = same_line = flag = 0; } else if (state == 4) { same_line = newlines - 2 + same_line; while (same_line-- > 0) { fout("\n"); } - - if (*(raw_ptr - 1) == '\r') { - *(raw_ptr - 1) = '\0'; - } else { - *raw_ptr = 0; - } - - fout("%s\n", comment_start); - - if (*(raw_ptr - 1) == '\0') { - *(raw_ptr - 1) = '\r'; - } else { - *raw_ptr = '\n'; - } + fout_raw_comment(comment_start); state = first_comment = same_line = flag = 0; } diff --git a/qtfred/src/mission/missionsave.h b/qtfred/src/mission/missionsave.h index 845627a20f2..470853d8345 100644 --- a/qtfred/src/mission/missionsave.h +++ b/qtfred/src/mission/missionsave.h @@ -516,6 +516,11 @@ class CFred_mission_save { */ int save_wings(); + /** + * @brief Utility function to save a raw comment, the start of which precedes the current raw_ptr, to a file while handling newlines properly + */ + void fout_raw_comment(const char *comment_start); + char* raw_ptr = nullptr; SCP_vector fso_ver_comment; int err = 0; From bda54ffe83040aa74711c3d4f2840b05015fc8be Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Fri, 11 Jul 2025 16:35:07 +0200 Subject: [PATCH 238/466] Fix some things in the particle code coverity flaggedf (#6821) --- code/particle/volumes/SpheroidVolume.cpp | 12 +++++++++++- code/weapon/muzzleflash.cpp | 6 +++--- code/weapon/weapons.cpp | 6 +++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/code/particle/volumes/SpheroidVolume.cpp b/code/particle/volumes/SpheroidVolume.cpp index f6dc99dd704..95d64cfdce0 100644 --- a/code/particle/volumes/SpheroidVolume.cpp +++ b/code/particle/volumes/SpheroidVolume.cpp @@ -16,7 +16,17 @@ namespace particle { float bias = m_bias * m_modular_curves.get_output(VolumeModularCurveOutput::BIAS, curveSource, &m_modular_curve_instance); if (!fl_equal(bias, 1.f)) { float mag = vm_vec_mag(&pos); - pos *= powf(mag, bias) / mag; + + if (fl_near_zero(mag)) { + if (fl_near_zero(bias)) { + //Mag and bias are zero, the point needs to be put on some point on the sphere's surface. Technically, this could be random, but as its exceedingly rare, don't bother + pos = vec3d{{{1.f, 0.f, 0.f}}}; + } + //else: Mag is zero, but bias is not. The point should stay at 0,0,0, so no change is necessary + } + else { + pos *= powf(mag, bias) / mag; + } } // maybe stretch it diff --git a/code/weapon/muzzleflash.cpp b/code/weapon/muzzleflash.cpp index be0d6ea9201..a069701ad7b 100644 --- a/code/weapon/muzzleflash.cpp +++ b/code/weapon/muzzleflash.cpp @@ -154,9 +154,9 @@ void parse_mflash_tbl(const char *filename) static void convert_mflash_to_particle() { Curve new_curve = Curve(";MuzzleFlashMinSizeScalingCurve"); - new_curve.keyframes.push_back(curve_keyframe{vec2d{ -0.00001f , 0.f}, CurveInterpFunction::Polynomial, -1.0f, 1.0f}); //just for numerical safety if we ever get an actual size of 0... - new_curve.keyframes.push_back(curve_keyframe{vec2d{ Min_pizel_size_muzzleflash, 1.f }, CurveInterpFunction::Constant, 0.0f, 1.0f}); - Curves.push_back(new_curve); + new_curve.keyframes.emplace_back(curve_keyframe{vec2d{ -0.00001f , 0.f}, CurveInterpFunction::Polynomial, -1.0f, 1.0f}); //just for numerical safety if we ever get an actual size of 0... + new_curve.keyframes.emplace_back(curve_keyframe{vec2d{ Min_pizel_size_muzzleflash, 1.f }, CurveInterpFunction::Constant, 0.0f, 1.0f}); + Curves.emplace_back(std::move(new_curve)); modular_curves_entry scaling_curve {(static_cast(Curves.size()) - 1), ::util::UniformFloatRange(1.f), ::util::UniformFloatRange(0.f), false}; for (const auto& mflash : Mflash_info) { diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 71463067a01..b19b99becdd 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -2640,9 +2640,9 @@ int parse_weapon(int subtype, bool replace, const char *filename) curve_name += "SpawnDelayCurve" + std::to_string(spawn_weap); Curve new_curve = Curve(curve_name); - new_curve.keyframes.push_back(curve_keyframe{ vec2d { 0.f, 0.f}, CurveInterpFunction::Constant, 0.0f, 1.0f }); - new_curve.keyframes.push_back(curve_keyframe{ vec2d { delay, 1.f}, CurveInterpFunction::Constant, 0.0f, 1.0f }); - Curves.push_back(new_curve); + new_curve.keyframes.emplace_back(curve_keyframe{ vec2d { 0.f, 0.f}, CurveInterpFunction::Constant, 0.0f, 1.0f }); + new_curve.keyframes.emplace_back(curve_keyframe{ vec2d { delay, 1.f}, CurveInterpFunction::Constant, 0.0f, 1.0f }); + Curves.emplace_back(std::move(new_curve)); wip->weapon_curves.add_curve("Age", weapon_info::WeaponCurveOutputs::SPAWN_RATE_MULT, modular_curves_entry{(static_cast(Curves.size()) - 1)}); } From 218d8c09bedcac94b7621a27190f0c68359ee3ae Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Fri, 11 Jul 2025 22:13:25 +0200 Subject: [PATCH 239/466] Rely on bound vai's rather than try to retrieve them (#6822) --- code/graphics/opengl/ShaderProgram.cpp | 14 ++------------ code/graphics/opengl/ShaderProgram.h | 6 +----- code/graphics/opengl/gropenglshader.cpp | 18 ++---------------- code/graphics/opengl/gropenglshader.h | 3 --- code/graphics/opengl/gropengltnl.cpp | 4 ++-- 5 files changed, 7 insertions(+), 38 deletions(-) diff --git a/code/graphics/opengl/ShaderProgram.cpp b/code/graphics/opengl/ShaderProgram.cpp index aa0d7194106..02b39442b5d 100644 --- a/code/graphics/opengl/ShaderProgram.cpp +++ b/code/graphics/opengl/ShaderProgram.cpp @@ -183,18 +183,16 @@ void opengl::ShaderProgram::linkProgram() { freeCompiledShaders(); } -void opengl::ShaderProgram::initAttribute(const SCP_string& name, opengl_vert_attrib::attrib_id attr_id, const vec4& default_value) +void opengl::ShaderProgram::initAttribute(const SCP_string& name, const vec4& default_value) { auto attrib_loc = glGetAttribLocation(_program_id, name.c_str()); if (attrib_loc == -1) { - // Not available, ignore + // Not available or optimized out, ignore return; } - _attribute_locations.insert(std::make_pair(attr_id, attrib_loc)); - // The shader needs to be in use before glVertexAttrib can be used use(); glVertexAttrib4f( @@ -205,14 +203,6 @@ void opengl::ShaderProgram::initAttribute(const SCP_string& name, opengl_vert_at default_value.xyzw.w ); } -GLint opengl::ShaderProgram::getAttributeLocation(opengl_vert_attrib::attrib_id attribute) { - auto iter = _attribute_locations.find(attribute); - if (iter == _attribute_locations.end()) { - return -1; - } else { - return iter->second; - } -} opengl::ShaderUniforms::ShaderUniforms(ShaderProgram* shaderProgram) : _program(shaderProgram) { Assertion(shaderProgram != nullptr, "Shader program may not be null!"); diff --git a/code/graphics/opengl/ShaderProgram.h b/code/graphics/opengl/ShaderProgram.h index 925338dbfce..dba0c92beef 100644 --- a/code/graphics/opengl/ShaderProgram.h +++ b/code/graphics/opengl/ShaderProgram.h @@ -46,8 +46,6 @@ class ShaderProgram { SCP_vector _compiled_shaders; - SCP_unordered_map _attribute_locations; - void freeCompiledShaders(); public: explicit ShaderProgram(const SCP_string& program_name); @@ -67,9 +65,7 @@ class ShaderProgram { void linkProgram(); - void initAttribute(const SCP_string& name, opengl_vert_attrib::attrib_id attr_id, const vec4& default_value); - - GLint getAttributeLocation(opengl_vert_attrib::attrib_id attribute); + void initAttribute(const SCP_string& name, const vec4& default_value); GLuint getShaderHandle(); }; diff --git a/code/graphics/opengl/gropenglshader.cpp b/code/graphics/opengl/gropenglshader.cpp index 9a30ea855dd..9c1dca46b97 100644 --- a/code/graphics/opengl/gropenglshader.cpp +++ b/code/graphics/opengl/gropenglshader.cpp @@ -57,7 +57,6 @@ SCP_vector GL_vertex_attrib_info = { opengl_vert_attrib::MODEL_ID, "vertModelID", {{{ 0.0f, 0.0f, 0.0f, 0.0f }}} }, { opengl_vert_attrib::RADIUS, "vertRadius", {{{ 1.0f, 0.0f, 0.0f, 0.0f }}} }, { opengl_vert_attrib::UVEC, "vertUvec", {{{ 0.0f, 1.0f, 0.0f, 0.0f }}} }, - { opengl_vert_attrib::WORLD_MATRIX, "vertWorldMatrix", {{{ 1.0f, 0.0f, 0.0f, 0.0f }}} }, { opengl_vert_attrib::MODEL_MATRIX, "vertModelMatrix", {{{ 1.0f, 0.0f, 0.0f, 0.0f }}} }, }; @@ -885,7 +884,7 @@ void opengl_compile_shader_actual(shader_type sdr, const uint &flags, opengl_sha // initialize the attributes for (auto& attr : sdr_info->attributes) { - new_shader.program->initAttribute(GL_vertex_attrib_info[attr].name, GL_vertex_attrib_info[attr].attribute_id, GL_vertex_attrib_info[attr].default_value); + new_shader.program->initAttribute(GL_vertex_attrib_info[attr].name, GL_vertex_attrib_info[attr].default_value); } for (auto& uniform_block : GL_uniform_blocks) { @@ -905,7 +904,7 @@ void opengl_compile_shader_actual(shader_type sdr, const uint &flags, opengl_sha if (sdr_info->type_id == variant.type_id && variant.flag & flags) { for (auto& attr : variant.attributes) { auto& attr_info = GL_vertex_attrib_info[attr]; - new_shader.program->initAttribute(attr_info.name, attr_info.attribute_id, attr_info.default_value); + new_shader.program->initAttribute(attr_info.name, attr_info.default_value); } nprintf(("shaders"," %s\n", variant.description)); @@ -1077,19 +1076,6 @@ void opengl_shader_init() nprintf(("shaders","\n")); } -/** - * Get the internal OpenGL location for a given attribute. Requires that the Current_shader global variable is valid - * - * @param attribute_text Name of the attribute - * @return Internal OpenGL location for the attribute - */ -GLint opengl_shader_get_attribute(opengl_vert_attrib::attrib_id attribute) -{ - Assertion(Current_shader != nullptr, "Current shader may not be null!"); - - return Current_shader->program->getAttributeLocation(attribute); -} - void opengl_shader_set_passthrough(bool textured, bool hdr) { opengl_shader_set_current(gr_opengl_maybe_create_shader(SDR_TYPE_PASSTHROUGH_RENDER, 0)); diff --git a/code/graphics/opengl/gropenglshader.h b/code/graphics/opengl/gropenglshader.h index 8b61e9041b7..6df7875f554 100644 --- a/code/graphics/opengl/gropenglshader.h +++ b/code/graphics/opengl/gropenglshader.h @@ -35,7 +35,6 @@ struct opengl_vert_attrib { MODEL_ID, RADIUS, UVEC, - WORLD_MATRIX, MODEL_MATRIX, NUM_ATTRIBS, }; @@ -152,8 +151,6 @@ void opengl_shader_shutdown(); int opengl_compile_shader(shader_type sdr, uint flags); -GLint opengl_shader_get_attribute(opengl_vert_attrib::attrib_id attribute); - void opengl_shader_set_passthrough(bool textured, bool hdr); void opengl_shader_set_default_material(bool textured, bool alpha, vec4* clr, float color_scale, uint32_t array_index, const material::clip_plane& clip_plane); diff --git a/code/graphics/opengl/gropengltnl.cpp b/code/graphics/opengl/gropengltnl.cpp index 691249cb6a7..db51f62b7bc 100644 --- a/code/graphics/opengl/gropengltnl.cpp +++ b/code/graphics/opengl/gropengltnl.cpp @@ -1190,7 +1190,7 @@ void opengl_bind_vertex_component(const vertex_format_data &vert_component, size if ( Current_shader != NULL ) { // grabbing a vertex attribute is dependent on what current shader has been set. i hope no one calls opengl_bind_vertex_layout before opengl_set_current_shader - GLint index = opengl_shader_get_attribute(attrib_info.attribute_id); + GLint index = attrib_info.attribute_id; if ( index >= 0 ) { GL_state.Array.EnableVertexAttrib(index); @@ -1232,7 +1232,7 @@ void opengl_bind_vertex_array(const vertex_layout& layout) { auto& bind_info = GL_array_binding_data[component->format_type]; auto& attrib_info = GL_vertex_attrib_info[bind_info.attribute_id]; - auto attribIndex = static_cast(opengl_shader_get_attribute(attrib_info.attribute_id)); + auto attribIndex = attrib_info.attribute_id; GLuint add_val_index = 0; for (GLint size = bind_info.size; size > 0; size -=4) { From b9b64fe49d356b5cc702ba0b0f723534d120a094 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 11 Jul 2025 21:01:33 -0500 Subject: [PATCH 240/466] Show team colors with Lua rendering methods (#6802) * get set team colors with lua's rendering methods * fix for checks * tweak * add includes --- code/missionui/missionweaponchoice.cpp | 8 +++- code/missionui/missionweaponchoice.h | 5 ++- code/model/modelrender.cpp | 4 +- code/model/modelrender.h | 2 +- code/scripting/api/objs/parse_object.cpp | 28 ++++++++++++++ code/scripting/api/objs/shipclass.cpp | 47 ++++++++++++++++-------- 6 files changed, 73 insertions(+), 21 deletions(-) diff --git a/code/missionui/missionweaponchoice.cpp b/code/missionui/missionweaponchoice.cpp index 2013236cff3..fa197e1ed4e 100644 --- a/code/missionui/missionweaponchoice.cpp +++ b/code/missionui/missionweaponchoice.cpp @@ -765,7 +765,8 @@ void draw_3d_overhead_view(int model_num, int bank_prim_offset, int bank_sec_offset, int bank_y_offset, - overhead_style style) + overhead_style style, + const SCP_string& tcolor) { ship_info* sip = &Ship_info[ship_class]; @@ -851,6 +852,11 @@ void draw_3d_overhead_view(int model_num, render_info.set_flags(MR_AUTOCENTER | MR_NO_FOGGING); + if (sip->uses_team_colors) { + SCP_string tc = tcolor.empty() ? sip->default_team_name : tcolor; + render_info.set_team_color(tc, "none", 0, 0); + } + model_render_immediate(&render_info, model_num, &object_orient, &vmd_zero_vector); Glowpoint_use_depth_buffer = true; diff --git a/code/missionui/missionweaponchoice.h b/code/missionui/missionweaponchoice.h index 7336fc7bd29..a59a16fa82c 100644 --- a/code/missionui/missionweaponchoice.h +++ b/code/missionui/missionweaponchoice.h @@ -7,6 +7,8 @@ * */ +#include "globalincs/globals.h" +#include "mod_table/mod_table.h" #ifndef __MISSION_WEAPON_CHOICE_H__ @@ -57,7 +59,8 @@ void draw_3d_overhead_view(int model_num, int bank_prim_offset = 106, int bank_sec_offset = -50, int bank_y_offset = 12, - overhead_style style = Default_overhead_ship_style); + overhead_style style = Default_overhead_ship_style, + const SCP_string& tcolor = ""); void wl_update_parse_object_weapons(p_object *pobjp, wss_unit *slot); int wl_update_ship_weapons(int objnum, wss_unit *slot); diff --git a/code/model/modelrender.cpp b/code/model/modelrender.cpp index 6ba9f0866e8..f72cb518e66 100644 --- a/code/model/modelrender.cpp +++ b/code/model/modelrender.cpp @@ -3099,7 +3099,7 @@ void modelinstance_replace_active_texture(polymodel_instance* pmi, const char* o // renders a model as if in the tech room or briefing UI // model_type 1 for ship class, 2 for weapon class, 3 for pof -bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int y2, float zoom, bool lighting, int class_idx, const matrix* orient, const SCP_string &pof_filename, float close_zoom, const vec3d *close_pos) +bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int y2, float zoom, bool lighting, int class_idx, const matrix* orient, const SCP_string &pof_filename, float close_zoom, const vec3d *close_pos, const SCP_string& tcolor) { model_render_params render_info; @@ -3118,7 +3118,7 @@ bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int closeup_zoom = sip->closeup_zoom; if (sip->uses_team_colors) { - render_info.set_team_color(sip->default_team_name, "none", 0, 0); + render_info.set_team_color(!tcolor.empty() ? tcolor : sip->default_team_name, "none", 0, 0); } if (sip->flags[Ship::Info_Flags::No_lighting]) { diff --git a/code/model/modelrender.h b/code/model/modelrender.h index fb4e3abd470..64f041a15ef 100644 --- a/code/model/modelrender.h +++ b/code/model/modelrender.h @@ -314,7 +314,7 @@ bool model_render_check_detail_box(const vec3d* view_pos, const polymodel* pm, i void model_render_arc(const vec3d* v1, const vec3d* v2, const SCP_vector *persistent_arc_points, const color* primary, const color* secondary, float arc_width, ubyte depth_limit); void model_render_insignias(const insignia_draw_data* insignia); void model_render_set_wireframe_color(const color* clr); -bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int y2, float zoom, bool lighting, int class_idx, const matrix* orient, const SCP_string& pof_filename = "", float closeup_zoom = 0, const vec3d* closeup_pos = &vmd_zero_vector); +bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int y2, float zoom, bool lighting, int class_idx, const matrix* orient, const SCP_string& pof_filename = "", float closeup_zoom = 0, const vec3d* closeup_pos = &vmd_zero_vector, const SCP_string& tcolor = ""); float convert_distance_and_diameter_to_pixel_size(float distance, float diameter, float field_of_view_deg, int screen_height); diff --git a/code/scripting/api/objs/parse_object.cpp b/code/scripting/api/objs/parse_object.cpp index c6468e87008..426c803c46b 100644 --- a/code/scripting/api/objs/parse_object.cpp +++ b/code/scripting/api/objs/parse_object.cpp @@ -7,6 +7,7 @@ #include "vecmath.h" #include "weaponclass.h" #include "wing.h" +//#include "globalincs/alphacolors.h" //Needed for team colors #include "mission/missionparse.h" @@ -332,6 +333,33 @@ ADE_VIRTVAR(Team, l_ParseObject, "team", "The team of the parsed ship.", "team", return ade_set_args(L, "o", l_Team.Set(poh->getObject()->team)); } +ADE_VIRTVAR(TeamColor, l_ParseObject, "string", "The team color", "string", "The name of the team color or empty if not set or invalid.") +{ + parse_object_h* poh = nullptr; + const char* team_color = nullptr; + if (!ade_get_args(L, "o|s", l_ParseObject.GetPtr(&poh), &team_color)) + return ade_set_error(L, "s", ""); + + if (!poh->isValid()) + return ade_set_error(L, "s", ""); + + //Set team color + if (ADE_SETTING_VAR && team_color != nullptr) { + + // Verify + /*if (Team_Colors.find(team_color) == Team_Colors.end()) { + mprintf(("Invalid team color specified in mission file for ship %s. Not setting!\n", poh->getObject()->name)); + } else { + poh->getObject()->team_color_setting = team_color; + }*/ + + LuaError(L, "Setting team colors is not yet supported!"); + + } + + return ade_set_args(L, "s", poh->getObject()->team_color_setting); +} + ADE_VIRTVAR(InitialHull, l_ParseObject, "number", "The initial hull percentage of this parsed ship.", "number", "The initial hull") { diff --git a/code/scripting/api/objs/shipclass.cpp b/code/scripting/api/objs/shipclass.cpp index 674d8b5f218..caf9c022f98 100644 --- a/code/scripting/api/objs/shipclass.cpp +++ b/code/scripting/api/objs/shipclass.cpp @@ -1113,7 +1113,7 @@ ADE_FUNC(isInTechroom, l_Shipclass, NULL, "Gets whether or not the ship class is ADE_FUNC(renderTechModel, l_Shipclass, "number X1, number Y1, number X2, number Y2, [number RotationPercent =0, number PitchPercent =0, number " - "BankPercent=40, number Zoom=1.3, boolean Lighting=true]", + "BankPercent=40, number Zoom=1.3, boolean Lighting=true, string TeamColor=nil]", "Draws ship model as if in techroom. True for regular lighting, false for flat lighting.", "boolean", "Whether ship was rendered") @@ -1123,7 +1123,8 @@ ADE_FUNC(renderTechModel, int idx; float zoom = 1.3f; bool lighting = true; - if(!ade_get_args(L, "oiiii|ffffb", l_Shipclass.Get(&idx), &x1, &y1, &x2, &y2, &rot_angles.h, &rot_angles.p, &rot_angles.b, &zoom, &lighting)) + const char* team_color = nullptr; + if(!ade_get_args(L, "oiiii|ffffbs", l_Shipclass.Get(&idx), &x1, &y1, &x2, &y2, &rot_angles.h, &rot_angles.p, &rot_angles.b, &zoom, &lighting, &team_color)) return ade_set_error(L, "b", false); if(idx < 0 || idx >= ship_info_size()) @@ -1146,17 +1147,20 @@ ADE_FUNC(renderTechModel, rot_angles.h = (rot_angles.h*0.01f) * PI2; vm_rotate_matrix_by_angles(&orient, &rot_angles); - return ade_set_args(L, "b", render_tech_model(TECH_SHIP, x1, y1, x2, y2, zoom, lighting, idx, &orient)); + SCP_string tcolor = team_color ? team_color : ""; + + return ade_set_args(L, "b", render_tech_model(TECH_SHIP, x1, y1, x2, y2, zoom, lighting, idx, &orient, tcolor)); } // Nuke's alternate tech model rendering function -ADE_FUNC(renderTechModel2, l_Shipclass, "number X1, number Y1, number X2, number Y2, [orientation Orientation=nil, number Zoom=1.3]", "Draws ship model as if in techroom", "boolean", "Whether ship was rendered") +ADE_FUNC(renderTechModel2, l_Shipclass, "number X1, number Y1, number X2, number Y2, [orientation Orientation=nil, number Zoom=1.3, string TeamColor=nil]", "Draws ship model as if in techroom", "boolean", "Whether ship was rendered") { int x1,y1,x2,y2; int idx; float zoom = 1.3f; - matrix_h *mh = NULL; - if(!ade_get_args(L, "oiiiio|f", l_Shipclass.Get(&idx), &x1, &y1, &x2, &y2, l_Matrix.GetPtr(&mh), &zoom)) + matrix_h *mh = nullptr; + const char* team_color = nullptr; + if(!ade_get_args(L, "oiiiio|fs", l_Shipclass.Get(&idx), &x1, &y1, &x2, &y2, l_Matrix.GetPtr(&mh), &zoom, &team_color)) return ade_set_error(L, "b", false); if(idx < 0 || idx >= ship_info_size()) @@ -1168,12 +1172,14 @@ ADE_FUNC(renderTechModel2, l_Shipclass, "number X1, number Y1, number X2, number //Handle angles matrix *orient = mh->GetMatrix(); - return ade_set_args(L, "b", render_tech_model(TECH_SHIP, x1, y1, x2, y2, zoom, true, idx, orient)); + SCP_string tcolor = team_color ? team_color : ""; + + return ade_set_args(L, "b", render_tech_model(TECH_SHIP, x1, y1, x2, y2, zoom, true, idx, orient, tcolor)); } ADE_FUNC(renderSelectModel, l_Shipclass, - "boolean restart, number x, number y, [number width = 629, number height = 355, number currentEffectSetting = default, number zoom = 1.3]", + "boolean restart, number x, number y, [number width = 629, number height = 355, number currentEffectSetting = default, number zoom = 1.3, string TeamColor=nil]", "Draws the 3D select ship model with the chosen effect at the specified coordinates. Restart should " "be true on the first frame this is called and false on subsequent frames. Valid selection effects are 1 (fs1) or 2 (fs2), " "defaults to the mod setting or the model's setting. Zoom is a multiplier to the model's closeup_zoom value.", @@ -1188,7 +1194,8 @@ ADE_FUNC(renderSelectModel, int y2 = 355; int effect = -1; float zoom = 1.3f; - if (!ade_get_args(L, "obii|iiif", l_Shipclass.Get(&idx), &restart, &x1, &y1, &x2, &y2, &effect, &zoom)) + const char* team_color = nullptr; + if (!ade_get_args(L, "obii|iiifs", l_Shipclass.Get(&idx), &restart, &x1, &y1, &x2, &y2, &effect, &zoom, &team_color)) return ADE_RETURN_NIL; if (idx < 0 || idx >= ship_info_size()) @@ -1223,7 +1230,8 @@ ADE_FUNC(renderSelectModel, model_render_params render_info; if (sip->uses_team_colors) { - render_info.set_team_color(sip->default_team_name, "none", 0, 0); + SCP_string tcolor = team_color ? team_color : sip->default_team_name; + render_info.set_team_color(tcolor, "none", 0, 0); } if (sip->replacement_textures.size() > 0) { @@ -1252,7 +1260,7 @@ ADE_FUNC(renderOverheadModel, "number x, number y, [number width = 467, number height = 362, number|table /* selectedSlot = -1 or empty table */, number selectedWeapon = -1, number hoverSlot = -1, " "number bank1_x = 170, number bank1_y = 203, number bank2_x = 170, number bank2_y = 246, number bank3_x = 170, number bank3_y = 290, " "number bank4_x = 552, number bank4_y = 203, number bank5_x = 552, number bank5_y = 246, number bank6_x = 552, number bank6_y = 290, " - "number bank7_x = 552, number bank7_y = 333, number style = 0]", + "number bank7_x = 552, number bank7_y = 333, number style = 0, string TeamColor=nil]", "Draws the 3D overhead ship model with the lines pointing from bank weapon selections to bank firepoints. SelectedSlot refers to loadout " "ship slots 1-12 where wing 1 is 1-4, wing 2 is 5-8, and wing 3 is 9-12. SelectedWeapon is the index into weapon classes. HoverSlot refers " "to the bank slots 1-7 where 1-3 are primaries and 4-6 are secondaries. Lines will be drawn from any bank containing the SelectedWeapon to " @@ -1293,10 +1301,12 @@ ADE_FUNC(renderOverheadModel, int weapon_list[MAX_SHIP_WEAPONS] = {-1, -1, -1, -1, -1, -1, -1}; + const char* team_color = nullptr; + if (lua_isnumber(L, 6)) { if (!ade_get_args(L, - "oii|iiiiiiiiiiiiiiiiiiii", + "oii|iiiiiiiiiiiiiiiiiiiis", l_Shipclass.Get(&idx), &x1, &y1, @@ -1319,7 +1329,8 @@ ADE_FUNC(renderOverheadModel, &bank6_y, &bank7_x, &bank7_y, - &style)) + &style, + &team_color)) return ADE_RETURN_NIL; // Convert this from the Lua index @@ -1333,7 +1344,7 @@ ADE_FUNC(renderOverheadModel, } } else { if (!ade_get_args(L, - "oii|iitiiiiiiiiiiiiiiiii", + "oii|iitiiiiiiiiiiiiiiiiis", l_Shipclass.Get(&idx), &x1, &y1, @@ -1356,7 +1367,8 @@ ADE_FUNC(renderOverheadModel, &bank6_y, &bank7_x, &bank7_y, - &style)) + &style, + &team_color)) return ADE_RETURN_NIL; int count = 0; @@ -1403,6 +1415,8 @@ ADE_FUNC(renderOverheadModel, ship_info* sip = &Ship_info[idx]; + SCP_string tcolor = team_color ? team_color : sip->default_team_name; + int modelNum = model_load(sip->pof_file, sip); model_page_in_textures(modelNum, idx); static float ShipRot = 0.0f; @@ -1436,7 +1450,8 @@ ADE_FUNC(renderOverheadModel, 0, 0, 0, - (overhead_style)style); + (overhead_style)style, + tcolor); return ade_set_args(L, "b", true); } From 6ccb856f78dcfc8004ba9a56929f0c8e8c16d005 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 12 Jul 2025 00:23:06 -0400 Subject: [PATCH 241/466] Prepare to sync 2025-07-12 Get the codebase into a state where it can be automatically synced with the FSO code. This commit will be rolled back after the sync. --- code/mission/missionparse.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index a0277022d1c..451a51cfa05 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6922,7 +6922,7 @@ bool parse_main(const char *mission_name, int flags) do { // don't do this for imports - if (!(flags & MPF_IMPORT_FSM) && !(flags & MPF_IMPORT_XWI)) { + if (!(flags & MPF_IMPORT_FSM)) { CFILE *ftemp = cfopen(mission_name, "rt", CFILE_NORMAL, CF_TYPE_MISSIONS); // fail situation. From 6921f550c13994a53e8ec3f45eefb4208a510a6b Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Sat, 12 Jul 2025 02:40:53 -0700 Subject: [PATCH 242/466] Enhanced functionality for conditional impacts (#6785) * add new conditions * don't worry about it * implement new conditions * bugfixing * handle pokethrough differently * clang for the clang god * appeasement * to the last i grapple with thee * from hell's heart etc * bugfixing * support hits while dying * fix shield math, cleanup * fix memory error --- code/debris/debris.cpp | 2 + code/debris/debris.h | 1 + code/network/multimsgs.cpp | 3 +- code/object/collidedebrisweapon.cpp | 28 +- code/object/collideshipweapon.cpp | 4 +- code/object/collideweaponweapon.cpp | 88 +++++- code/ship/shiphit.cpp | 112 +++++++- code/ship/shiphit.h | 5 +- code/weapon/beam.cpp | 9 +- code/weapon/weapon.h | 46 ++- code/weapon/weapons.cpp | 416 ++++++++++++++++++---------- 11 files changed, 529 insertions(+), 185 deletions(-) diff --git a/code/debris/debris.cpp b/code/debris/debris.cpp index 83bebee81be..11b024220c0 100644 --- a/code/debris/debris.cpp +++ b/code/debris/debris.cpp @@ -705,6 +705,8 @@ object *debris_create_only(int parent_objnum, int parent_ship_class, int alt_typ } } + db->max_hull = obj->hull_strength; + if (hull_flag) { MONITOR_INC(NumHullDebris,1); } else { diff --git a/code/debris/debris.h b/code/debris/debris.h index f325bc752d4..7377f0e7fad 100644 --- a/code/debris/debris.h +++ b/code/debris/debris.h @@ -51,6 +51,7 @@ typedef struct debris { int submodel_num; // What submodel this uses TIMESTAMP arc_next_time; // When the next damage/emp arc will be created. bool is_hull; // indicates whether this is a collideable, destructable piece of debris from the model, or just a generic debris fragment + float max_hull; int species; // What species this is from. -1 if don't care. TIMESTAMP arc_timeout; // timestamp that holds time for arcs to stop appearing TIMESTAMP sound_delay; // timestamp to signal when sound should start diff --git a/code/network/multimsgs.cpp b/code/network/multimsgs.cpp index 9e32d4b788a..3fb12ebecba 100644 --- a/code/network/multimsgs.cpp +++ b/code/network/multimsgs.cpp @@ -6997,7 +6997,8 @@ void process_asteroid_info( ubyte *data, header *hinfo ) // if we know the other object is a weapon, then do a weapon hit to kill the weapon if ( other_objp && (other_objp->type == OBJ_WEAPON) ){ - weapon_hit( other_objp, objp, &hitpos ); + bool armed = weapon_hit( other_objp, objp, &hitpos ); + maybe_play_conditional_impacts({}, other_objp, objp, armed, -1, &hitpos); } break; } diff --git a/code/object/collidedebrisweapon.cpp b/code/object/collidedebrisweapon.cpp index 2fec71cdcca..d33426fdb30 100644 --- a/code/object/collidedebrisweapon.cpp +++ b/code/object/collidedebrisweapon.cpp @@ -68,8 +68,18 @@ int collide_debris_weapon( obj_pair * pair ) if(!weapon_override && !debris_override) { vec3d force = weapon_obj->phys_info.vel * Weapon_info[Weapons[weapon_obj->instance].weapon_info_index].mass; - weapon_hit( weapon_obj, pdebris, &hitpos, -1, &hitnormal ); - debris_hit( pdebris, weapon_obj, &hitpos, Weapon_info[Weapons[weapon_obj->instance].weapon_info_index].damage , &force); + bool armed = weapon_hit( weapon_obj, pdebris, &hitpos, -1 ); + float damage = Weapon_info[Weapons[weapon_obj->instance].weapon_info_index].damage; + std::array, NumHitTypes> impact_data = {}; + impact_data[static_cast>(HitType::HULL)] = ConditionData { + SpecialImpactCondition::DEBRIS, + HitType::HULL, + damage, + pdebris->hull_strength, + Debris[pdebris->instance].max_hull, + }; + maybe_play_conditional_impacts(impact_data, weapon_obj, pdebris, armed, -1, &hitpos, nullptr, &hitnormal); + debris_hit( pdebris, weapon_obj, &hitpos, damage , &force); } if (scripting::hooks::OnDebrisCollision->isActive() && !(debris_override && !weapon_override)) @@ -147,8 +157,18 @@ int collide_asteroid_weapon( obj_pair * pair ) if(!weapon_override && !asteroid_override) { vec3d force = weapon_obj->phys_info.vel * Weapon_info[Weapons[weapon_obj->instance].weapon_info_index].mass; - weapon_hit( weapon_obj, pasteroid, &hitpos, -1, &hitnormal); - asteroid_hit( pasteroid, weapon_obj, &hitpos, Weapon_info[Weapons[weapon_obj->instance].weapon_info_index].damage, &force ); + bool armed = weapon_hit( weapon_obj, pasteroid, &hitpos, -1); + float damage = Weapon_info[Weapons[weapon_obj->instance].weapon_info_index].damage; + std::array, NumHitTypes> impact_data = {}; + impact_data[static_cast>(HitType::HULL)] = ConditionData { + SpecialImpactCondition::DEBRIS, + HitType::HULL, + damage, + pasteroid->hull_strength, + Asteroid_info[Asteroids[pasteroid->instance].asteroid_type].initial_asteroid_strength, + }; + maybe_play_conditional_impacts(impact_data, weapon_obj, pasteroid, armed, -1, &hitpos, nullptr, &hitnormal); + asteroid_hit( pasteroid, weapon_obj, &hitpos, damage, &force ); } if (scripting::hooks::OnAsteroidCollision->isActive() && !(asteroid_override && !weapon_override)) diff --git a/code/object/collideshipweapon.cpp b/code/object/collideshipweapon.cpp index 00f90367036..240eb281af2 100644 --- a/code/object/collideshipweapon.cpp +++ b/code/object/collideshipweapon.cpp @@ -82,7 +82,7 @@ static void ship_weapon_do_hit_stuff(object *pship_obj, object *weapon_obj, cons model_instance_local_to_global_dir(&worldNormal, hit_dir, pm, pmi, submodel_num, &pship_obj->orient); // Apply hit & damage & stuff to weapon - weapon_hit(weapon_obj, pship_obj, world_hitpos, quadrant_num, &worldNormal, hitpos, submodel_num); //NOLINT(readability-suspicious-call-argument) + weapon_hit(weapon_obj, pship_obj, world_hitpos, quadrant_num); //NOLINT(readability-suspicious-call-argument) if (wip->damage_time >= 0.0f && wp->lifeleft <= wip->damage_time) { if (wip->atten_damage >= 0.0f) { @@ -135,7 +135,7 @@ static void ship_weapon_do_hit_stuff(object *pship_obj, object *weapon_obj, cons } } - ship_apply_local_damage(pship_obj, weapon_obj, world_hitpos, damage, wip->damage_type_idx, quadrant_num, CREATE_SPARKS, submodel_num, nullptr, dot); + ship_apply_local_damage(pship_obj, weapon_obj, world_hitpos, damage, wip->damage_type_idx, quadrant_num, CREATE_SPARKS, submodel_num, &worldNormal, dot, hitpos); //NOLINT(readability-suspicious-call-argument) // let the hud shield gauge know when Player or Player target is hit hud_shield_quadrant_hit(pship_obj, quadrant_num); diff --git a/code/object/collideweaponweapon.cpp b/code/object/collideweaponweapon.cpp index cb31acfb1b3..a337f3084c6 100644 --- a/code/object/collideweaponweapon.cpp +++ b/code/object/collideweaponweapon.cpp @@ -139,9 +139,27 @@ int collide_weapon_weapon( obj_pair * pair ) if (wipB->weapon_hitpoints > 0) { // Two bombs collide, detonate both. if ((wipA->wi_flags[Weapon::Info_Flags::Bomb]) && (wipB->wi_flags[Weapon::Info_Flags::Bomb])) { wpA->weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(A, B, &A->pos, -1, nullptr); + std::array, NumHitTypes> impact_data_b = {}; + impact_data_b[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(wipB->armor_type_idx), + HitType::HULL, + aDamage, + B->hull_strength, + i2fl(wipB->weapon_hitpoints), + }; + bool a_armed = weapon_hit(A, B, &A->pos, -1); + maybe_play_conditional_impacts(impact_data_b, A, B, a_armed, -1, &A->pos); wpB->weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(B, A, &B->pos, -1, nullptr); + std::array, NumHitTypes> impact_data_a = {}; + impact_data_a[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(wipA->armor_type_idx), + HitType::HULL, + bDamage, + A->hull_strength, + i2fl(wipA->weapon_hitpoints), + }; + bool b_armed = weapon_hit(B, A, &B->pos, -1); + maybe_play_conditional_impacts(impact_data_a, B, A, b_armed, -1, &B->pos); } else { A->hull_strength -= bDamage; B->hull_strength -= aDamage; @@ -157,29 +175,83 @@ int collide_weapon_weapon( obj_pair * pair ) if (A->hull_strength < 0.0f) { wpA->weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(A, B, &A->pos, -1, nullptr); + std::array, NumHitTypes> impact_data_b = {}; + impact_data_b[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(wipB->armor_type_idx), + HitType::HULL, + aDamage, + B->hull_strength, + i2fl(wipB->weapon_hitpoints), + }; + bool a_armed = weapon_hit(A, B, &A->pos, -1); + maybe_play_conditional_impacts(impact_data_b, A, B, a_armed, -1, &A->pos); } if (B->hull_strength < 0.0f) { wpB->weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(B, A, &B->pos, -1, nullptr); + std::array, NumHitTypes> impact_data_a = {}; + impact_data_a[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(wipA->armor_type_idx), + HitType::HULL, + bDamage, + A->hull_strength, + i2fl(wipA->weapon_hitpoints), + }; + bool b_armed = weapon_hit(B, A, &B->pos, -1); + maybe_play_conditional_impacts(impact_data_a, B, A, b_armed, -1, &B->pos); } } } else { A->hull_strength -= bDamage; wpB->weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(B, A, &B->pos, -1, nullptr); + std::array, NumHitTypes> impact_data_a = {}; + impact_data_a[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(wipA->armor_type_idx), + HitType::HULL, + bDamage, + A->hull_strength, + i2fl(wipA->weapon_hitpoints), + }; + bool b_armed = weapon_hit(B, A, &B->pos, -1); + maybe_play_conditional_impacts(impact_data_a, B, A, b_armed, -1, &B->pos); if (A->hull_strength < 0.0f) { wpA->weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(A, B, &A->pos, -1, nullptr); + std::array, NumHitTypes> impact_data_b = {}; + impact_data_b[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(wipB->armor_type_idx), + HitType::HULL, + aDamage, + B->hull_strength, + i2fl(wipB->weapon_hitpoints), + }; + bool a_armed = weapon_hit(A, B, &A->pos, -1); + maybe_play_conditional_impacts(impact_data_b, A, B, a_armed, -1, &A->pos); } } } else if (wipB->weapon_hitpoints > 0) { B->hull_strength -= aDamage; wpA->weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(A, B, &A->pos, -1, nullptr); + std::array, NumHitTypes> impact_data_b = {}; + impact_data_b[0] = ConditionData { + ImpactCondition(wipB->armor_type_idx), + HitType::HULL, + aDamage, + B->hull_strength, + i2fl(wipB->weapon_hitpoints), + }; + bool a_armed = weapon_hit(A, B, &A->pos, -1); + maybe_play_conditional_impacts(impact_data_b, A, B, a_armed, -1, &A->pos); if (B->hull_strength < 0.0f) { wpB->weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(B, A, &B->pos, -1, nullptr); + std::array, NumHitTypes> impact_data_a = {}; + impact_data_a[0] = ConditionData { + ImpactCondition(wipA->armor_type_idx), + HitType::HULL, + bDamage, + A->hull_strength, + i2fl(wipA->weapon_hitpoints), + }; + bool b_armed = weapon_hit(B, A, &B->pos, -1); + maybe_play_conditional_impacts(impact_data_a, B, A, b_armed, -1, &B->pos); } } diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 39bf78bbd60..6d412278e0f 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -662,7 +662,7 @@ void do_subobj_heal_stuff(const object* ship_objp, const object* other_obj, cons // //WMC - hull_should_apply armor means that the initial subsystem had no armor, so the hull should apply armor instead. -float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3d *hitpos, int submodel_num, float damage, bool *hull_should_apply_armor, float hit_dot) +std::pair, float> do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3d *hitpos, int submodel_num, float damage, bool *hull_should_apply_armor, float hit_dot) { vec3d g_subobj_pos; float damage_left, damage_if_hull; @@ -687,10 +687,12 @@ float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3 ship_p = &Ships[ship_objp->instance]; + std::optional subsys_impact = std::nullopt; + // Don't damage player subsystems in a training mission. if ( The_mission.game_type & MISSION_TYPE_TRAINING ) { if (ship_objp == Player_obj){ - return damage; + return std::make_pair(subsys_impact, damage); } } @@ -700,7 +702,7 @@ float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3 // MK, 9/2/99. Shockwaves do zero subsystem damage on small ships. // Goober5000 - added back in via flag if ((Ship_info[ship_p->ship_info_index].is_small_ship()) && !(The_mission.ai_profile->flags[AI::Profile_Flags::Shockwaves_damage_small_ship_subsystems])) - return damage; + return std::make_pair(subsys_impact, damage); else { damage_left = shockwave_get_damage(other_obj->instance) / 4.0f; damage_if_hull = damage_left; @@ -719,7 +721,7 @@ float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3 weapon_info* wip = &Weapon_info[weapon_info_index]; if ( wip->wi_flags[Weapon::Info_Flags::Training] ) { - return damage_left; + return std::make_pair(subsys_impact, damage_left); } damage_left *= wip->subsystem_factor; @@ -731,7 +733,7 @@ float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3 if (Beams_use_damage_factors) { if ( wip->wi_flags[Weapon::Info_Flags::Training] ) { - return damage_left; + return std::make_pair(subsys_impact, damage_left); } damage_left *= wip->subsystem_factor; damage_if_hull *= wip->armor_factor; @@ -919,6 +921,7 @@ float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3 } } } + // HORRIBLE HACK! // MK, 9/4/99 // When Helios bombs are dual fired against the Juggernaut in sm3-01 (FS2), they often @@ -1000,6 +1003,16 @@ float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3 damage_to_apply *= ss_dif_scale; } + if (j == 0) { + subsys_impact = ConditionData { + ImpactCondition(subsystem->armor_type_idx), + HitType::SUBSYS, + damage_to_apply, + subsystem->current_hits, + subsystem->max_hits, + }; + } + subsystem->current_hits -= damage_to_apply; if (!(subsystem->flags[Ship::Subsystem_Flags::No_aggregate])) { ship_p->subsys_info[subsystem->system_info->type].aggregate_current_hits -= damage_to_apply; @@ -1037,7 +1050,7 @@ float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3 // It had taken a few MX-50s to destory an Anubis (with 40% hull), then it took maybe ten. // So, I left it alone. -- MK, 4/15/98 - return damage; + return std::make_pair(subsys_impact, damage); } // Store who/what killed the player, so we can tell the player how he died @@ -2292,7 +2305,7 @@ static int maybe_shockwave_damage_adjust(const object *ship_objp, const object * // Goober5000 - sanity checked this whole function in the case that other_obj is NULL, which // will happen with the explosion-effect sexp void ai_update_lethality(const object *ship_objp, const object *weapon_obj, float damage); -static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hitpos, float damage, int quadrant, int submodel_num, int damage_type_idx = -1, bool wash_damage = false, float hit_dot = 1.f) +static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hitpos, float damage, int quadrant, int submodel_num, int damage_type_idx = -1, bool wash_damage = false, float hit_dot = 1.f, const vec3d* hit_normal = nullptr, const vec3d* local_hitpos = nullptr) { // mprintf(("doing damage\n")); @@ -2397,6 +2410,7 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi } } + std::array, NumHitTypes> impact_data = {}; // If the ship is invulnerable, do nothing if (ship_objp->flags[Object::Object_Flags::Invulnerable]) { @@ -2405,6 +2419,26 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi // if ship is already dying, shorten deathroll. if (shipp->flags[Ship::Ship_Flags::Dying]) { + if (quadrant >= 0 && !(ship_objp->flags[Object::Object_Flags::No_shields])) { + impact_data[static_cast>(HitType::SHIELD)] = ConditionData { + ImpactCondition(shipp->shield_armor_type_idx), + HitType::SHIELD, + 0.0f, + // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last ten percent of a shield doesn't matter + MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp))), + shield_get_max_quad(ship_objp) - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp)), + }; + } else { + impact_data[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(shipp->armor_type_idx), + HitType::HULL, + 0.0f, + ship_objp->hull_strength, + shipp->ship_max_hull_strength, + }; + } + maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); + shiphit_hit_after_death(ship_objp, (damage * difficulty_scale_factor)); return; } @@ -2417,6 +2451,15 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi // mprintf(("applying damage ge to shield\n")); float shield_damage = damage * damage_scale; + auto shield_impact = ConditionData { + ImpactCondition(shipp->shield_armor_type_idx), + HitType::SHIELD, + 0.0f, + // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last ten percent of a shield doesn't matter + MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp))), + shield_get_max_quad(ship_objp) - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp)), + }; + if ( damage > 0.0f ) { float piercing_pct = 0.0f; @@ -2435,20 +2478,23 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi float shield_factor = 1.0f; if (weapon_info_index >= 0 && (!other_obj_is_beam || Beams_use_damage_factors)) shield_factor = Weapon_info[weapon_info_index].shield_factor; - + + shield_impact.damage = shield_damage * shield_factor; // apply shield damage float remaining_damage = shield_apply_damage(ship_objp, quadrant, shield_damage * shield_factor); // remove the shield factor, since the overflow will no longer be thrown at shields remaining_damage /= shield_factor; - + // Unless the backwards compatible flag is on, remove difficulty scaling as well // The hull/subsystem code below will re-add it where necessary if (!The_mission.ai_profile->flags[AI::Profile_Flags::Carry_shield_difficulty_scaling_bug]) - remaining_damage /= difficulty_scale_factor; - + remaining_damage /= difficulty_scale_factor; + // the rest of the damage is what overflowed from the shield damage and pierced damage = remaining_damage + (damage * piercing_pct); } + + impact_data[static_cast>(HitType::SHIELD)] = shield_impact; } // Apply leftover damage to the ship's subsystem and hull. @@ -2456,7 +2502,11 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi bool apply_hull_armor = true; // apply damage to subsystems, and get back any remaining damage that needs to go to the hull - damage = do_subobj_hit_stuff(ship_objp, other_obj, hitpos, submodel_num, damage, &apply_hull_armor, hit_dot); + auto damage_pair = do_subobj_hit_stuff(ship_objp, other_obj, hitpos, submodel_num, damage, &apply_hull_armor, hit_dot); + + damage = damage_pair.second; + + impact_data[static_cast>(HitType::SUBSYS)] = damage_pair.first; // damage scaling doesn't apply to subsystems, but it does to the hull damage *= damage_scale; @@ -2511,6 +2561,14 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi } } + impact_data[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(shipp->armor_type_idx), + HitType::HULL, + damage, + ship_objp->hull_strength, + shipp->ship_max_hull_strength, + }; + // multiplayer clients don't do damage if (((Game_mode & GM_MULTIPLAYER) && MULTIPLAYER_CLIENT)) { } else { @@ -2623,6 +2681,8 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi } } + maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); + // handle weapon and afterburner leeching here if(other_obj_is_weapon || other_obj_is_beam) { Assert(weapon_info_index >= 0); @@ -2788,7 +2848,7 @@ void ship_apply_tag(ship *shipp, int tag_level, float tag_time, object *target, // hitpos is in world coordinates. // if quadrant is not -1, then that part of the shield takes damage properly. // (it might be possible to make `other_obj` const, but that would set off another const-cascade) -void ship_apply_local_damage(object *ship_objp, object *other_obj, const vec3d *hitpos, float damage, int damage_type_idx, int quadrant, bool create_spark, int submodel_num, const vec3d *hit_normal, float hit_dot) +void ship_apply_local_damage(object *ship_objp, object *other_obj, const vec3d *hitpos, float damage, int damage_type_idx, int quadrant, bool create_spark, int submodel_num, const vec3d *hit_normal, float hit_dot, const vec3d* local_hitpos) { Assert(ship_objp); // Goober5000 Assert(other_obj); // Goober5000 @@ -2805,6 +2865,28 @@ void ship_apply_local_damage(object *ship_objp, object *other_obj, const vec3d * // Ie, player can always do damage. AI can only damage team if that ship is targeted. if (wp->target_num != OBJ_INDEX(ship_objp)) { if ((ship_p->team == wp->team) && !(Objects[other_obj->parent].flags[Object::Object_Flags::Player_ship]) ) { + // need to play the impact effect(s) for the weapon if we have one, since we won't get the chance to do it later + // we won't account for subsystems; that's a lot of extra logic for little benefit in this edge case + std::array, NumHitTypes> impact_data = {}; + if (quadrant >= 0 && !(ship_objp->flags[Object::Object_Flags::No_shields])) { + impact_data[static_cast>(HitType::SHIELD)] = ConditionData { + ImpactCondition(ship_p->shield_armor_type_idx), + HitType::SHIELD, + 0.0f, + // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last ten percent of a shield doesn't matter + MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp))), + shield_get_max_quad(ship_objp) - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp)), + }; + } else { + impact_data[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(ship_p->armor_type_idx), + HitType::HULL, + 0.0f, + ship_objp->hull_strength, + ship_p->ship_max_hull_strength, + }; + } + maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); return; } } @@ -2862,9 +2944,9 @@ void ship_apply_local_damage(object *ship_objp, object *other_obj, const vec3d * if (wip_index >= 0 && Weapon_info[wip_index].wi_flags[Weapon::Info_Flags::Heals]) { ship_do_healing(ship_objp, other_obj, hitpos, damage, submodel_num); create_sparks = false; + } else { + ship_do_damage(ship_objp, other_obj, hitpos, damage, quadrant, submodel_num, damage_type_idx, false, hit_dot, hit_normal, local_hitpos); } - else - ship_do_damage(ship_objp, other_obj, hitpos, damage, quadrant, submodel_num, damage_type_idx, false, hit_dot); // DA 5/5/98: move ship_hit_create_sparks() after do_damage() since number of sparks depends on hull strength // doesn't hit shield and we want sparks diff --git a/code/ship/shiphit.h b/code/ship/shiphit.h index bd225eee788..fed27dc1ebd 100644 --- a/code/ship/shiphit.h +++ b/code/ship/shiphit.h @@ -7,6 +7,7 @@ * */ +#include "weapon/weapon.h" #ifndef _SHIPHIT_H @@ -34,7 +35,7 @@ constexpr float DEATHROLL_ROTVEL_CAP = 6.3f; // maximum added deathroll rotve // function to destroy a subsystem. Called internally and from multiplayer messaging code extern void do_subobj_destroyed_stuff( ship *ship_p, ship_subsys *subsys, const vec3d *hitpos, bool no_explosion = false ); -float do_subobj_hit_stuff(object *ship_obj, const object *other_obj, const vec3d *hitpos, int submodel_num, float damage, bool *hull_should_apply_armor, float hit_dot = 1.f); +std::pair, float> do_subobj_hit_stuff(object *ship_obj, const object *other_obj, const vec3d *hitpos, int submodel_num, float damage, bool *hull_should_apply_armor, float hit_dot = 1.f); // Goober5000 // (it might be possible to make `target` const, but that would set off another const-cascade) @@ -45,7 +46,7 @@ extern void ship_apply_tag(ship *ship_p, int tag_level, float tag_time, object * // hitpos is in world coordinates. // if quadrant is not -1, then that part of the shield takes damage properly. // (it might be possible to make `other_obj` const, but that would set off another const-cascade) -void ship_apply_local_damage(object *ship_obj, object *other_obj, const vec3d *hitpos, float damage, int damage_type_idx, int quadrant, bool create_spark=true, int submodel_num=-1, const vec3d *hit_normal=nullptr, float hit_dot = 1.f); +void ship_apply_local_damage(object *ship_obj, object *other_obj, const vec3d *hitpos, float damage, int damage_type_idx, int quadrant, bool create_spark=true, int submodel_num=-1, const vec3d *hit_normal=nullptr, float hit_dot = 1.f, const vec3d* local_hitpos = nullptr); // This gets called to apply damage when a damaging force hits a ship, but at no // point in particular. Like from a shockwave. This routine will see if the diff --git a/code/weapon/beam.cpp b/code/weapon/beam.cpp index 770576dc07d..7c8c914b5a0 100644 --- a/code/weapon/beam.cpp +++ b/code/weapon/beam.cpp @@ -4072,12 +4072,14 @@ void beam_handle_collisions(beam *b) if (trgt->hull_strength < 0) { Weapons[trgt->instance].weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(trgt, NULL, &trgt->pos); + bool armed = weapon_hit(trgt, nullptr, &trgt->pos); + maybe_play_conditional_impacts({}, trgt, nullptr, armed, -1, &trgt->pos); } } else { if (!(Game_mode & GM_MULTIPLAYER) || MULTIPLAYER_MASTER) { Weapons[trgt->instance].weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(&Objects[target], NULL, &Objects[target].pos); + bool armed = weapon_hit(trgt, nullptr, &trgt->pos); + maybe_play_conditional_impacts({}, trgt, nullptr, armed, -1, &trgt->pos); } } @@ -4089,7 +4091,8 @@ void beam_handle_collisions(beam *b) if (!(Game_mode & GM_MULTIPLAYER) || MULTIPLAYER_MASTER) { Weapons[Objects[target].instance].weapon_flags.set(Weapon::Weapon_Flags::Destroyed_by_weapon); - weapon_hit(&Objects[target], NULL, &Objects[target].pos); + bool armed = weapon_hit(&Objects[target], nullptr, &Objects[target].pos); + maybe_play_conditional_impacts({}, &Objects[target], nullptr, armed, -1, &Objects[target].pos); } } break; diff --git a/code/weapon/weapon.h b/code/weapon/weapon.h index bf5582ce645..73bae6fecbb 100644 --- a/code/weapon/weapon.h +++ b/code/weapon/weapon.h @@ -80,6 +80,7 @@ constexpr int BANK_SWITCH_DELAY = 250; // after switching banks, 1/4 second dela // range. Check the comment in weapon_set_tracking_info() for more details #define LOCKED_HOMING_EXTENDED_LIFE_FACTOR 1.2f + struct homing_cache_info { TIMESTAMP next_update; vec3d expected_pos; @@ -291,13 +292,45 @@ enum class HomingAcquisitionType { RANDOM, }; +enum class HitType { + SHIELD, + SUBSYS, + HULL, + NONE, +}; + +constexpr size_t NumHitTypes = static_cast>(HitType::NONE); + +enum class SpecialImpactCondition { + DEBRIS, + ASTEROID, + EMPTY_SPACE, +}; + +using ImpactCondition = std::variant; + +struct ConditionData { + ImpactCondition condition = SpecialImpactCondition::EMPTY_SPACE; + HitType hit_type = HitType::NONE; + float damage = 0.0f; + float health = 1.0f; + float max_health = 1.0f; +}; + struct ConditionalImpact { particle::ParticleEffectHandle effect; - float min_health_threshold; //factor, 0-1 - float max_health_threshold; //factor, 0-1 - float min_angle_threshold; //in degrees - float max_angle_threshold; //in degrees + std::optional pokethrough_effect; + ::util::ParsedRandomFloatRange min_health_threshold; // factor, 0-1 + ::util::ParsedRandomFloatRange max_health_threshold; // factor, 0-1 + ::util::ParsedRandomFloatRange min_damage_hits_ratio; // factor + ::util::ParsedRandomFloatRange max_damage_hits_ratio; // factor + ::util::ParsedRandomFloatRange min_angle_threshold; // in degrees + ::util::ParsedRandomFloatRange max_angle_threshold; // in degrees + float laser_pokethrough_threshold; // factor, 0-1 bool dinky; + bool disable_if_player_parent; + bool disable_on_subsys_passthrough; + bool disable_main_on_pokethrough; }; enum class FiringPattern { @@ -513,7 +546,7 @@ struct weapon_info particle::ParticleEffectHandle piercing_impact_effect; particle::ParticleEffectHandle piercing_impact_secondary_effect; - SCP_map> conditional_impacts; + SCP_map> conditional_impacts; particle::ParticleEffectHandle muzzle_effect; @@ -962,7 +995,8 @@ void weapon_set_tracking_info(int weapon_objnum, int parent_objnum, int target_o size_t* get_pointer_to_weapon_fire_pattern_index(int weapon_type, int ship_idx, ship_subsys* src_turret); bool weapon_armed(weapon *wp, bool hit_target); -void weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, int quadrant = -1, const vec3d* hitnormal = nullptr, const vec3d* local_hitpos = nullptr, int submodel = -1 ); +void maybe_play_conditional_impacts(const std::array, NumHitTypes>& impact_data, const object* weapon_objp, const object* impacted_objp, bool armed_weapon, int submodel, const vec3d* hitpos, const vec3d* local_hitpos = nullptr, const vec3d* hit_normal = nullptr); +bool weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, int quadrant = -1 ); void spawn_child_weapons( object *objp, int spawn_index_override = -1); // call to detonate a weapon. essentially calls weapon_hit() with other_obj as NULL, and sends a packet in multiplayer diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index b19b99becdd..185f1110185 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -2380,41 +2380,79 @@ int parse_weapon(int subtype, bool replace, const char *filename) } while (optional_string("$Conditional Impact:")) { - int armor_index; + ImpactCondition impact_condition; ConditionalImpact ci; - ci.min_health_threshold = std::numeric_limits::lowest(); - ci.max_health_threshold = std::numeric_limits::max(); - ci.min_angle_threshold = 0.f; - ci.max_angle_threshold = 180.f; + ci.min_health_threshold = ::util::UniformFloatRange(std::numeric_limits::lowest()); + ci.max_health_threshold = ::util::UniformFloatRange(std::numeric_limits::max()); + ci.min_damage_hits_ratio = ::util::UniformFloatRange(std::numeric_limits::lowest()); + ci.max_damage_hits_ratio = ::util::UniformFloatRange(std::numeric_limits::max()); + ci.min_angle_threshold = ::util::UniformFloatRange(0.f); + ci.max_angle_threshold = ::util::UniformFloatRange(180.f); + ci.laser_pokethrough_threshold = 0.1f; ci.dinky = false; + ci.disable_if_player_parent = false; + ci.disable_on_subsys_passthrough = false; + ci.disable_main_on_pokethrough = false; bool invalid_armor = false; - required_string("+Armor Type:"); + if (optional_string("+Armor Type:")) { stuff_string(fname, F_NAME, NAME_LENGTH); - if (!stricmp(fname, "NO ARMOR")) { - armor_index = -1; - } else { - armor_index = armor_type_get_idx(fname); - if (armor_index < 0) { - Warning(LOCATION, "Armor type '%s' not found for conditional impact in weapon %s!", fname, wip->name); - invalid_armor = true; - } - }; - parse_optional_float_into("+Min Health Threshold:", &ci.min_health_threshold); - parse_optional_float_into("+Max Health Threshold:", &ci.max_health_threshold); - parse_optional_float_into("+Min Angle Threshold:", &ci.min_angle_threshold); - parse_optional_float_into("+Max Angle Threshold:", &ci.max_angle_threshold); + if (!stricmp(fname, "NO ARMOR")) { + impact_condition = -1; + } else { + impact_condition = armor_type_get_idx(fname); + if (std::holds_alternative(impact_condition) && std::get(impact_condition) < 0) { + Warning(LOCATION, "Armor type '%s' not found for conditional impact in weapon %s!", fname, wip->name); + invalid_armor = true; + } + }; + } else if (optional_string("+Asteroid")) { + impact_condition = SpecialImpactCondition::ASTEROID; + } else if (optional_string("+Debris")) { + impact_condition = SpecialImpactCondition::DEBRIS; + } else if (optional_string("+Empty Space")) { + impact_condition = SpecialImpactCondition::EMPTY_SPACE; + } + if (optional_string("+Min Health Threshold:")) { + ci.min_health_threshold = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + if (optional_string("+Max Health Threshold:")) { + ci.max_health_threshold = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + if (optional_string("+Min Damage/Hitpoints Ratio:")) { + ci.min_damage_hits_ratio = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + if (optional_string("+Max Damage/Hitpoints Ratio:")) { + ci.max_damage_hits_ratio = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + if (optional_string("+Min Angle Threshold:")) { + ci.min_angle_threshold = ::util::ParsedRandomFloatRange::parseRandomRange(); + } + if (optional_string("+Max Angle Threshold:")) { + ci.max_angle_threshold = ::util::ParsedRandomFloatRange::parseRandomRange(); + } parse_optional_bool_into("+Dinky:", &ci.dinky); + parse_optional_bool_into("+Disable If Player Parent:", &ci.disable_if_player_parent); + parse_optional_bool_into("+Disable On Subsystem Passthrough:", &ci.disable_on_subsys_passthrough); + required_string("+Effect Name:"); ci.effect = particle::util::parseEffect(wip->name); + if (optional_string("+Laser Pokethrough Effect Name:")) { + ci.pokethrough_effect = particle::util::parseEffect(wip->name); + if (optional_string("+Laser Pokethrough Threshold:")) { + stuff_float(&ci.laser_pokethrough_threshold); + } + parse_optional_bool_into("+Disable Main On Pokethrough:", &ci.disable_main_on_pokethrough); + } + SCP_vector ci_vec; - if (wip->conditional_impacts.count(armor_index) == 1) { - SCP_vector existing_cis = wip->conditional_impacts[armor_index]; + if (wip->conditional_impacts.count(impact_condition) == 1) { + SCP_vector existing_cis = wip->conditional_impacts[impact_condition]; ci_vec.insert(ci_vec.end(), existing_cis.begin(), existing_cis.end()); } ci_vec.push_back(ci); if (!invalid_armor) { - wip->conditional_impacts[armor_index] = ci_vec; + wip->conditional_impacts[impact_condition] = ci_vec; } } @@ -7733,7 +7771,8 @@ bool weapon_armed(weapon *wp, bool hit_target) static std::unique_ptr weapon_hit_make_effect_host(const object* weapon_obj, const object* impacted_obj, int impacted_submodel, const vec3d* hitpos, const vec3d* local_hitpos) { if (impacted_obj == nullptr || impacted_obj->type != OBJ_SHIP || local_hitpos == nullptr) { //Fall back to Vector. Since we don't have a ship, it's quite likely whatever we're hitting will immediately die, so don't try to attach a particle source. - auto vector_host = std::make_unique(*hitpos, weapon_obj->last_orient, weapon_obj->phys_info.vel); + vec3d vel = impacted_obj == nullptr ? weapon_obj->phys_info.vel : impacted_obj->phys_info.vel; + auto vector_host = std::make_unique(*hitpos, weapon_obj->last_orient, vel); vector_host->setRadius(impacted_obj == nullptr ? weapon_obj->radius : impacted_obj->radius); return vector_host; } @@ -7755,133 +7794,166 @@ static std::unique_ptr weapon_hit_make_effect_host(const object* wea } } -/** - * Called when a weapon hits something (or, in the case of - * missiles explodes for any particular reason) - */ -void weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, int quadrant, const vec3d* hitnormal, const vec3d* local_hitpos, int submodel) -{ - Assert(weapon_obj != NULL); - if(weapon_obj == NULL){ - return; - } - Assert((weapon_obj->type == OBJ_WEAPON) && (weapon_obj->instance >= 0) && (weapon_obj->instance < MAX_WEAPONS)); - if((weapon_obj->type != OBJ_WEAPON) || (weapon_obj->instance < 0) || (weapon_obj->instance >= MAX_WEAPONS)){ - return; - } - - int num = weapon_obj->instance; - int weapon_type = Weapons[num].weapon_info_index; - weapon_info *wip; - weapon *wp; - bool hit_target = false; +void process_conditional_impact( + const ConditionData& entry, + const object* weapon_objp, + const weapon_info* wip, + const object* impacted_objp, + bool armed_weapon, + bool subsys_hit, + int submodel, + const vec3d* hitpos, + const vec3d* local_hitpos, + const vec3d* hitnormal, + float hit_angle, + float radius_mult, + float laser_pokethrough_amount, + const vec3d* laser_head_pos, + bool* valid_conditional_impact +) { + auto conditional_impact_it = wip->conditional_impacts.find(entry.condition); + if (conditional_impact_it != wip->conditional_impacts.end()) { + float health_fraction = entry.health / entry.max_health; + float damage_hits_fraction = entry.damage / entry.health; + for (const auto& ci : conditional_impact_it->second) { + if (((!armed_weapon) == ci.dinky) + && (!ci.disable_if_player_parent || (&Objects[weapon_objp->parent] != Player_obj)) + && ((entry.hit_type != HitType::HULL || !subsys_hit) || !ci.disable_on_subsys_passthrough) + && health_fraction >= ci.min_health_threshold.next() + && health_fraction <= ci.max_health_threshold.next() + && damage_hits_fraction >= ci.min_damage_hits_ratio.next() + && damage_hits_fraction <= ci.max_damage_hits_ratio.next() + && hit_angle >= fl_radians(ci.min_angle_threshold.next()) + && hit_angle <= fl_radians(ci.max_angle_threshold.next()) + ) { + bool pokethrough = (laser_pokethrough_amount >= ci.laser_pokethrough_threshold && ci.pokethrough_effect && wip->render_type == WRT_LASER); + + if (!(pokethrough && ci.disable_main_on_pokethrough)) { + auto particleSource = particle::ParticleManager::get()->createSource(ci.effect); + particleSource->setHost(weapon_hit_make_effect_host(weapon_objp, impacted_objp, submodel, hitpos, local_hitpos)); + particleSource->setTriggerRadius(weapon_objp->radius * radius_mult); + particleSource->setTriggerVelocity(vm_vec_mag_quick(&weapon_objp->phys_info.vel)); + if (hitnormal) { + particleSource->setNormal(*hitnormal); + } + particleSource->finishCreation(); + } - ship *shipp; - weapon *target_wp; - weapon_info *target_wip; - int objnum; + if (pokethrough) { + auto pokethroughParticleSource = particle::ParticleManager::get()->createSource(ci.pokethrough_effect.value()); + pokethroughParticleSource->setHost(weapon_hit_make_effect_host(weapon_objp, nullptr, submodel, laser_head_pos, nullptr)); + pokethroughParticleSource->setTriggerRadius(weapon_objp->radius * radius_mult); + pokethroughParticleSource->setTriggerVelocity(vm_vec_mag_quick(&weapon_objp->phys_info.vel)); + if (hitnormal) { + pokethroughParticleSource->setNormal(*hitnormal); + } + pokethroughParticleSource->finishCreation(); + } - Assert((weapon_type >= 0) && (weapon_type < weapon_info_size())); - if ((weapon_type < 0) || (weapon_type >= weapon_info_size())) { - return; + *valid_conditional_impact = true; + } + } } - wp = &Weapons[weapon_obj->instance]; - wip = &Weapon_info[weapon_type]; - objnum = wp->objnum; +} - if (scripting::hooks::OnMissileDeathStarted->isActive() && wip->subtype == WP_MISSILE) { - // analagous to On Ship Death Started - scripting::hooks::OnMissileDeathStarted->run(scripting::hooks::WeaponDeathConditions{ wp }, - scripting::hook_param_list( - scripting::hook_param("Weapon", 'o', weapon_obj), - scripting::hook_param("Object", 'o', impacted_obj))); +void maybe_play_conditional_impacts(const std::array, NumHitTypes>& impact_data, const object* weapon_objp, const object* impacted_objp, bool armed_weapon, int submodel, const vec3d* hitpos, const vec3d* local_hitpos, const vec3d* hitnormal) { + if (weapon_objp == nullptr || weapon_objp->type != OBJ_WEAPON) { + return; } - - // check if the weapon actually hit the intended target - if (weapon_has_homing_object(wp)) - if (wp->homing_object == impacted_obj) - hit_target = true; - - //This is an expensive check - bool armed_weapon = weapon_armed(&Weapons[num], hit_target); - - // if this is the player ship, and is a laser hit, skip it. wait for player "pain" to take care of it - if ((impacted_obj != Player_obj) || (wip->subtype != WP_LASER) || !MULTIPLAYER_CLIENT) { // NOLINT(readability-simplify-boolean-expr) - weapon_hit_do_sound(impacted_obj, wip, hitpos, armed_weapon, quadrant); + auto wp = &Weapons[weapon_objp->instance]; + auto wip = &Weapon_info[wp->weapon_info_index]; + ship* shipp = nullptr; + if (impacted_objp != nullptr && impacted_objp->type == OBJ_SHIP) { + shipp = &Ships[impacted_objp->instance]; } - - bool valid_conditional_impact = false; - int relevant_armor_idx = -1; - float relevant_fraction = 1.0f; float hit_angle = 0.0f; - vec3d reverse_incoming = weapon_obj->orient.vec.fvec; + vec3d reverse_incoming = weapon_objp->orient.vec.fvec; vm_vec_negate(&reverse_incoming); + if (hitnormal) { + hit_angle = vm_vec_delta_ang(hitnormal, &reverse_incoming, nullptr); + } - float radius_mult = 1.f; - + float radius_mult = 1.0f; if (wip->render_type == WRT_LASER) { radius_mult = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_RADIUS_MULT, *wp, &wp->modular_curves_instance); } - if (hitnormal) { - hit_angle = vm_vec_delta_ang(hitnormal, &reverse_incoming, nullptr); - } + float laser_pokethrough_amount = 0.0f; + vec3d laser_head_pos = vmd_zero_vector; + if (wip->render_type == WRT_LASER) { + float length_mult = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_LENGTH_MULT, *wp, &wp->modular_curves_instance); - if (!wip->conditional_impacts.empty() && impacted_obj != nullptr) { - switch (impacted_obj->type) { - case OBJ_SHIP: - shipp = &Ships[impacted_obj->instance]; - if (quadrant == -1) { - relevant_armor_idx = shipp->armor_type_idx; - relevant_fraction = impacted_obj->hull_strength / i2fl(shipp->ship_max_hull_strength); - } else { - relevant_armor_idx = shipp->shield_armor_type_idx; - relevant_fraction = ship_quadrant_shield_strength(impacted_obj, quadrant); - } - break; - case OBJ_WEAPON: - target_wp = &Weapons[impacted_obj->instance]; - target_wip = &Weapon_info[target_wp->weapon_info_index]; - relevant_armor_idx = target_wip->armor_type_idx; - relevant_fraction = impacted_obj->hull_strength / i2fl(target_wip->weapon_hitpoints); - break; - default: - break; + if (wip->laser_length_by_frametime) { + length_mult *= flFrametime; } - - if (wip->conditional_impacts.count(relevant_armor_idx) == 1) { - for (const auto& ci : wip->conditional_impacts[relevant_armor_idx]) { - if (((!armed_weapon) == ci.dinky) - && relevant_fraction >= ci.min_health_threshold - && relevant_fraction <= ci.max_health_threshold - && hit_angle >= fl_radians(ci.min_angle_threshold) - && hit_angle <= fl_radians(ci.max_angle_threshold) - ) { - auto particleSource = particle::ParticleManager::get()->createSource(ci.effect); - particleSource->setHost(weapon_hit_make_effect_host(weapon_obj, impacted_obj, submodel, hitpos, local_hitpos)); - particleSource->setTriggerRadius(weapon_obj->radius * radius_mult); - particleSource->setTriggerVelocity(vm_vec_mag_quick(&weapon_obj->phys_info.vel)); - if (hitnormal) - { - particleSource->setNormal(*hitnormal); - } - particleSource->finishCreation(); + float laser_length = wip->laser_length * length_mult; - valid_conditional_impact = true; - } - } - } + vm_vec_scale_add(&laser_head_pos, &weapon_objp->last_pos, &weapon_objp->orient.vec.fvec, laser_length); + laser_pokethrough_amount = vm_vec_dist_quick(&laser_head_pos, hitpos) / laser_length; } + bool subsys_hit = impact_data[static_cast>(HitType::SUBSYS)].has_value(); + bool valid_conditional_impact = false; + for (const auto& entry : impact_data) { + if (!entry.has_value()) { + continue; + } + process_conditional_impact( + *entry, + weapon_objp, + wip, + impacted_objp, + armed_weapon, + subsys_hit, + submodel, + hitpos, + local_hitpos, + hitnormal, + hit_angle, + radius_mult, + laser_pokethrough_amount, + &laser_head_pos, + &valid_conditional_impact + ); + } + + // check for empty space impacts + if (impacted_objp == nullptr) { + auto space_entry = ConditionData { + SpecialImpactCondition::EMPTY_SPACE, + HitType::NONE, + 0.0f, + 1.0f, + 1.0f, + }; + process_conditional_impact( + space_entry, + weapon_objp, + wip, + impacted_objp, + armed_weapon, + subsys_hit, + submodel, + hitpos, + local_hitpos, + hitnormal, + hit_angle, + radius_mult, + laser_pokethrough_amount, + &laser_head_pos, + &valid_conditional_impact + ); + } + if (!valid_conditional_impact && wip->impact_weapon_expl_effect.isValid() && armed_weapon) { auto particleSource = particle::ParticleManager::get()->createSource(wip->impact_weapon_expl_effect); - particleSource->setHost(weapon_hit_make_effect_host(weapon_obj, impacted_obj, submodel, hitpos, local_hitpos)); - particleSource->setTriggerRadius(weapon_obj->radius * radius_mult); - particleSource->setTriggerVelocity(vm_vec_mag_quick(&weapon_obj->phys_info.vel)); + particleSource->setHost(weapon_hit_make_effect_host(weapon_objp, impacted_objp, submodel, hitpos, local_hitpos)); + particleSource->setTriggerRadius(weapon_objp->radius * radius_mult); + particleSource->setTriggerVelocity(vm_vec_mag_quick(&weapon_objp->phys_info.vel)); if (hitnormal) { @@ -7890,9 +7962,9 @@ void weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, particleSource->finishCreation(); } else if (!valid_conditional_impact && wip->dinky_impact_weapon_expl_effect.isValid() && !armed_weapon) { auto particleSource = particle::ParticleManager::get()->createSource(wip->dinky_impact_weapon_expl_effect); - particleSource->setHost(weapon_hit_make_effect_host(weapon_obj, impacted_obj, submodel, hitpos, local_hitpos)); - particleSource->setTriggerRadius(weapon_obj->radius * radius_mult); - particleSource->setTriggerVelocity(vm_vec_mag_quick(&weapon_obj->phys_info.vel)); + particleSource->setHost(weapon_hit_make_effect_host(weapon_objp, impacted_objp, submodel, hitpos, local_hitpos)); + particleSource->setTriggerRadius(weapon_objp->radius * radius_mult); + particleSource->setTriggerVelocity(vm_vec_mag_quick(&weapon_objp->phys_info.vel)); if (hitnormal) { @@ -7901,18 +7973,16 @@ void weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, particleSource->finishCreation(); } - if ((impacted_obj != nullptr) && (quadrant == -1) && (!valid_conditional_impact && wip->piercing_impact_effect.isValid() && armed_weapon)) { - if ((impacted_obj->type == OBJ_SHIP) || (impacted_obj->type == OBJ_DEBRIS)) { + if (impacted_objp != nullptr && impact_data[static_cast>(HitType::SHIELD)].has_value() && (!valid_conditional_impact && wip->piercing_impact_effect.isValid() && armed_weapon)) { + if ((impacted_objp->type == OBJ_SHIP) || (impacted_objp->type == OBJ_DEBRIS)) { int ok_to_draw = 1; - if (impacted_obj->type == OBJ_SHIP) { + if (impacted_objp->type == OBJ_SHIP) { float draw_limit, hull_pct; int dmg_type_idx, piercing_type; - shipp = &Ships[impacted_obj->instance]; - - hull_pct = impacted_obj->hull_strength / shipp->ship_max_hull_strength; + hull_pct = impacted_objp->hull_strength / shipp->ship_max_hull_strength; dmg_type_idx = wip->damage_type_idx; draw_limit = Ship_info[shipp->ship_info_index].piercing_damage_draw_limit; @@ -7933,9 +8003,9 @@ void weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, using namespace particle; auto primarySource = ParticleManager::get()->createSource(wip->piercing_impact_effect); - primarySource->setHost(weapon_hit_make_effect_host(weapon_obj, impacted_obj, submodel, hitpos, local_hitpos)); - primarySource->setTriggerRadius(weapon_obj->radius * radius_mult); - primarySource->setTriggerVelocity(vm_vec_mag_quick(&weapon_obj->phys_info.vel)); + primarySource->setHost(weapon_hit_make_effect_host(weapon_objp, impacted_objp, submodel, hitpos, local_hitpos)); + primarySource->setTriggerRadius(weapon_objp->radius * radius_mult); + primarySource->setTriggerVelocity(vm_vec_mag_quick(&weapon_objp->phys_info.vel)); if (hitnormal) { @@ -7945,9 +8015,9 @@ void weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, if (wip->piercing_impact_secondary_effect.isValid()) { auto secondarySource = ParticleManager::get()->createSource(wip->piercing_impact_secondary_effect); - secondarySource->setHost(weapon_hit_make_effect_host(weapon_obj, impacted_obj, submodel, hitpos, local_hitpos)); - secondarySource->setTriggerRadius(weapon_obj->radius * radius_mult); - secondarySource->setTriggerVelocity(vm_vec_mag_quick(&weapon_obj->phys_info.vel)); + secondarySource->setHost(weapon_hit_make_effect_host(weapon_objp, impacted_objp, submodel, hitpos, local_hitpos)); + secondarySource->setTriggerRadius(weapon_objp->radius * radius_mult); + secondarySource->setTriggerVelocity(vm_vec_mag_quick(&weapon_objp->phys_info.vel)); if (hitnormal) { @@ -7958,6 +8028,61 @@ void weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, } } } +} + +/** + * Called when a weapon hits something (or, in the case of + * missiles explodes for any particular reason) + * Returns true if weapon is armed, false otherwise + */ +bool weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, int quadrant) +{ + Assert(weapon_obj != nullptr); + if(weapon_obj == nullptr){ + return false; + } + Assert((weapon_obj->type == OBJ_WEAPON) && (weapon_obj->instance >= 0) && (weapon_obj->instance < MAX_WEAPONS)); + if((weapon_obj->type != OBJ_WEAPON) || (weapon_obj->instance < 0) || (weapon_obj->instance >= MAX_WEAPONS)){ + return false; + } + + int num = weapon_obj->instance; + int weapon_type = Weapons[num].weapon_info_index; + weapon_info *wip; + weapon *wp; + bool hit_target = false; + + ship *shipp; + int objnum; + + Assert((weapon_type >= 0) && (weapon_type < weapon_info_size())); + if ((weapon_type < 0) || (weapon_type >= weapon_info_size())) { + return false; + } + wp = &Weapons[weapon_obj->instance]; + wip = &Weapon_info[weapon_type]; + objnum = wp->objnum; + + if (scripting::hooks::OnMissileDeathStarted->isActive() && wip->subtype == WP_MISSILE) { + // analagous to On Ship Death Started + scripting::hooks::OnMissileDeathStarted->run(scripting::hooks::WeaponDeathConditions{ wp }, + scripting::hook_param_list( + scripting::hook_param("Weapon", 'o', weapon_obj), + scripting::hook_param("Object", 'o', impacted_obj))); + } + + // check if the weapon actually hit the intended target + if (weapon_has_homing_object(wp)) + if (wp->homing_object == impacted_obj) + hit_target = true; + + //This is an expensive check + bool armed_weapon = weapon_armed(&Weapons[num], hit_target); + + // if this is the player ship, and is a laser hit, skip it. wait for player "pain" to take care of it + if ((impacted_obj != Player_obj) || (wip->subtype != WP_LASER) || !MULTIPLAYER_CLIENT) { // NOLINT(readability-simplify-boolean-expr) + weapon_hit_do_sound(impacted_obj, wip, hitpos, armed_weapon, quadrant); + } //Set shockwaves flag int sw_flag = SW_WEAPON; @@ -8006,7 +8131,7 @@ void weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, //No impacted_obj means this weapon detonates if (wip->pierce_objects && impacted_obj && impacted_obj->type != OBJ_WEAPON) - return; + return armed_weapon; // For all objects that had this weapon as a target, wipe it out, forcing find of a new enemy for ( auto so = GET_FIRST(&Ship_obj_list); so != END_OF_LIST(&Ship_obj_list); so = GET_NEXT(so) ) { @@ -8057,6 +8182,7 @@ void weapon_hit( object* weapon_obj, object* impacted_obj, const vec3d* hitpos, if ( parent->type == OBJ_SHIP && parent->signature == weapon_obj->parent_sig) Ships[Objects[weapon_obj->parent].instance].weapons.remote_detonaters_active--; } + return armed_weapon; } void weapon_detonate(object *objp) @@ -8078,9 +8204,11 @@ void weapon_detonate(object *objp) // call weapon hit // Wanderer - use last frame pos for the corkscrew missiles if ( (Weapon_info[Weapons[objp->instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Corkscrew]) ) { - weapon_hit(objp, NULL, &objp->last_pos); + bool armed = weapon_hit(objp, nullptr, &objp->last_pos); + maybe_play_conditional_impacts({}, objp, nullptr, armed, -1, &objp->pos); } else { - weapon_hit(objp, NULL, &objp->pos); + bool armed = weapon_hit(objp, nullptr, &objp->pos); + maybe_play_conditional_impacts({}, objp, nullptr, armed, -1, &objp->pos); } } From bdde909fa6acc32dbfff9ea3085ee1c67e1edacd Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Sat, 12 Jul 2025 11:41:30 +0200 Subject: [PATCH 243/466] Threading Framework & Threaded Collisions (#6814) * Dispatch ship weapon collisions and defer handling * multithread ship weapon collision * properly spin * proper memory order * externalize generic threadpool settings * Wait for threads to shut down * Add result pipelining * cleanup * incorrect nullopt type * randomrange todo * parallelize ship ship collisions * bogus debug return * remove warnings * clang tidy --- code/cmdline/cmdline.cpp | 10 + code/cmdline/cmdline.h | 1 + code/model/modelcollide.cpp | 26 +- code/object/collideshipship.cpp | 665 ++++++++++++++++-------------- code/object/collideshipweapon.cpp | 218 +++++++--- code/object/objcollide.cpp | 183 +++++++- code/object/objcollide.h | 15 +- code/ship/shield.cpp | 2 +- code/ship/ship.h | 2 +- code/source_groups.cmake | 2 + code/utils/RandomRange.h | 6 +- code/utils/threading.cpp | 82 ++++ code/utils/threading.h | 19 + freespace2/freespace.cpp | 8 +- 14 files changed, 827 insertions(+), 412 deletions(-) create mode 100644 code/utils/threading.cpp create mode 100644 code/utils/threading.h diff --git a/code/cmdline/cmdline.cpp b/code/cmdline/cmdline.cpp index e56a4f736a8..dcdbe0c8abf 100644 --- a/code/cmdline/cmdline.cpp +++ b/code/cmdline/cmdline.cpp @@ -534,6 +534,7 @@ cmdline_parm luadev_arg("-luadev", "Make lua errors non-fatal", AT_NONE); // Cmd cmdline_parm override_arg("-override_data", "Enable override directory", AT_NONE); // Cmdline_override_data cmdline_parm imgui_debug_arg("-imgui_debug", nullptr, AT_NONE); cmdline_parm vulkan("-vulkan", nullptr, AT_NONE); +cmdline_parm multithreading("-threads", nullptr, AT_INT); char *Cmdline_start_mission = NULL; int Cmdline_dis_collisions = 0; @@ -572,6 +573,7 @@ bool Cmdline_lua_devmode = false; bool Cmdline_override_data = false; bool Cmdline_show_imgui_debug = false; bool Cmdline_vulkan = false; +int Cmdline_multithreading = 1; // Other cmdline_parm get_flags_arg(GET_FLAGS_STRING, "Output the launcher flags file", AT_STRING); @@ -2407,6 +2409,14 @@ bool SetCmdlineParams() } } + if (multithreading.found()) { + Cmdline_multithreading = abs(multithreading.get_int()); + if (Cmdline_multithreading < 1) { + Cmdline_multithreading = 1; + Warning(LOCATION,"-threads must be an integer greater or equal to 1. Invalid thread count will be disregarded."); + } + } + return true; } diff --git a/code/cmdline/cmdline.h b/code/cmdline/cmdline.h index 89a9491e699..72f0c2591a5 100644 --- a/code/cmdline/cmdline.h +++ b/code/cmdline/cmdline.h @@ -159,6 +159,7 @@ extern bool Cmdline_lua_devmode; extern bool Cmdline_override_data; extern bool Cmdline_show_imgui_debug; extern bool Cmdline_vulkan; +extern int Cmdline_multithreading; enum class WeaponSpewType { NONE = 0, STANDARD, ALL }; extern WeaponSpewType Cmdline_spew_weapon_stats; diff --git a/code/model/modelcollide.cpp b/code/model/modelcollide.cpp index e95e938414f..ad7dfed84f3 100644 --- a/code/model/modelcollide.cpp +++ b/code/model/modelcollide.cpp @@ -30,25 +30,24 @@ // checking a collision rather than passing a bunch of parameters around. These are // not persistant between calls to model_collide -static mc_info *Mc; // The mc_info passed into model_collide - -static polymodel *Mc_pm; // The polygon model we're checking -static int Mc_submodel; // The current submodel we're checking +thread_local static mc_info *Mc; // The mc_info passed into model_collide + +thread_local static polymodel *Mc_pm; // The polygon model we're checking +thread_local static int Mc_submodel; // The current submodel we're checking -static polymodel_instance *Mc_pmi; +thread_local static polymodel_instance *Mc_pmi; -static matrix Mc_orient; // A matrix to rotate a world point into the current +thread_local static matrix Mc_orient; // A matrix to rotate a world point into the current // submodel's frame of reference. -static vec3d Mc_base; // A point used along with Mc_orient. +thread_local static vec3d Mc_base; // A point used along with Mc_orient. -static vec3d Mc_p0; // The ray origin rotated into the current submodel's frame of reference -static vec3d Mc_p1; // The ray end rotated into the current submodel's frame of reference -static float Mc_mag; // The length of the ray -static vec3d Mc_direction; // A vector from the ray's origin to its end, in the current submodel's frame of reference +thread_local static vec3d Mc_p0; // The ray origin rotated into the current submodel's frame of reference +thread_local static vec3d Mc_p1; // The ray end rotated into the current submodel's frame of reference +thread_local static float Mc_mag; // The length of the ray +thread_local static vec3d Mc_direction; // A vector from the ray's origin to its end, in the current submodel's frame of reference -static vec3d **Mc_point_list = NULL; // A pointer to the current submodel's vertex list +thread_local static vec3d **Mc_point_list = nullptr; // A pointer to the current submodel's vertex list -static float Mc_edge_time; void model_collide_free_point_list() @@ -1135,7 +1134,6 @@ int model_collide(mc_info *mc_info_obj) Mc_orient = *Mc->orient; Mc_base = *Mc->pos; Mc_mag = vm_vec_dist( Mc->p0, Mc->p1 ); - Mc_edge_time = FLT_MAX; if ( Mc->model_instance_num >= 0 ) { Mc_pmi = model_get_instance(Mc->model_instance_num); diff --git a/code/object/collideshipship.cpp b/code/object/collideshipship.cpp index 14896f6d350..1cca7872e32 100644 --- a/code/object/collideshipship.cpp +++ b/code/object/collideshipship.cpp @@ -867,11 +867,15 @@ extern void hud_start_text_flash(char *txt, int t, int interval); * Procss player_ship:planet damage. * If within range of planet, apply damage to ship. */ -static void mcp_1(object *player_objp, object *planet_objp) +static void mcp_1(obj_pair * pair, const std::any& data) { float planet_radius; float dist; + bool ship_is_first = std::any_cast(data); + object* planet_objp = ship_is_first ? pair->b : pair->a; + object* player_objp = ship_is_first ? pair->a : pair->b; + planet_radius = planet_objp->radius; dist = vm_vec_dist_quick(&player_objp->pos, &planet_objp->pos); @@ -900,9 +904,9 @@ static int is_planet(object *objp) /** * If exactly one of these is a planet and the other is a player ship, do something special. - * @return true if this was a ship:planet (or planet_ship) collision and we processed it. Else return false. + * @return true if this was a ship:planet (or planet_ship) collision (planet involved / bool is ship first) and we processed it. Else return false. */ -static int maybe_collide_planet (object *obj1, object *obj2) +static std::pair maybe_collide_planet (object *obj1, object *obj2) { ship_info *sip1, *sip2; @@ -911,17 +915,15 @@ static int maybe_collide_planet (object *obj1, object *obj2) if (sip1->flags[Ship::Info_Flags::Player_ship]) { if (is_planet(obj2)) { - mcp_1(obj1, obj2); - return 1; + return {true, true}; } } else if (sip2->flags[Ship::Info_Flags::Player_ship]) { if (is_planet(obj1)) { - mcp_1(obj2, obj1); - return 1; + return {true, false}; } } - return 0; + return {false, false}; } /** @@ -1100,31 +1102,340 @@ static void maybe_push_little_ship_from_fast_big_ship(object *big_obj, object *s } } +void collide_ship_ship_process(obj_pair * pair, const std::any& collision_data) { + auto ship_ship_hit_info = std::any_cast(collision_data); + + object *A = pair->a; + object *B = pair->b; + + bool a_override = false, b_override = false; + + // get world hitpos - do it here in case the override hooks need it + vec3d world_hit_pos; + vm_vec_add(&world_hit_pos, &ship_ship_hit_info.heavy->pos, &ship_ship_hit_info.hit_pos); + + // get submodel handle if scripting needs it + bool has_submodel = (ship_ship_hit_info.heavy_submodel_num >= 0); + scripting::api::submodel_h smh(ship_ship_hit_info.heavy_model_num, ship_ship_hit_info.heavy_submodel_num); + + if (scripting::hooks::OnShipCollision->isActive()) { + a_override = scripting::hooks::OnShipCollision->isOverride(scripting::hooks::CollisionConditions{ {A, B} }, + scripting::hook_param_list(scripting::hook_param("Self", 'o', A), + scripting::hook_param("Object", 'o', B), + scripting::hook_param("Ship", 'o', A), + scripting::hook_param("ShipB", 'o', B), + scripting::hook_param("Hitpos", 'o', world_hit_pos), + scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == A)), + scripting::hook_param("ShipBSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == B)))); + + // Yes, this should be reversed. + b_override = scripting::hooks::OnShipCollision->isOverride(scripting::hooks::CollisionConditions{ {A, B} }, + scripting::hook_param_list(scripting::hook_param("Self", 'o', B), + scripting::hook_param("Object", 'o', A), + scripting::hook_param("Ship", 'o', B), + scripting::hook_param("ShipB", 'o', A), + scripting::hook_param("Hitpos", 'o', world_hit_pos), + scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == B)), + scripting::hook_param("ShipBSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == A)))); + } + + object* heavy_obj = ship_ship_hit_info.heavy; + object* light_obj = ship_ship_hit_info.light; + + if(!a_override && !b_override) + { + // + // Start of a codeblock that was originally taken from ship_ship_check_collision + // Moved here to properly handle ship-ship collision overrides and not process their physics when overridden by lua + // + + ship *light_shipp = &Ships[ship_ship_hit_info.light->instance]; + ship *heavy_shipp = &Ships[ship_ship_hit_info.heavy->instance]; + + const ship_info* light_sip = &Ship_info[light_shipp->ship_info_index]; + const ship_info* heavy_sip = &Ship_info[heavy_shipp->ship_info_index]; + + // Update ai to deal with collisions + if (OBJ_INDEX(heavy_obj) == Ai_info[light_shipp->ai_index].target_objnum) { + Ai_info[light_shipp->ai_index].ai_flags.set(AI::AI_Flags::Target_collision); + } + if (OBJ_INDEX(light_obj) == Ai_info[heavy_shipp->ai_index].target_objnum) { + Ai_info[heavy_shipp->ai_index].ai_flags.set(AI::AI_Flags::Target_collision); + } + + // SET PHYSICS PARAMETERS + // already have (hitpos - heavy) and light_cm_pos + + // get r_heavy and r_light + ship_ship_hit_info.r_heavy = ship_ship_hit_info.hit_pos; + vm_vec_sub(&ship_ship_hit_info.r_light, &ship_ship_hit_info.hit_pos, &ship_ship_hit_info.light_collision_cm_pos); + + // set normal for edge hit + if (ship_ship_hit_info.edge_hit) { + vm_vec_copy_normalize(&ship_ship_hit_info.collision_normal, &ship_ship_hit_info.r_light); + vm_vec_negate(&ship_ship_hit_info.collision_normal); + } + + // do physics + calculate_ship_ship_collision_physics(&ship_ship_hit_info); + + // Provide some separation for the case of same team + if (heavy_shipp->team == light_shipp->team) { + // If a couple of small ships, just move them apart. + + if ((heavy_sip->is_small_ship()) && (light_sip->is_small_ship())) { + if ((heavy_obj->flags[Object::Object_Flags::Player_ship]) || (light_obj->flags[Object::Object_Flags::Player_ship])) { + vec3d h_to_l_vec; + vec3d rel_vel_h; + vec3d perp_rel_vel; + + vm_vec_sub(&h_to_l_vec, &heavy_obj->pos, &light_obj->pos); + vm_vec_sub(&rel_vel_h, &heavy_obj->phys_info.vel, &light_obj->phys_info.vel); + float mass_sum = light_obj->phys_info.mass + heavy_obj->phys_info.mass; + + // get comp of rel_vel perp to h_to_l_vec; + float mag = vm_vec_dot(&h_to_l_vec, &rel_vel_h) / vm_vec_mag_squared(&h_to_l_vec); + vm_vec_scale_add(&perp_rel_vel, &rel_vel_h, &h_to_l_vec, -mag); + vm_vec_normalize(&perp_rel_vel); + + vm_vec_scale_add2(&heavy_obj->phys_info.vel, &perp_rel_vel, + heavy_sip->collision_physics.both_small_bounce * light_obj->phys_info.mass / mass_sum); + vm_vec_scale_add2(&light_obj->phys_info.vel, &perp_rel_vel, + -(light_sip->collision_physics.both_small_bounce) * heavy_obj->phys_info.mass / mass_sum); + + vm_vec_rotate(&heavy_obj->phys_info.prev_ramp_vel, &heavy_obj->phys_info.vel, &heavy_obj->orient); + vm_vec_rotate(&light_obj->phys_info.prev_ramp_vel, &light_obj->phys_info.vel, &light_obj->orient); + } + } + else { + // add extra velocity to separate the two objects, backing up the direction we came in. + // TODO: add effect of velocity from rotating submodel + float rel_vel = vm_vec_mag_quick(&ship_ship_hit_info.light_rel_vel); + if (rel_vel < 1) { + rel_vel = 1.0f; + } + float mass_sum = heavy_obj->phys_info.mass + light_obj->phys_info.mass; + vm_vec_scale_add2(&heavy_obj->phys_info.vel, &ship_ship_hit_info.light_rel_vel, + heavy_sip->collision_physics.bounce * light_obj->phys_info.mass / (mass_sum * rel_vel)); + vm_vec_rotate(&heavy_obj->phys_info.prev_ramp_vel, &heavy_obj->phys_info.vel, &heavy_obj->orient); + vm_vec_scale_add2(&light_obj->phys_info.vel, &ship_ship_hit_info.light_rel_vel, + -(light_sip->collision_physics.bounce) * heavy_obj->phys_info.mass / (mass_sum * rel_vel)); + vm_vec_rotate(&light_obj->phys_info.prev_ramp_vel, &light_obj->phys_info.vel, &light_obj->orient); + } + } + + // + // End of the codeblock that was originally taken from ship_ship_check_collision + // + + float damage; + + if ( ship_ship_hit_info.player_involved && (Player->control_mode == PCM_WARPOUT_STAGE1) ) { + gameseq_post_event( GS_EVENT_PLAYER_WARPOUT_STOP ); + HUD_printf("%s", XSTR( "Warpout sequence aborted.", 466)); + } + + damage = 0.005f * ship_ship_hit_info.impulse; // Cut collision-based damage in half. + // Decrease heavy damage by 2x. + if (damage > 5.0f){ + damage = 5.0f + (damage - 5.0f)/2.0f; + } + + do_kamikaze_crash(A, B); + + if (ship_ship_hit_info.impulse > 0) { + //Only flash the "Collision" text if not landing + if ( ship_ship_hit_info.player_involved && !ship_ship_hit_info.is_landing) { + hud_start_text_flash(XSTR("Collision", 1431), 2000); + } + } + + //If this is a landing, play a different sound + if (ship_ship_hit_info.is_landing) { + if (vm_vec_mag(&ship_ship_hit_info.light_rel_vel) > MIN_LANDING_SOUND_VEL) { + if ( ship_ship_hit_info.player_involved ) { + if ( !snd_is_playing(Player_collide_sound) ) { + Player_collide_sound = snd_play_3d( gamesnd_get_game_sound(light_sip->collision_physics.landing_sound_idx), &world_hit_pos, &View_position ); + } + } else { + if ( !snd_is_playing(AI_collide_sound) ) { + AI_collide_sound = snd_play_3d( gamesnd_get_game_sound(light_sip->collision_physics.landing_sound_idx), &world_hit_pos, &View_position ); + } + } + } + } + else { + collide_ship_ship_do_sound(&world_hit_pos, A, B, ship_ship_hit_info.player_involved); + } + + // check if we should do force feedback stuff + if (ship_ship_hit_info.player_involved && (ship_ship_hit_info.impulse > 0)) { + float scaler; + vec3d v; + + scaler = -ship_ship_hit_info.impulse / Player_obj->phys_info.mass * 300; + vm_vec_copy_normalize(&v, &world_hit_pos); + joy_ff_play_vector_effect(&v, scaler); + } + +#ifndef NDEBUG + if ( !Collide_friendly ) { + if ( Ships[A->instance].team == Ships[B->instance].team ) { + vec3d collision_vec, right_angle_vec; + vm_vec_normalized_dir(&collision_vec, &ship_ship_hit_info.hit_pos, &A->pos); + if (vm_vec_dot(&collision_vec, &A->orient.vec.fvec) > 0.999f){ + right_angle_vec = A->orient.vec.rvec; + } else { + vm_vec_cross(&right_angle_vec, &A->orient.vec.uvec, &collision_vec); + } + + vm_vec_scale_add2( &A->phys_info.vel, &right_angle_vec, +2.0f); + vm_vec_scale_add2( &B->phys_info.vel, &right_angle_vec, -2.0f); + + return; + } + } +#endif + + //Only do damage if not a landing + if (!ship_ship_hit_info.is_landing) { + // Scale damage based on skill level for player. + if ((light_obj->flags[Object::Object_Flags::Player_ship]) || (heavy_obj->flags[Object::Object_Flags::Player_ship])) { + + // Cyborg17 - Pretty hackish, but it's our best option, limit the amount of times a collision can + // happen to multiplayer clients, because otherwise the server can kill clients far too quickly. + // So here it goes, first only do this on the master (has an intrinsic multiplayer check) + if (MULTIPLAYER_MASTER) { + // check to see if both colliding ships are player ships + bool second_player_check = false; + if ((light_obj->flags[Object::Object_Flags::Player_ship]) && (heavy_obj->flags[Object::Object_Flags::Player_ship])) + second_player_check = true; + + // iterate through each player + for (net_player & current_player : Net_players) { + // check that this player's ship is valid, and that it's not the server ship. + if ((current_player.m_player != nullptr) && !(current_player.flags & NETINFO_FLAG_AM_MASTER) && (current_player.m_player->objnum > 0) && current_player.m_player->objnum < MAX_OBJECTS) { + // check that one of the colliding ships is this player's ship + if ((light_obj == &Objects[current_player.m_player->objnum]) || (heavy_obj == &Objects[current_player.m_player->objnum])) { + // finally if the host is also a player, ignore making these adjustments for him because he is in a pure simulation. + if (&Ships[Objects[current_player.m_player->objnum].instance] != Player_ship) { + Assertion(Interp_info.find(current_player.m_player->objnum) != Interp_info.end(), "Somehow the collision code thinks there is not a player ship interp record in multi when there really *should* be. This is a coder mistake, please report!"); + + // temp set this as an uninterpolated ship, to make the collision look more natural until the next update comes in. + Interp_info[current_player.m_player->objnum].force_interpolation_mode(); + + // check to see if it has been long enough since the last collision, if not, negate the damage + if (!timestamp_elapsed(current_player.s_info.player_collision_timestamp)) { + damage = 0.0f; + } else { + // make the usual adjustments + damage *= (float)(Game_skill_level * Game_skill_level + 1) / (NUM_SKILL_LEVELS + 1); + // if everything is good to go, set the timestamp for the next collision + current_player.s_info.player_collision_timestamp = _timestamp(PLAYER_COLLISION_TIMESTAMP); + } + } + + // did we find the player we were looking for? + if (!second_player_check) { + break; + // if we found one of the players we were looking for, set this to false so that the next one breaks the loop + } else { + second_player_check = false; + } + } + } + } + // if not in multiplayer, just do the damage adjustment. + } else { + damage *= (float) (Game_skill_level*Game_skill_level+1)/(NUM_SKILL_LEVELS+1); + } + } else if (Ships[light_obj->instance].team == Ships[heavy_obj->instance].team) { + // Decrease damage if non-player ships and not large. + // Looks dumb when fighters are taking damage from bumping into each other. + if ((light_obj->radius < 50.0f) && (heavy_obj->radius <50.0f)) { + damage /= 4.0f; + } + } + + int quadrant_num = -1; + if (!The_mission.ai_profile->flags[AI::Profile_Flags::No_shield_damage_from_ship_collisions] && !(ship_ship_hit_info.heavy->flags[Object::Object_Flags::No_shields])) { + quadrant_num = get_ship_quadrant_from_global(&world_hit_pos, ship_ship_hit_info.heavy); + if (!ship_is_shield_up(ship_ship_hit_info.heavy, quadrant_num)) + quadrant_num = -1; + } + + float damage_heavy = (100.0f * damage / heavy_obj->phys_info.mass); + ship_apply_local_damage(ship_ship_hit_info.heavy, ship_ship_hit_info.light, &world_hit_pos, damage_heavy, light_shipp->collision_damage_type_idx, + quadrant_num, CREATE_SPARKS, ship_ship_hit_info.heavy_submodel_num, &ship_ship_hit_info.collision_normal); + + hud_shield_quadrant_hit(ship_ship_hit_info.heavy, quadrant_num); + + // don't draw sparks (using sphere hitpos) + float damage_light = (100.0f * damage / heavy_obj->phys_info.mass); + ship_apply_local_damage(ship_ship_hit_info.light, ship_ship_hit_info.heavy, &world_hit_pos, damage_light, heavy_shipp->collision_damage_type_idx, + MISS_SHIELDS, NO_SPARKS, -1, &ship_ship_hit_info.collision_normal); + + hud_shield_quadrant_hit(ship_ship_hit_info.light, -1); + + maybe_push_little_ship_from_fast_big_ship(ship_ship_hit_info.heavy, ship_ship_hit_info.light, ship_ship_hit_info.impulse, &ship_ship_hit_info.collision_normal); + } + } + + if (!scripting::hooks::OnShipCollision->isActive()) { + return; + } + + if(!b_override || a_override) + { + scripting::hooks::OnShipCollision->run(scripting::hooks::CollisionConditions{ {A, B} }, + scripting::hook_param_list(scripting::hook_param("Self", 'o', A), + scripting::hook_param("Object", 'o', B), + scripting::hook_param("Ship", 'o', A), + scripting::hook_param("ShipB", 'o', B), + scripting::hook_param("Hitpos", 'o', world_hit_pos), + scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == A)), + scripting::hook_param("ShipBSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == B)))); + } + if((b_override && !a_override) || (!b_override && !a_override)) + { + // Yes, this should be reversed. + scripting::hooks::OnShipCollision->run(scripting::hooks::CollisionConditions{ {A, B} }, + scripting::hook_param_list(scripting::hook_param("Self", 'o', B), + scripting::hook_param("Object", 'o', A), + scripting::hook_param("Ship", 'o', B), + scripting::hook_param("ShipB", 'o', A), + scripting::hook_param("Hitpos", 'o', world_hit_pos), + scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == B)), + scripting::hook_param("ShipBSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == A)))); + } +} + /** * Checks ship-ship collisions. * @return 1 if all future collisions between these can be ignored because pair->a or pair->b aren't ships * @return Otherwise always returns 0, since two ships can always collide unless one (1) dies or (2) warps out. */ -int collide_ship_ship( obj_pair * pair ) +//returns never_hits, process_data +collision_result collide_ship_ship_check( obj_pair * pair ) { int player_involved; float dist; object *A = pair->a; object *B = pair->b; - if ( A->type == OBJ_WAYPOINT ) return 1; - if ( B->type == OBJ_WAYPOINT ) return 1; + if ( A->type == OBJ_WAYPOINT ) return { true, std::any(), &collide_ship_ship_process }; + if ( B->type == OBJ_WAYPOINT ) return { true, std::any(), &collide_ship_ship_process }; Assert( A->type == OBJ_SHIP ); Assert( B->type == OBJ_SHIP ); // Cyborg17 - no ship-ship collisions when doing multiplayer rollback if ( (Game_mode & GM_MULTIPLAYER) && multi_ship_record_get_rollback_wep_mode() ) { - return 0; + return { false, std::any(), &collide_ship_ship_process }; } if (reject_due_collision_groups(A,B)) - return 0; + return { false, std::any(), &collide_ship_ship_process }; // If the player is one of the two colliding ships, flag this... it is used in // several places this function. @@ -1137,20 +1448,21 @@ int collide_ship_ship( obj_pair * pair ) // collision related. Yes, from time to time that will look strange, but there are too many // side effects if we allow it. if (MULTIPLAYER_CLIENT){ - return 0; + return { false, std::any(), &collide_ship_ship_process }; } } // Don't check collisions for warping out player if past stage 1. if ( player_involved && (Player->control_mode > PCM_WARPOUT_STAGE1) ) { - return 0; + return { false, std::any(), &collide_ship_ship_process }; } dist = vm_vec_dist( &A->pos, &B->pos ); // If one of these is a planet, do special stuff. - if (maybe_collide_planet(A, B)) - return 0; + const auto& [planet_collision, planet_collision_data] = maybe_collide_planet(A, B); + if (planet_collision) + return { false, planet_collision_data, &mcp_1 }; if ( dist < A->radius + B->radius ) { int hit; @@ -1175,14 +1487,12 @@ int collide_ship_ship( obj_pair * pair ) } } - ship_info *light_sip = &Ship_info[Ships[LightOne->instance].ship_info_index]; - ship_info* heavy_sip = &Ship_info[Ships[HeavyOne->instance].ship_info_index]; - collision_info_struct ship_ship_hit_info; init_collision_info_struct(&ship_ship_hit_info); ship_ship_hit_info.heavy = HeavyOne; // heavy object, generally slower moving ship_ship_hit_info.light = LightOne; // light object, generally faster moving + ship_ship_hit_info.player_involved = player_involved; hit = ship_ship_check_collision(&ship_ship_hit_info); @@ -1190,304 +1500,7 @@ int collide_ship_ship( obj_pair * pair ) if ( hit ) { - bool a_override = false, b_override = false; - - // get world hitpos - do it here in case the override hooks need it - vec3d world_hit_pos; - vm_vec_add(&world_hit_pos, &ship_ship_hit_info.heavy->pos, &ship_ship_hit_info.hit_pos); - - // get submodel handle if scripting needs it - bool has_submodel = (ship_ship_hit_info.heavy_submodel_num >= 0); - scripting::api::submodel_h smh(ship_ship_hit_info.heavy_model_num, ship_ship_hit_info.heavy_submodel_num); - - if (scripting::hooks::OnShipCollision->isActive()) { - a_override = scripting::hooks::OnShipCollision->isOverride(scripting::hooks::CollisionConditions{ {A, B} }, - scripting::hook_param_list(scripting::hook_param("Self", 'o', A), - scripting::hook_param("Object", 'o', B), - scripting::hook_param("Ship", 'o', A), - scripting::hook_param("ShipB", 'o', B), - scripting::hook_param("Hitpos", 'o', world_hit_pos), - scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == A)), - scripting::hook_param("ShipBSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == B)))); - - // Yes, this should be reversed. - b_override = scripting::hooks::OnShipCollision->isOverride(scripting::hooks::CollisionConditions{ {A, B} }, - scripting::hook_param_list(scripting::hook_param("Self", 'o', B), - scripting::hook_param("Object", 'o', A), - scripting::hook_param("Ship", 'o', B), - scripting::hook_param("ShipB", 'o', A), - scripting::hook_param("Hitpos", 'o', world_hit_pos), - scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == B)), - scripting::hook_param("ShipBSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == A)))); - } - - if(!a_override && !b_override) - { - // - // Start of a codeblock that was originally taken from ship_ship_check_collision - // Moved here to properly handle ship-ship collision overrides and not process their physics when overridden by lua - // - - ship *light_shipp = &Ships[ship_ship_hit_info.light->instance]; - ship *heavy_shipp = &Ships[ship_ship_hit_info.heavy->instance]; - - object* heavy_obj = ship_ship_hit_info.heavy; - object* light_obj = ship_ship_hit_info.light; - // Update ai to deal with collisions - if (OBJ_INDEX(heavy_obj) == Ai_info[light_shipp->ai_index].target_objnum) { - Ai_info[light_shipp->ai_index].ai_flags.set(AI::AI_Flags::Target_collision); - } - if (OBJ_INDEX(light_obj) == Ai_info[heavy_shipp->ai_index].target_objnum) { - Ai_info[heavy_shipp->ai_index].ai_flags.set(AI::AI_Flags::Target_collision); - } - - // SET PHYSICS PARAMETERS - // already have (hitpos - heavy) and light_cm_pos - - // get r_heavy and r_light - ship_ship_hit_info.r_heavy = ship_ship_hit_info.hit_pos; - vm_vec_sub(&ship_ship_hit_info.r_light, &ship_ship_hit_info.hit_pos, &ship_ship_hit_info.light_collision_cm_pos); - - // set normal for edge hit - if (ship_ship_hit_info.edge_hit) { - vm_vec_copy_normalize(&ship_ship_hit_info.collision_normal, &ship_ship_hit_info.r_light); - vm_vec_negate(&ship_ship_hit_info.collision_normal); - } - - // do physics - calculate_ship_ship_collision_physics(&ship_ship_hit_info); - - // Provide some separation for the case of same team - if (heavy_shipp->team == light_shipp->team) { - // If a couple of small ships, just move them apart. - - if ((heavy_sip->is_small_ship()) && (light_sip->is_small_ship())) { - if ((heavy_obj->flags[Object::Object_Flags::Player_ship]) || (light_obj->flags[Object::Object_Flags::Player_ship])) { - vec3d h_to_l_vec; - vec3d rel_vel_h; - vec3d perp_rel_vel; - - vm_vec_sub(&h_to_l_vec, &heavy_obj->pos, &light_obj->pos); - vm_vec_sub(&rel_vel_h, &heavy_obj->phys_info.vel, &light_obj->phys_info.vel); - float mass_sum = light_obj->phys_info.mass + heavy_obj->phys_info.mass; - - // get comp of rel_vel perp to h_to_l_vec; - float mag = vm_vec_dot(&h_to_l_vec, &rel_vel_h) / vm_vec_mag_squared(&h_to_l_vec); - vm_vec_scale_add(&perp_rel_vel, &rel_vel_h, &h_to_l_vec, -mag); - vm_vec_normalize(&perp_rel_vel); - - vm_vec_scale_add2(&heavy_obj->phys_info.vel, &perp_rel_vel, - heavy_sip->collision_physics.both_small_bounce * light_obj->phys_info.mass / mass_sum); - vm_vec_scale_add2(&light_obj->phys_info.vel, &perp_rel_vel, - -(light_sip->collision_physics.both_small_bounce) * heavy_obj->phys_info.mass / mass_sum); - - vm_vec_rotate(&heavy_obj->phys_info.prev_ramp_vel, &heavy_obj->phys_info.vel, &heavy_obj->orient); - vm_vec_rotate(&light_obj->phys_info.prev_ramp_vel, &light_obj->phys_info.vel, &light_obj->orient); - } - } - else { - // add extra velocity to separate the two objects, backing up the direction we came in. - // TODO: add effect of velocity from rotating submodel - float rel_vel = vm_vec_mag_quick(&ship_ship_hit_info.light_rel_vel); - if (rel_vel < 1) { - rel_vel = 1.0f; - } - float mass_sum = heavy_obj->phys_info.mass + light_obj->phys_info.mass; - vm_vec_scale_add2(&heavy_obj->phys_info.vel, &ship_ship_hit_info.light_rel_vel, - heavy_sip->collision_physics.bounce * light_obj->phys_info.mass / (mass_sum * rel_vel)); - vm_vec_rotate(&heavy_obj->phys_info.prev_ramp_vel, &heavy_obj->phys_info.vel, &heavy_obj->orient); - vm_vec_scale_add2(&light_obj->phys_info.vel, &ship_ship_hit_info.light_rel_vel, - -(light_sip->collision_physics.bounce) * heavy_obj->phys_info.mass / (mass_sum * rel_vel)); - vm_vec_rotate(&light_obj->phys_info.prev_ramp_vel, &light_obj->phys_info.vel, &light_obj->orient); - } - } - - // - // End of the codeblock that was originally taken from ship_ship_check_collision - // - - float damage; - - if ( player_involved && (Player->control_mode == PCM_WARPOUT_STAGE1) ) { - gameseq_post_event( GS_EVENT_PLAYER_WARPOUT_STOP ); - HUD_printf("%s", XSTR( "Warpout sequence aborted.", 466)); - } - - damage = 0.005f * ship_ship_hit_info.impulse; // Cut collision-based damage in half. - // Decrease heavy damage by 2x. - if (damage > 5.0f){ - damage = 5.0f + (damage - 5.0f)/2.0f; - } - - do_kamikaze_crash(A, B); - - if (ship_ship_hit_info.impulse > 0) { - //Only flash the "Collision" text if not landing - if ( player_involved && !ship_ship_hit_info.is_landing) { - hud_start_text_flash(XSTR("Collision", 1431), 2000); - } - } - - //If this is a landing, play a different sound - if (ship_ship_hit_info.is_landing) { - if (vm_vec_mag(&ship_ship_hit_info.light_rel_vel) > MIN_LANDING_SOUND_VEL) { - if ( player_involved ) { - if ( !snd_is_playing(Player_collide_sound) ) { - Player_collide_sound = snd_play_3d( gamesnd_get_game_sound(light_sip->collision_physics.landing_sound_idx), &world_hit_pos, &View_position ); - } - } else { - if ( !snd_is_playing(AI_collide_sound) ) { - AI_collide_sound = snd_play_3d( gamesnd_get_game_sound(light_sip->collision_physics.landing_sound_idx), &world_hit_pos, &View_position ); - } - } - } - } - else { - collide_ship_ship_do_sound(&world_hit_pos, A, B, player_involved); - } - - // check if we should do force feedback stuff - if (player_involved && (ship_ship_hit_info.impulse > 0)) { - float scaler; - vec3d v; - - scaler = -ship_ship_hit_info.impulse / Player_obj->phys_info.mass * 300; - vm_vec_copy_normalize(&v, &world_hit_pos); - joy_ff_play_vector_effect(&v, scaler); - } - - #ifndef NDEBUG - if ( !Collide_friendly ) { - if ( Ships[A->instance].team == Ships[B->instance].team ) { - vec3d collision_vec, right_angle_vec; - vm_vec_normalized_dir(&collision_vec, &ship_ship_hit_info.hit_pos, &A->pos); - if (vm_vec_dot(&collision_vec, &A->orient.vec.fvec) > 0.999f){ - right_angle_vec = A->orient.vec.rvec; - } else { - vm_vec_cross(&right_angle_vec, &A->orient.vec.uvec, &collision_vec); - } - - vm_vec_scale_add2( &A->phys_info.vel, &right_angle_vec, +2.0f); - vm_vec_scale_add2( &B->phys_info.vel, &right_angle_vec, -2.0f); - - return 0; - } - } - #endif - - //Only do damage if not a landing - if (!ship_ship_hit_info.is_landing) { - // Scale damage based on skill level for player. - if ((LightOne->flags[Object::Object_Flags::Player_ship]) || (HeavyOne->flags[Object::Object_Flags::Player_ship])) { - - // Cyborg17 - Pretty hackish, but it's our best option, limit the amount of times a collision can - // happen to multiplayer clients, because otherwise the server can kill clients far too quickly. - // So here it goes, first only do this on the master (has an intrinsic multiplayer check) - if (MULTIPLAYER_MASTER) { - // check to see if both colliding ships are player ships - bool second_player_check = false; - if ((LightOne->flags[Object::Object_Flags::Player_ship]) && (HeavyOne->flags[Object::Object_Flags::Player_ship])) - second_player_check = true; - - // iterate through each player - for (net_player & current_player : Net_players) { - // check that this player's ship is valid, and that it's not the server ship. - if ((current_player.m_player != nullptr) && !(current_player.flags & NETINFO_FLAG_AM_MASTER) && (current_player.m_player->objnum > 0) && current_player.m_player->objnum < MAX_OBJECTS) { - // check that one of the colliding ships is this player's ship - if ((LightOne == &Objects[current_player.m_player->objnum]) || (HeavyOne == &Objects[current_player.m_player->objnum])) { - // finally if the host is also a player, ignore making these adjustments for him because he is in a pure simulation. - if (&Ships[Objects[current_player.m_player->objnum].instance] != Player_ship) { - Assertion(Interp_info.find(current_player.m_player->objnum) != Interp_info.end(), "Somehow the collision code thinks there is not a player ship interp record in multi when there really *should* be. This is a coder mistake, please report!"); - - // temp set this as an uninterpolated ship, to make the collision look more natural until the next update comes in. - Interp_info[current_player.m_player->objnum].force_interpolation_mode(); - - // check to see if it has been long enough since the last collision, if not, negate the damage - if (!timestamp_elapsed(current_player.s_info.player_collision_timestamp)) { - damage = 0.0f; - } else { - // make the usual adjustments - damage *= (float)(Game_skill_level * Game_skill_level + 1) / (NUM_SKILL_LEVELS + 1); - // if everything is good to go, set the timestamp for the next collision - current_player.s_info.player_collision_timestamp = _timestamp(PLAYER_COLLISION_TIMESTAMP); - } - } - - // did we find the player we were looking for? - if (!second_player_check) { - break; - // if we found one of the players we were looking for, set this to false so that the next one breaks the loop - } else { - second_player_check = false; - } - } - } - } - // if not in multiplayer, just do the damage adjustment. - } else { - damage *= (float) (Game_skill_level*Game_skill_level+1)/(NUM_SKILL_LEVELS+1); - } - } else if (Ships[LightOne->instance].team == Ships[HeavyOne->instance].team) { - // Decrease damage if non-player ships and not large. - // Looks dumb when fighters are taking damage from bumping into each other. - if ((LightOne->radius < 50.0f) && (HeavyOne->radius <50.0f)) { - damage /= 4.0f; - } - } - - int quadrant_num = -1; - if (!The_mission.ai_profile->flags[AI::Profile_Flags::No_shield_damage_from_ship_collisions] && !(ship_ship_hit_info.heavy->flags[Object::Object_Flags::No_shields])) { - quadrant_num = get_ship_quadrant_from_global(&world_hit_pos, ship_ship_hit_info.heavy); - if (!ship_is_shield_up(ship_ship_hit_info.heavy, quadrant_num)) - quadrant_num = -1; - } - - float damage_heavy = (100.0f * damage / HeavyOne->phys_info.mass); - ship_apply_local_damage(ship_ship_hit_info.heavy, ship_ship_hit_info.light, &world_hit_pos, damage_heavy, light_shipp->collision_damage_type_idx, - quadrant_num, CREATE_SPARKS, ship_ship_hit_info.heavy_submodel_num, &ship_ship_hit_info.collision_normal); - - hud_shield_quadrant_hit(ship_ship_hit_info.heavy, quadrant_num); - - // don't draw sparks (using sphere hitpos) - float damage_light = (100.0f * damage / LightOne->phys_info.mass); - ship_apply_local_damage(ship_ship_hit_info.light, ship_ship_hit_info.heavy, &world_hit_pos, damage_light, heavy_shipp->collision_damage_type_idx, - MISS_SHIELDS, NO_SPARKS, -1, &ship_ship_hit_info.collision_normal); - - hud_shield_quadrant_hit(ship_ship_hit_info.light, -1); - - maybe_push_little_ship_from_fast_big_ship(ship_ship_hit_info.heavy, ship_ship_hit_info.light, ship_ship_hit_info.impulse, &ship_ship_hit_info.collision_normal); - } - } - - if (!scripting::hooks::OnShipCollision->isActive()) { - return 0; - } - - if(!(b_override && !a_override)) - { - scripting::hooks::OnShipCollision->run(scripting::hooks::CollisionConditions{ {A, B} }, - scripting::hook_param_list(scripting::hook_param("Self", 'o', A), - scripting::hook_param("Object", 'o', B), - scripting::hook_param("Ship", 'o', A), - scripting::hook_param("ShipB", 'o', B), - scripting::hook_param("Hitpos", 'o', world_hit_pos), - scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == A)), - scripting::hook_param("ShipBSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == B)))); - } - if((b_override && !a_override) || (!b_override && !a_override)) - { - // Yes, this should be reversed. - scripting::hooks::OnShipCollision->run(scripting::hooks::CollisionConditions{ {A, B} }, - scripting::hook_param_list(scripting::hook_param("Self", 'o', B), - scripting::hook_param("Object", 'o', A), - scripting::hook_param("Ship", 'o', B), - scripting::hook_param("ShipB", 'o', A), - scripting::hook_param("Hitpos", 'o', world_hit_pos), - scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == B)), - scripting::hook_param("ShipBSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel && (ship_ship_hit_info.heavy == A)))); - } - - return 0; + return { false, ship_ship_hit_info, &collide_ship_ship_process }; } } else { @@ -1500,7 +1513,7 @@ int collide_ship_ship( obj_pair * pair ) if (((Ships[A->instance].is_arriving(ship::warpstage::STAGE1, false)) && (Ship_info[Ships[A->instance].ship_info_index].is_big_or_huge())) || ((Ships[B->instance].is_arriving(ship::warpstage::STAGE1, false)) && (Ship_info[Ships[B->instance].ship_info_index].is_big_or_huge())) ) { pair->next_check_time = timestamp(0); // check next time - return 0; + return { false, std::any(), &collide_ship_ship_process }; } // get max of (1) max_vel.z, (2) 10, (3) afterburner_max_vel.z, (4) vel.z (for warping in ships exceeding expected max vel) @@ -1537,6 +1550,16 @@ int collide_ship_ship( obj_pair * pair ) pair->next_check_time = timestamp(0); // check next time } } - - return 0; + + return { false, std::any(), &collide_ship_ship_process }; +} + +int collide_ship_ship( obj_pair * pair ) { + const auto& [never_check_again, collision_data, process_fnc] = collide_ship_ship_check(pair); + + if (collision_data.has_value()) { + process_fnc(pair, collision_data); + } + + return never_check_again ? 1 : 0; } diff --git a/code/object/collideshipweapon.cpp b/code/object/collideshipweapon.cpp index 240eb281af2..68c6278fd06 100644 --- a/code/object/collideshipweapon.cpp +++ b/code/object/collideshipweapon.cpp @@ -28,10 +28,12 @@ #include "ship/shiphit.h" #include "weapon/weapon.h" +//mc, notify_ai_shield_down, shield_collision, quadrant_num, shield_tri_hit, shield_hitpoint +using ship_weapon_collision_data = std::tuple, int, bool, int, int, vec3d>; extern int Game_skill_level; extern float ai_endangered_time(const object *ship_objp, const object *weapon_objp); -static int check_inside_radius_for_big_ships( object *ship, object *weapon_obj, obj_pair *pair ); +static std::tuple check_inside_radius_for_big_ships( object *ship, object *weapon_obj, obj_pair *pair ); extern float flFrametime; @@ -176,7 +178,8 @@ static void ship_weapon_do_hit_stuff(object *pship_obj, object *weapon_obj, cons extern int Framecount; -static int ship_weapon_check_collision(object *ship_objp, object *weapon_objp, float time_limit = 0.0f, int *next_hit = nullptr) +//need_postproc, recheck, do collision? +static std::tuple ship_weapon_check_collision(object *ship_objp, object *weapon_objp, float time_limit = 0.0f, int *next_hit = nullptr) { mc_info mc_hull, mc_shield, *mc; ship *shipp; @@ -202,7 +205,7 @@ static int ship_weapon_check_collision(object *ship_objp, object *weapon_objp, f Assert( shipp->objnum == OBJ_INDEX(ship_objp)); // Make ships that are warping in not get collision detection done - if ( shipp->is_arriving() ) return 0; + if ( shipp->is_arriving() ) return {false, true, {std::nullopt, -1, false, -1, -1, ZERO_VECTOR}}; // Return information for AI to detect incoming fire. // Could perhaps be done elsewhere at lower cost --MK, 11/7/97 @@ -440,6 +443,11 @@ static int ship_weapon_check_collision(object *ship_objp, object *weapon_objp, f shield_collision = 0; } + int notify_ai_shield_down = -1; + + int shield_tri_hit = -1; + vec3d shield_hitpos = ZERO_VECTOR; + if (shield_collision) { // pick out the shield quadrant quadrant_num = get_quadrant(&mc_shield.hit_point, ship_objp); @@ -450,7 +458,7 @@ static int ship_weapon_check_collision(object *ship_objp, object *weapon_objp, f // so that the AI can put energy torwards repairing that shield segement (but put behind a flag) // --wookieejedi if (The_mission.ai_profile->flags[AI::Profile_Flags::Fix_AI_shield_management_bug] && SCP_vector_inbounds(ship_objp->shield_quadrant, quadrant_num)) { - Ai_info[Ships[ship_objp->instance].ai_index].danger_shield_quadrant = quadrant_num; + notify_ai_shield_down = quadrant_num; } quadrant_num = -1; shield_collision = 0; @@ -460,7 +468,8 @@ static int ship_weapon_check_collision(object *ship_objp, object *weapon_objp, f if (quadrant_num >= 0) { // do the hit effect if ( mc_shield.shield_hit_tri != -1 && (mc_shield.hit_dist*(flFrametime + time_limit) - flFrametime) < 0.0f ) { - add_shield_point(OBJ_INDEX(ship_objp), mc_shield.shield_hit_tri, &mc_shield.hit_point, wip->shield_impact_effect_radius); + shield_tri_hit = mc_shield.shield_hit_tri; + shield_hitpos = mc_shield.hit_point; } // if this weapon pierces the shield, then do the hit effect, but act like a shield collision never occurred; @@ -477,25 +486,72 @@ static int ship_weapon_check_collision(object *ship_objp, object *weapon_objp, f { mc = &mc_shield; Assert(quadrant_num >= 0); - valid_hit_occurred = 1; + valid_hit_occurred = 1; //Hit } else if (hull_collision) { mc = &mc_hull; - valid_hit_occurred = 1; + valid_hit_occurred = 1; //Hit } else - mc = nullptr; + mc = nullptr; //No hit, maybe stop checking // deal with predictive collisions. Find their actual hit time and see if they occured in current frame if (next_hit && valid_hit_occurred) { // find hit time *next_hit = (int) (1000.0f * (mc->hit_dist*(flFrametime + time_limit) - flFrametime) ); if (*next_hit > 0) - // if hit occurs outside of this frame, do not do damage - return 1; + // if hit occurs outside of this frame, do not do damage + return { false, false, {std::nullopt, -1, false, -1, -1, ZERO_VECTOR} }; //No hit, but continue checking + } + + bool postproc = valid_hit_occurred || notify_ai_shield_down >= 0; + ship_weapon_collision_data collision_data { + valid_hit_occurred ? std::optional(*mc) : std::nullopt, notify_ai_shield_down, postproc, quadrant_num, shield_tri_hit, shield_hitpos + }; + + // when the $Fixed Missile Detonation: flag is active, skip this whole block, as it's redundant to a similar check in weapon_home() + if (!valid_hit_occurred && !Fixed_missile_detonation && (Missiontime - wp->creation_time > F1_0/2) && (wip->is_homing()) && (wp->homing_object == ship_objp)) { + if (dist < wip->shockwave.inner_rad) { + vec3d vec_to_ship; + vm_vec_normalized_dir(&vec_to_ship, &ship_objp->pos, &weapon_objp->pos); + + // this causes the weapon to detonate if it has flown past the center of the ship + if (vm_vec_dot(&vec_to_ship, &weapon_objp->orient.vec.fvec) < 0.0f) { + // check if we're colliding against "invisible" ship + if (!(shipp->flags[Ship::Ship_Flags::Dont_collide_invis])) { + wp->lifeleft = 0.001f; + wp->weapon_flags.set(Weapon::Weapon_Flags::Begun_detonation); + + if (ship_objp == Player_obj) + nprintf(("Jim", "Frame %i: Weapon %d set to detonate, dist = %7.3f.\n", Framecount, OBJ_INDEX(weapon_objp), dist)); + valid_hit_occurred = 1; //No hit, continue checking + } + } + } } + return { postproc, !static_cast(valid_hit_occurred), collision_data} ; +} + +static void ship_weapon_process_collision(obj_pair* pair, const ship_weapon_collision_data& collision_data) { + object *ship_objp = pair->a; + object *weapon_objp = pair->b; + weapon* wp = &Weapons[weapon_objp->instance]; + ship* shipp = &Ships[ship_objp->instance]; + const weapon_info* wip = &Weapon_info[wp->weapon_info_index]; + const ship_info* sip = &Ship_info[shipp->ship_info_index]; + + const auto& [mc_opt, notify_ai_shield_down, shield_collision, quadrant_num, shield_tri_hit, shield_hitpos] = collision_data; + bool valid_hit_occurred = mc_opt.has_value(); + auto mc = valid_hit_occurred ? &(*mc_opt) : nullptr; + + if (notify_ai_shield_down >= 0) + Ai_info[Ships[ship_objp->instance].ai_index].danger_shield_quadrant = notify_ai_shield_down; + + if (shield_tri_hit >= 0) + add_shield_point(OBJ_INDEX(ship_objp), shield_tri_hit, &shield_hitpos, wip->shield_impact_effect_radius); + if ( valid_hit_occurred ) { wp->collisionInfo = new mc_info; // The weapon will free this memory later @@ -509,20 +565,20 @@ static int ship_weapon_check_collision(object *ship_objp, object *weapon_objp, f if (scripting::hooks::OnWeaponCollision->isActive()) { ship_override = scripting::hooks::OnWeaponCollision->isOverride(scripting::hooks::CollisionConditions{ {ship_objp, weapon_objp} }, - scripting::hook_param_list(scripting::hook_param("Self", 'o', ship_objp), - scripting::hook_param("Object", 'o', weapon_objp), - scripting::hook_param("Ship", 'o', ship_objp), - scripting::hook_param("Weapon", 'o', weapon_objp), - scripting::hook_param("Hitpos", 'o', mc->hit_point_world))); + scripting::hook_param_list(scripting::hook_param("Self", 'o', ship_objp), + scripting::hook_param("Object", 'o', weapon_objp), + scripting::hook_param("Ship", 'o', ship_objp), + scripting::hook_param("Weapon", 'o', weapon_objp), + scripting::hook_param("Hitpos", 'o', mc->hit_point_world))); } if (scripting::hooks::OnShipCollision->isActive()) { weapon_override = scripting::hooks::OnShipCollision->isOverride(scripting::hooks::CollisionConditions{ {ship_objp, weapon_objp} }, - scripting::hook_param_list(scripting::hook_param("Self", 'o', weapon_objp), - scripting::hook_param("Object", 'o', ship_objp), - scripting::hook_param("Ship", 'o', ship_objp), - scripting::hook_param("Weapon", 'o', weapon_objp), - scripting::hook_param("Hitpos", 'o', mc->hit_point_world), - scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel))); + scripting::hook_param_list(scripting::hook_param("Self", 'o', weapon_objp), + scripting::hook_param("Object", 'o', ship_objp), + scripting::hook_param("Ship", 'o', ship_objp), + scripting::hook_param("Weapon", 'o', weapon_objp), + scripting::hook_param("Hitpos", 'o', mc->hit_point_world), + scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel))); } if(!ship_override && !weapon_override) { @@ -536,44 +592,26 @@ static int ship_weapon_check_collision(object *ship_objp, object *weapon_objp, f if (scripting::hooks::OnWeaponCollision->isActive() && !(weapon_override && !ship_override)) { scripting::hooks::OnWeaponCollision->run(scripting::hooks::CollisionConditions{ {ship_objp, weapon_objp} }, - scripting::hook_param_list(scripting::hook_param("Self", 'o', ship_objp), - scripting::hook_param("Object", 'o', weapon_objp), - scripting::hook_param("Ship", 'o', ship_objp), - scripting::hook_param("Weapon", 'o', weapon_objp), - scripting::hook_param("Hitpos", 'o', mc->hit_point_world))); + scripting::hook_param_list(scripting::hook_param("Self", 'o', ship_objp), + scripting::hook_param("Object", 'o', weapon_objp), + scripting::hook_param("Ship", 'o', ship_objp), + scripting::hook_param("Weapon", 'o', weapon_objp), + scripting::hook_param("Hitpos", 'o', mc->hit_point_world))); } if (scripting::hooks::OnShipCollision->isActive() && !ship_override) { scripting::hooks::OnShipCollision->run(scripting::hooks::CollisionConditions{ {ship_objp, weapon_objp} }, - scripting::hook_param_list(scripting::hook_param("Self", 'o', weapon_objp), - scripting::hook_param("Object", 'o', ship_objp), - scripting::hook_param("Ship", 'o', ship_objp), - scripting::hook_param("Weapon", 'o', weapon_objp), - scripting::hook_param("Hitpos", 'o', mc->hit_point_world), - scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel))); - } - } - // when the $Fixed Missile Detonation: flag is active, skip this whole block, as it's redundant to a similar check in weapon_home() - else if (!Fixed_missile_detonation && (Missiontime - wp->creation_time > F1_0/2) && (wip->is_homing()) && (wp->homing_object == ship_objp)) { - if (dist < wip->shockwave.inner_rad) { - vec3d vec_to_ship; - vm_vec_normalized_dir(&vec_to_ship, &ship_objp->pos, &weapon_objp->pos); - - // this causes the weapon to detonate if it has flown past the center of the ship - if (vm_vec_dot(&vec_to_ship, &weapon_objp->orient.vec.fvec) < 0.0f) { - // check if we're colliding against "invisible" ship - if (!(shipp->flags[Ship::Ship_Flags::Dont_collide_invis])) { - wp->lifeleft = 0.001f; - wp->weapon_flags.set(Weapon::Weapon_Flags::Begun_detonation); - - if (ship_objp == Player_obj) - nprintf(("Jim", "Frame %i: Weapon %d set to detonate, dist = %7.3f.\n", Framecount, OBJ_INDEX(weapon_objp), dist)); - valid_hit_occurred = 1; - } - } + scripting::hook_param_list(scripting::hook_param("Self", 'o', weapon_objp), + scripting::hook_param("Object", 'o', ship_objp), + scripting::hook_param("Ship", 'o', ship_objp), + scripting::hook_param("Weapon", 'o', weapon_objp), + scripting::hook_param("Hitpos", 'o', mc->hit_point_world), + scripting::hook_param("ShipSubmodel", 'o', scripting::api::l_Submodel.Set(smh), has_submodel))); } } +} - return valid_hit_occurred; +static void ship_weapon_process_collision(obj_pair* pair, const std::any& collision_data) { + ship_weapon_process_collision(pair, std::any_cast(collision_data)); } @@ -584,7 +622,6 @@ static int ship_weapon_check_collision(object *ship_objp, object *weapon_objp, f */ int collide_ship_weapon( obj_pair * pair ) { - int did_hit; object *ship = pair->a; object *weapon_obj = pair->b; @@ -618,13 +655,18 @@ int collide_ship_weapon( obj_pair * pair ) // Note: culling ships with auto spread shields seems to waste more performance than it saves, // so we're not doing that here if ( !(sip->flags[Ship::Info_Flags::Auto_spread_shields]) && vm_vec_dist_squared(&ship->pos, &weapon_obj->pos) < (1.2f*ship->radius*ship->radius) ) { - return check_inside_radius_for_big_ships( ship, weapon_obj, pair ); + const auto& [do_postproc, never_hits, collision_data] = check_inside_radius_for_big_ships( ship, weapon_obj, pair ); + if (do_postproc) + ship_weapon_process_collision(pair, collision_data); + return never_hits; } } - did_hit = ship_weapon_check_collision( ship, weapon_obj ); + const auto& [do_postproc, check_if_never_hits, collision_data] = ship_weapon_check_collision( ship, weapon_obj ); + if (do_postproc) + ship_weapon_process_collision(pair, collision_data); - if ( !did_hit ) { + if ( check_if_never_hits ) { // Since we didn't hit, check to see if we can disable all future collisions // between these two. return weapon_will_never_hit( weapon_obj, ship, pair ); @@ -633,6 +675,53 @@ int collide_ship_weapon( obj_pair * pair ) return 0; } +//returns never_hits, process_data +collision_result collide_ship_weapon_check( obj_pair * pair ) +{ + object *ship = pair->a; + object *weapon_obj = pair->b; + + Assert( ship->type == OBJ_SHIP ); + Assert( weapon_obj->type == OBJ_WEAPON ); + + ship_info *sip = &Ship_info[Ships[ship->instance].ship_info_index]; + + // Cyborg17 - no ship-ship collisions when doing multiplayer rollback + if ( (Game_mode & GM_MULTIPLAYER) && multi_ship_record_get_rollback_wep_mode() && (weapon_obj->parent_sig == OBJ_INDEX(ship)) ) { + return {false, std::any(), &ship_weapon_process_collision}; + } + + // Don't check collisions for player if past first warpout stage. + if ( Player->control_mode > PCM_WARPOUT_STAGE1) { + if ( ship == Player_obj ) + return {false, std::any(), &ship_weapon_process_collision}; + } + + if (reject_due_collision_groups(ship, weapon_obj)) + return {false, std::any(), &ship_weapon_process_collision}; + + // Cull lasers within big ship spheres by casting a vector forward for (1) exit sphere or (2) lifetime of laser + // If it does hit, don't check the pair until about 200 ms before collision. + // If it does not hit and is within error tolerance, cull the pair. + + if ( (sip->is_big_or_huge()) && (weapon_obj->phys_info.flags & PF_CONST_VEL) ) { + // Check when within ~1.1 radii. + // This allows good transition between sphere checking (leaving the laser about 200 ms from radius) and checking + // within the sphere with little time between. There may be some time for "small" big ships + // Note: culling ships with auto spread shields seems to waste more performance than it saves, + // so we're not doing that here + if ( !(sip->flags[Ship::Info_Flags::Auto_spread_shields]) && vm_vec_dist_squared(&ship->pos, &weapon_obj->pos) < (1.2f*ship->radius*ship->radius) ) { + const auto& [do_postproc, never_hits, collision_data] = check_inside_radius_for_big_ships( ship, weapon_obj, pair ); + return {never_hits, do_postproc ? collision_data : std::any(), &ship_weapon_process_collision}; + } + } + + const auto& [do_postproc, check_if_never_hits, collision_data] = ship_weapon_check_collision( ship, weapon_obj ); + bool never_hits = check_if_never_hits ? weapon_will_never_hit( weapon_obj, ship, pair ) : false; + + return {never_hits, do_postproc ? collision_data : std::any(), &ship_weapon_process_collision}; +} + /** * Upper limit estimate ship speed at end of time */ @@ -664,7 +753,7 @@ static float estimate_ship_speed_upper_limit( object *ship, float time ) * @return 1 if pair can be culled * @return 0 if pair can not be culled */ -static int check_inside_radius_for_big_ships( object *ship, object *weapon_obj, obj_pair *pair ) +static std::tuple check_inside_radius_for_big_ships( object *ship, object *weapon_obj, obj_pair *pair ) { vec3d error_vel; // vel perpendicular to laser float error_vel_mag; // magnitude of error_vel @@ -702,20 +791,21 @@ static int check_inside_radius_for_big_ships( object *ship, object *weapon_obj, // Note: when estimated hit time is less than 200 ms, look at every frame int hit_time; // estimated time of hit in ms + const auto& [do_postproc, does_not_hit, collision_data] = ship_weapon_check_collision( ship, weapon_obj, limit_time, &hit_time ); // modify ship_weapon_check_collision to do damage if hit_time is negative (ie, hit occurs in this frame) - if ( ship_weapon_check_collision( ship, weapon_obj, limit_time, &hit_time ) ) { + if ( !does_not_hit ) { // hit occured in while in sphere if (hit_time < 0) { // hit occured in the frame - return 1; + return {do_postproc, true, collision_data}; } else if (hit_time > 200) { pair->next_check_time = timestamp(hit_time - 200); - return 0; + return {do_postproc, false, collision_data}; // set next check time to time - 200 } else { // set next check time to next frame pair->next_check_time = 1; - return 0; + return {do_postproc, false, collision_data}; } } else { if (limit_time > time_to_max_error) { @@ -725,10 +815,10 @@ static int check_inside_radius_for_big_ships( object *ship, object *weapon_obj, } else { pair->next_check_time = 1; } - return 0; + return {do_postproc, false, collision_data}; } else { // no hit and within error tolerance - return 1; + return {do_postproc, true, collision_data}; } } } diff --git a/code/object/objcollide.cpp b/code/object/objcollide.cpp index 854cd0e76d5..1d2a9365a2d 100644 --- a/code/object/objcollide.cpp +++ b/code/object/objcollide.cpp @@ -18,6 +18,9 @@ #include "weapon/beam.h" #include "weapon/weapon.h" #include "tracing/Monitor.h" +#include "utils/threading.h" + +#include // the next 2 variables are used for pair statistics @@ -722,12 +725,115 @@ void obj_quicksort_colliders(SCP_vector *list, int left, int right, int axi } } +struct collision_thread_data { + struct collision_queue_item { + obj_pair objs; + uint ctype; + }; + struct collision_queue_result { + obj_pair objs; + bool never_recheck; + std::any collision_data; + void (*process_collision)( obj_pair *pair, const std::any& collision_data ); + }; + + std::atomic_size_t queue_length, result_length; + std::mutex queue_mutex, result_mutex; + std::unique_ptr> queue_load, queue_process; + std::unique_ptr> queue_results, queue_send; + + collision_thread_data() : + queue_length(0), + result_length(0), + queue_load(std::make_unique>()), + queue_process(std::make_unique>()), + queue_results(std::make_unique>()), + queue_send(std::make_unique>()) {} +}; + +std::unique_ptr collision_thread_data_buffer; +std::atomic_bool collision_processing_done = false; + +void spin_up_mp_collision() { + collision_processing_done.store(false); + threading::spin_up_threaded_task(threading::WorkerThreadTask::COLLISION); +} + +void spin_down_mp_collision() { + threading::spin_down_threaded_task(); + collision_processing_done.store(true); +} + +void queue_mp_collision(uint ctype, const obj_pair& colliding) { + size_t min_queue_length = std::numeric_limits::max(); + size_t target_thread = 0; + for (size_t i = 0; i < threading::get_num_workers(); i++) { + size_t queue_length = collision_thread_data_buffer[i].queue_length.load(std::memory_order_acquire); + if (queue_length == 0) { + target_thread = i; + break; + } + else if (queue_length < min_queue_length) { + target_thread = i; + min_queue_length = queue_length; + } + } + { + auto& thread = collision_thread_data_buffer[target_thread]; + std::scoped_lock lock(thread.queue_mutex); + thread.queue_load->emplace_back( collision_thread_data::collision_queue_item{colliding, ctype} ); + thread.queue_length.fetch_add(1, std::memory_order_release); + } +} + +void post_process_threaded_collisions() { + SCP_map workerThreads; + for (size_t i = 0; i < threading::get_num_workers(); i++) + workerThreads.emplace(i, 0); + + while (!workerThreads.empty()) { + for(auto& [i, processed] : workerThreads) { + auto& thread = collision_thread_data_buffer[i]; + + if (thread.result_length.load(std::memory_order_acquire) > processed) { + { + std::scoped_lock lock(thread.result_mutex); + thread.queue_results.swap(thread.queue_send); + } + for (auto& collision : *thread.queue_send) { + uint key = (OBJ_INDEX(collision.objs.a) << collision_cache_bitshift) + OBJ_INDEX(collision.objs.b); + collider_pair *collision_info = &Collision_cached_pairs[key]; + + if (collision.collision_data.has_value()) + collision.process_collision(&collision.objs, collision.collision_data); + + if (collision.never_recheck) { + collision_info->next_check_time = -1; + } else { + collision_info->next_check_time = collision.objs.next_check_time; + } + } + processed += thread.queue_send->size(); + thread.queue_send->clear(); + } + else if (thread.queue_length.load(std::memory_order_acquire) == 0) { + thread.queue_results->clear(); + workerThreads.erase(i); + break; + } + } + } + + spin_down_mp_collision(); +} + void obj_collide_pair(object *A, object *B) { TRACE_SCOPE(tracing::CollidePair); int (*check_collision)( obj_pair *pair ) = nullptr; int swapped = 0; + bool support_mp = false; if ( A==B ) return; // Don't check collisions with yourself @@ -752,9 +858,11 @@ void obj_collide_pair(object *A, object *B) case COLLISION_OF(OBJ_WEAPON,OBJ_SHIP): swapped = 1; check_collision = collide_ship_weapon; + support_mp = true; break; case COLLISION_OF(OBJ_SHIP, OBJ_WEAPON): check_collision = collide_ship_weapon; + support_mp = true; break; case COLLISION_OF(OBJ_DEBRIS, OBJ_WEAPON): check_collision = collide_debris_weapon; @@ -786,6 +894,10 @@ void obj_collide_pair(object *A, object *B) break; case COLLISION_OF(OBJ_SHIP,OBJ_SHIP): check_collision = collide_ship_ship; +#ifdef NDEBUG + //This is, due to debug prints, unfortunately only safe in release builds... + support_mp = true; +#endif break; case COLLISION_OF(OBJ_SHIP, OBJ_BEAM): @@ -971,12 +1083,17 @@ void obj_collide_pair(object *A, object *B) new_pair.b = B; new_pair.next_check_time = collision_info->next_check_time; - if ( check_collision(&new_pair) ) { - // don't have to check ever again - collision_info->next_check_time = -1; - } else { - collision_info->next_check_time = new_pair.next_check_time; - } + if (threading::is_threading() && support_mp) { + queue_mp_collision(ctype, new_pair); + } + else { + if (check_collision(&new_pair)) { + // don't have to check ever again + collision_info->next_check_time = -1; + } else { + collision_info->next_check_time = new_pair.next_check_time; + } + } } void obj_find_overlap_colliders(SCP_vector &overlap_list_out, SCP_vector &list, int axis, bool collide) @@ -1024,8 +1141,56 @@ void obj_find_overlap_colliders(SCP_vector &overlap_list_out, SCP_vectorempty() || thread.queue_length.load(std::memory_order_acquire) > 0 || !collision_processing_done.load(std::memory_order_acquire)) { + if (!thread.queue_process->empty()) { + + for (auto& collision_check : *thread.queue_process) { + collision_result (*check_collision)( obj_pair *pair ) = nullptr; + + switch( collision_check.ctype ) { + case COLLISION_OF(OBJ_WEAPON, OBJ_SHIP): + case COLLISION_OF(OBJ_SHIP, OBJ_WEAPON): + check_collision = collide_ship_weapon_check; + break; + case COLLISION_OF(OBJ_SHIP, OBJ_SHIP): + check_collision = collide_ship_ship_check; + break; + default: + UNREACHABLE("Got non MP-compatible collision type!"); + } + + auto&& [check_again, collision_data_maybe, collision_fnc] = check_collision(&collision_check.objs); + + { + std::scoped_lock lock{thread.result_mutex}; + thread.queue_results->emplace_back(collision_thread_data::collision_queue_result{collision_check.objs, check_again, collision_data_maybe, collision_fnc}); + } + thread.result_length.fetch_add(1, std::memory_order_release); + thread.queue_length.fetch_sub(1, std::memory_order_release); + } + thread.queue_process->clear(); + } + else if (thread.queue_length.load(std::memory_order_acquire) > 0) { + //We must have data in the load queue then. + std::scoped_lock lock(thread.queue_mutex); + thread.queue_load.swap(thread.queue_process); + thread.queue_load->clear(); + } + } +} + +void collide_init() { + if (threading::is_threading()) + collision_thread_data_buffer = std::make_unique(threading::get_num_workers()); +} + // used only in obj_sort_and_collide() static SCP_vector sort_list_y; static SCP_vector sort_list_z; @@ -1038,6 +1203,9 @@ void obj_sort_and_collide(SCP_vector* Collision_list) if ( !(Game_detail_flags & DETAIL_FLAG_COLLISION) ) return; + if (threading::is_threading()) + spin_up_mp_collision(); + if (!Collision_cache_stale_objects.empty()) { obj_collide_retime_stale_pairs(); } @@ -1068,6 +1236,9 @@ void obj_sort_and_collide(SCP_vector* Collision_list) obj_quicksort_colliders(&sort_list_z, 0, (int)(sort_list_z.size() - 1), 2); } obj_find_overlap_colliders(sort_list_y, sort_list_z, 2, true); + + if (threading::is_threading()) + post_process_threaded_collisions(); } void collide_apply_gravity_flags_weapons() { diff --git a/code/object/objcollide.h b/code/object/objcollide.h index ad8cef414e7..4f3b851d310 100644 --- a/code/object/objcollide.h +++ b/code/object/objcollide.h @@ -13,6 +13,8 @@ #define _COLLIDESTUFF_H #include "globalincs/pstypes.h" +#include +#include class object; struct CFILE; @@ -36,6 +38,7 @@ struct collision_info_struct { bool edge_hit; // if edge is hit, need to change collision normal bool submodel_move_hit; // if collision is against a moving submodel bool is_landing; //SUSHI: Maybe treat current collision as a landing + bool player_involved; }; //Collision physics constants @@ -57,9 +60,11 @@ struct obj_pair { object *a; object *b; int next_check_time; // a timestamp that when elapsed means to check for a collision - struct obj_pair *next; }; +//Never check again | data for collision post-processing | collision post-proc function +using collision_result = std::tuple; + extern SCP_vector Collision_sort_list; #define COLLISION_OF(a,b) (((a)<<8)|(b)) @@ -86,6 +91,7 @@ int weapon_will_never_hit( object *weapon, object *other, obj_pair * current_pai // CODE is locatated in CollideGeneral.cpp int collide_subdivide(vec3d *p0, vec3d *p1, float prad, vec3d *q0, vec3d *q1, float qrad); +void collide_init(); //=============================================================================== // SPECIFIC COLLISION DETECTION FUNCTIONS @@ -101,6 +107,9 @@ int collide_weapon_weapon( obj_pair * pair ); // CODE is locatated in CollideShipWeapon.cpp int collide_ship_weapon( obj_pair * pair ); +//Same as above, but for deferred collision processing / usage in multithreading +collision_result collide_ship_weapon_check( obj_pair * pair ); + // Checks debris-weapon collisions. pair->a is debris and pair->b is weapon. // Returns 1 if all future collisions between these can be ignored // CODE is locatated in CollideDebrisWeapon.cpp @@ -118,6 +127,10 @@ int collide_asteroid_weapon(obj_pair *pair); // Returns 1 if all future collisions between these can be ignored // CODE is locatated in CollideShipShip.cpp int collide_ship_ship( obj_pair * pair ); +//Same as above, but for deferred collision processing / usage in multithreading +collision_result collide_ship_ship_check( obj_pair * pair ); + +void collide_mp_worker_thread(size_t threadIdx); // Predictive functions. // Returns true if vector from curpos to goalpos with radius radius will collide with object goalobjp diff --git a/code/ship/shield.cpp b/code/ship/shield.cpp index 67f21de13b9..9bf985c4131 100644 --- a/code/ship/shield.cpp +++ b/code/ship/shield.cpp @@ -808,7 +808,7 @@ MONITOR(NumShieldHits) /** * Add data for a shield hit. */ -void add_shield_point(int objnum, int tri_num, vec3d *hit_pos, float radius_override) +void add_shield_point(int objnum, int tri_num, const vec3d *hit_pos, float radius_override) { if (Num_shield_points >= MAX_SHIELD_POINTS) return; diff --git a/code/ship/ship.h b/code/ship/ship.h index 24748a0a25c..5a6e3ae02da 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1766,7 +1766,7 @@ extern void create_shield_explosion(int objnum, int model_num, matrix *orient, v extern void shield_hit_init(); extern void create_shield_explosion_all(object *objp); extern void shield_frame_init(); -extern void add_shield_point(int objnum, int tri_num, vec3d *hit_pos, float radius_override); +extern void add_shield_point(int objnum, int tri_num, const vec3d *hit_pos, float radius_override); extern void add_shield_point_multi(int objnum, int tri_num, vec3d *hit_pos); extern void shield_point_multi_setup(); extern void shield_hit_close(); diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 7f6c3efa68a..601ae93841a 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -1704,6 +1704,8 @@ add_file_folder("Utils" utils/string_utils.cpp utils/string_utils.h utils/strings.h + utils/threading.cpp + utils/threading.h utils/tuples.h utils/unicode.cpp utils/unicode.h diff --git a/code/utils/RandomRange.h b/code/utils/RandomRange.h index 7ccf21c9e4a..ae7a24e75e1 100644 --- a/code/utils/RandomRange.h +++ b/code/utils/RandomRange.h @@ -79,12 +79,12 @@ class RandomRange { //Sampling a random_device is REALLY expensive. //Instead of sampling one for each seed, create a pseudorandom seeder which is initialized ONCE from a random_device. - inline static std::mt19937 seeder {std::random_device()()}; + inline static thread_local std::mt19937 seeder {std::random_device()()}; public: template =1 || !std::is_convertible::value) && !std::is_same_v, RandomRange>, int>::type> explicit RandomRange(T&& distributionFirstParameter, Ts&&... distributionParameters) - : m_generator(seeder()), m_distribution(distributionFirstParameter, distributionParameters...) + : m_generator(std::random_device()()), m_distribution(distributionFirstParameter, distributionParameters...) { m_minValue = static_cast(m_distribution.min()); m_maxValue = static_cast(m_distribution.max()); @@ -98,7 +98,7 @@ class RandomRange { m_constant = true; } - RandomRange() : m_generator(seeder()), m_distribution() + RandomRange() : m_generator(std::random_device()()), m_distribution() { m_minValue = static_cast(0.0); m_maxValue = static_cast(0.0); diff --git a/code/utils/threading.cpp b/code/utils/threading.cpp new file mode 100644 index 00000000000..11b405a144d --- /dev/null +++ b/code/utils/threading.cpp @@ -0,0 +1,82 @@ +#include "threading.h" + +#include "cmdline/cmdline.h" +#include "object/objcollide.h" +#include "globalincs/pstypes.h" + +#include +#include +#include +#include + +namespace threading { + std::condition_variable wait_for_task; + std::mutex wait_for_task_mutex; + bool wait_for_task_condition; + std::atomic worker_task; + + SCP_vector worker_threads; + + //Internal Functions + static void mp_worker_thread_main(size_t threadIdx) { + while(true) { + { + std::unique_lock lk(wait_for_task_mutex); + wait_for_task.wait(lk, []() { return wait_for_task_condition; }); + } + + switch (worker_task.load(std::memory_order_acquire)) { + case WorkerThreadTask::EXIT: + return; + case WorkerThreadTask::COLLISION: + collide_mp_worker_thread(threadIdx); + break; + default: + UNREACHABLE("Invalid threaded worker task!"); + } + } + } + + //External Functions + + void spin_up_threaded_task(WorkerThreadTask task) { + worker_task.store(task); + { + std::scoped_lock lock {wait_for_task_mutex}; + wait_for_task_condition = true; + wait_for_task.notify_all(); + } + } + + void spin_down_threaded_task() { + std::scoped_lock lock {wait_for_task_mutex}; + wait_for_task_condition = false; + } + + void init_task_pool() { + if (!is_threading()) + return; + + mprintf(("Spinning up threadpool with %d threads...\n", Cmdline_multithreading - 1)); + + for (size_t i = 0; i < static_cast(Cmdline_multithreading - 1); i++) { + worker_threads.emplace_back([i](){ mp_worker_thread_main(i); }); + } + } + + void shut_down_task_pool() { + spin_up_threaded_task(WorkerThreadTask::EXIT); + + for(auto& thread : worker_threads) { + thread.join(); + } + } + + bool is_threading() { + return Cmdline_multithreading > 1; + } + + size_t get_num_workers() { + return worker_threads.size(); + } +} \ No newline at end of file diff --git a/code/utils/threading.h b/code/utils/threading.h new file mode 100644 index 00000000000..ab33e655bbd --- /dev/null +++ b/code/utils/threading.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace threading { + enum class WorkerThreadTask : uint8_t { EXIT, COLLISION }; + + //Call this to start a task on the task pool. Note that task-specific data must be set up before calling this. + void spin_up_threaded_task(WorkerThreadTask task); + + //This _must_ be called on the main thread BEFORE a task completes on a thread of the task pool. + void spin_down_threaded_task(); + + void init_task_pool(); + void shut_down_task_pool(); + + bool is_threading(); + size_t get_num_workers(); +} \ No newline at end of file diff --git a/freespace2/freespace.cpp b/freespace2/freespace.cpp index cb11df5ec2d..ccc3998b946 100644 --- a/freespace2/freespace.cpp +++ b/freespace2/freespace.cpp @@ -193,6 +193,7 @@ #include "tracing/Monitor.h" #include "tracing/tracing.h" #include "utils/Random.h" +#include "utils/threading.h" #include "weapon/beam.h" #include "weapon/emp.h" #include "weapon/flak.h" @@ -1757,6 +1758,8 @@ void game_init() // init os stuff next os_init( Osreg_class_name, Window_title.c_str(), Osreg_app_name ); + threading::init_task_pool(); + #ifndef NDEBUG mprintf(("FreeSpace 2 Open version: %s\n", FS_VERSION_FULL)); @@ -2006,7 +2009,8 @@ void game_init() // Initialize SEXPs. Must happen before ship init for LuaAI sexp_startup(); - obj_init(); + obj_init(); + collide_init(); mflash_game_init(); armor_init(); ai_init(); @@ -7023,6 +7027,8 @@ void game_shutdown(void) } lcl_xstr_close(); + + threading::shut_down_task_pool(); } // game_stop_looped_sounds() From 8e64845cf10915a2ade294574e1b3df6c978668a Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Sat, 12 Jul 2025 21:02:40 +0200 Subject: [PATCH 244/466] Make sure to close the g3 frame (#6825) --- code/radar/radarorb.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/radar/radarorb.cpp b/code/radar/radarorb.cpp index 72af47f99ec..f8a51e46427 100644 --- a/code/radar/radarorb.cpp +++ b/code/radar/radarorb.cpp @@ -426,6 +426,9 @@ void HudGaugeRadarOrb::render(float /*frametime*/, bool config) // For now config view stops here but it should be doable to have // this render the orb outlines using the next two functions eventually if (config) { + if(g3_yourself) + g3_end_frame(); + return; } From e99995497cea3ff635429d1f8393985ce52a5dc3 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 12 Jul 2025 15:30:49 -0400 Subject: [PATCH 245/466] Revert "Prepare to sync 2025-07-12" This reverts commit 6ccb856f78dcfc8004ba9a56929f0c8e8c16d005 (PR #31), with appropriate conflicts resolved. --- code/mission/missionparse.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index b71e5b62438..33d13d401a8 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6922,7 +6922,7 @@ bool parse_main(const char *mission_name, int flags) do { // don't do this for imports - if (!(flags & MPF_IMPORT_FSM)) { + if (!(flags & MPF_IMPORT_FSM) && !(flags & MPF_IMPORT_XWI)) { CFILE *ftemp = cfopen(mission_name, "rt", CF_TYPE_MISSIONS); // fail situation. From 6c35dac53512112968d578ac10c0eada9ced17ce Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Sat, 12 Jul 2025 22:19:23 +0200 Subject: [PATCH 246/466] Allow autodetection of number of cores for threading (#6824) * Allow autodetection of threads * cleanup --- code/cmdline/cmdline.cpp | 4 -- code/utils/threading.cpp | 104 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 96 insertions(+), 12 deletions(-) diff --git a/code/cmdline/cmdline.cpp b/code/cmdline/cmdline.cpp index dcdbe0c8abf..7dae2532cab 100644 --- a/code/cmdline/cmdline.cpp +++ b/code/cmdline/cmdline.cpp @@ -2411,10 +2411,6 @@ bool SetCmdlineParams() if (multithreading.found()) { Cmdline_multithreading = abs(multithreading.get_int()); - if (Cmdline_multithreading < 1) { - Cmdline_multithreading = 1; - Warning(LOCATION,"-threads must be an integer greater or equal to 1. Invalid thread count will be disregarded."); - } } return true; diff --git a/code/utils/threading.cpp b/code/utils/threading.cpp index 11b405a144d..69e576b5ae2 100644 --- a/code/utils/threading.cpp +++ b/code/utils/threading.cpp @@ -9,13 +9,18 @@ #include #include +#ifdef WIN32 +#include +#endif + namespace threading { - std::condition_variable wait_for_task; - std::mutex wait_for_task_mutex; - bool wait_for_task_condition; - std::atomic worker_task; + static size_t num_threads = 1; + static std::condition_variable wait_for_task; + static std::mutex wait_for_task_mutex; + static bool wait_for_task_condition; + static std::atomic worker_task; - SCP_vector worker_threads; + static SCP_vector worker_threads; //Internal Functions static void mp_worker_thread_main(size_t threadIdx) { @@ -37,6 +42,82 @@ namespace threading { } } + static size_t get_number_of_physical_cores_fallback() { + unsigned int hardware_threads = std::thread::hardware_concurrency(); + if (hardware_threads > 0) { + return hardware_threads; + } + else { + Warning(LOCATION, "Could not autodetect available number of threads! Disabling multithreading..."); + return 1; + } + } + + //We don't want to rely on std::thread::hardware_concurrency() unless we have to, as it reports threads, not physical cores, and FSO doesn't gain much from hyperthreaded threads at the moment. +#ifdef WIN32 + static size_t get_number_of_physical_cores() { + auto glpi = (BOOL (WINAPI *)(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION, PDWORD)) GetProcAddress( + GetModuleHandle(TEXT("kernel32")), + "GetLogicalProcessorInformation"); + + if (glpi == nullptr) + return get_number_of_physical_cores_fallback(); + + DWORD length = 0; + glpi(nullptr, &length); + SCP_vector infoBuffer(length / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION)); + DWORD error = glpi(infoBuffer.data(), &length); + + if (error != 0) + return get_number_of_physical_cores_fallback(); + + size_t num_cores = 0; + for (const auto& info : infoBuffer) { + if (info.Relationship == RelationProcessorCore && info.ProcessorMask != 0) + num_cores++; + } + + if (num_cores < 1) { + //invalid results, try fallback + return get_number_of_physical_cores_fallback(); + } + else { + return num_cores; + } + } +#elif defined SCP_UNIX + static size_t get_number_of_physical_cores() { + try { + std::ifstream cpuinfo("/proc/cpuinfo"); + SCP_string line; + while (std::getline(cpuinfo, line)) { + //Looking for a cpu cores property is fine assuming a user has only one physical CPU socket. If they have multiple CPU's, this'll underreport the core count, but that should be very rare in typical configurations + if (line.find("cpu cores") != SCP_string::npos){ + size_t numberpos = line.find(": "); + if (numberpos == SCP_string::npos) + return get_number_of_physical_cores_fallback(); + + int num_cores = std::stoi(line.substr(numberpos + 2)); + + if (num_cores < 1) { + //invalid results, try fallback + return get_number_of_physical_cores_fallback(); + } + else { + return num_cores; + } + } + } + return get_number_of_physical_cores_fallback(); + } + catch (const std::exception&) { + return get_number_of_physical_cores_fallback(); + } + } +#else +#define get_number_of_physical_cores() get_number_of_physical_cores_fallback() +#endif + //External Functions void spin_up_threaded_task(WorkerThreadTask task) { @@ -54,12 +135,19 @@ namespace threading { } void init_task_pool() { + if (Cmdline_multithreading == 0) { + num_threads = get_number_of_physical_cores() - 1; + } + else { + num_threads = Cmdline_multithreading - 1; + } + if (!is_threading()) return; - mprintf(("Spinning up threadpool with %d threads...\n", Cmdline_multithreading - 1)); + mprintf(("Spinning up threadpool with %d threads...\n", static_cast(num_threads))); - for (size_t i = 0; i < static_cast(Cmdline_multithreading - 1); i++) { + for (size_t i = 0; i < num_threads; i++) { worker_threads.emplace_back([i](){ mp_worker_thread_main(i); }); } } @@ -73,7 +161,7 @@ namespace threading { } bool is_threading() { - return Cmdline_multithreading > 1; + return num_threads > 0; } size_t get_num_workers() { From 22feeddc6fd4e9d5d2229714e38d4c3b401ed638 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 12 Jul 2025 18:40:08 -0400 Subject: [PATCH 247/466] ensure SEXP help has appropriate newlines or spaces Some SEXP help did not have newlines separating what should be separate lines of text, or spaces separating what should be separate words. Add newlines or spaces where appropriate. (Regexes are A-1 SUPAR.) --- code/parse/sexp.cpp | 112 ++++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 909ee74f813..2daf908ea62 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -37467,7 +37467,7 @@ SCP_vector Sexp_help = { "\t1:\tThe name of the navpoint" }, { OP_NAV_DISTANCE, "distance-to-nav\r\n" - "Returns the distance from the center of the player ship to a nav point. Takes 1 argument..." + "Returns the distance from the center of the player ship to a nav point. Takes 1 argument...\r\n" "\t1:\tThe name of the navpoint" }, { OP_NAV_ADD_WAYPOINT, "add-nav-waypoint\r\n" @@ -37530,12 +37530,12 @@ SCP_vector Sexp_help = { "\t1:\tShips to mark (ships must be in-mission)\r\n" }, { OP_NAV_ISLINKED, "is-nav-linked\r\n" - "Determines if a ship is linked for autopilot (\"set-nav-carry\" or \"set-nav-needslink\" + linked)" + "Determines if a ship is linked for autopilot (\"set-nav-carry\" or \"set-nav-needslink\" + linked).\r\n" "Takes 1 argument...\r\n" "\t1:\tShip to check (evaluation returns NAN until ship is in-mission)\r\n"}, { OP_NAV_USECINEMATICS, "use-nav-cinematics\r\n" - "Controls the use of the cinematic autopilot camera. Takes 1 Argument..." + "Controls the use of the cinematic autopilot camera. Takes 1 Argument...\r\n" "\t1:\tSet to true to enable automatic cinematics, set to false to disable automatic cinematics." }, { OP_NAV_USEAP, "use-autopilot\r\n" @@ -37640,7 +37640,7 @@ SCP_vector Sexp_help = { "\tPerforms the bitwise XOR operator on its arguments. This is the same as if the logical XOR operator was performed on each successive bit. Takes 2 or more numeric arguments.\r\n" }, { OP_ANGLE_VECTORS, "angle-vectors\r\n" - "\tCalculates the angle between two vectors." + "\tCalculates the angle between two vectors. " "Takes 6 arguments...\r\n" "\t1: The x component of the first vector.\r\n" "\t2: The y component of the first vector.\r\n" @@ -37650,40 +37650,40 @@ SCP_vector Sexp_help = { "\t6: The z component of the second vector.\r\n"}, { OP_SET_OBJECT_SPEED_X, "set-object-speed-x (deprecated in favor of ship-maneuver)\r\n" - "\tSets the X speed of a ship or wing (ship/wing must be in-mission)." + "\tSets the X speed of a ship or wing (ship/wing must be in-mission). " "Takes 2 or 3 arguments...\r\n" "\t1: The name of the object.\r\n" "\t2: The speed to set.\r\n" "\t3: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" }, { OP_SET_OBJECT_SPEED_Y, "set-object-speed-y (deprecated in favor of ship-maneuver)\r\n" - "\tSets the Y speed of a ship or wing (ship/wing must be in-mission)." + "\tSets the Y speed of a ship or wing (ship/wing must be in-mission). " "Takes 2 or 3 arguments...\r\n" "\t1: The name of the object.\r\n" "\t2: The speed to set.\r\n" "\t3: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" }, { OP_SET_OBJECT_SPEED_Z, "set-object-speed-z (deprecated in favor of ship-maneuver)\r\n" - "\tSets the Z speed of a ship or wing (ship/wing must be in-mission)." + "\tSets the Z speed of a ship or wing (ship/wing must be in-mission). " "Takes 2 or 3 arguments...\r\n" "\t1: The name of the object.\r\n" "\t2: The speed to set.\r\n" "\t3: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" }, { OP_GET_OBJECT_SPEED_X, "get-object-speed-x\r\n" - "\tReturns the X speed of a ship or wing as an integer (evaluation returns NAN until ship/wing is in-mission)." + "\tReturns the X speed of a ship or wing as an integer (evaluation returns NAN until ship/wing is in-mission). " "Takes 2 or 3 arguments...\r\n" "\t1: The name of the object.\r\n" "\t2: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" }, { OP_GET_OBJECT_SPEED_Y, "get-object-speed-y\r\n" - "\tReturns the Y speed of a ship or wing as an integer (evaluation returns NAN until ship/wing is in-mission)." + "\tReturns the Y speed of a ship or wing as an integer (evaluation returns NAN until ship/wing is in-mission). " "Takes 2 or 3 arguments...\r\n" "\t1: The name of the object.\r\n" "\t2: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" }, { OP_GET_OBJECT_SPEED_Z, "get-object-speed-z\r\n" - "\tReturns the Z speed of a ship or wing as an integer (evaluation returns NAN until ship/wing is in-mission)." + "\tReturns the Z speed of a ship or wing as an integer (evaluation returns NAN until ship/wing is in-mission). " "Takes 2 or 3 arguments...\r\n" "\t1: The name of the object.\r\n" "\t2: Whether the speed on the axis should be set according to the universe grid (when false) or according to the object's facing (when true); You almost always want to set this to true; (optional; defaults to false).\r\n" }, @@ -37726,7 +37726,7 @@ SCP_vector Sexp_help = { // Goober5000 { OP_SET_OBJECT_POSITION, "set-object-position\r\n" - "\tInstantaneously sets an object's spatial coordinates." + "\tInstantaneously sets an object's spatial coordinates. " "Takes 4 arguments...\r\n" "\t1: The name of a ship, wing, or waypoint (object does not need to be in-mission).\r\n" "\t2: The new X coordinate.\r\n" @@ -37750,7 +37750,7 @@ SCP_vector Sexp_help = { // Goober5000 { OP_SET_OBJECT_ORIENTATION, "set-object-orientation\r\n" - "\tInstantaneously sets an object's spatial orientation." + "\tInstantaneously sets an object's spatial orientation. " "Takes 4 arguments...\r\n" "\t1: The name of a ship or wing (ship/wing does not need to be in-mission).\r\n" "\t2: The new pitch angle, in degrees. The angle can be any number; it does not have to be between 0 and 360.\r\n" @@ -37966,8 +37966,8 @@ SCP_vector Sexp_help = { { OP_GOAL_INCOMPLETE, "Mission Goal Incomplete (Boolean operator)\r\n" "\tReturns true if the specified goal in the this mission is incomplete. This " - "sexpression will only be useful in conjunction with another sexpression like" - "has-time-elapsed. Used alone, it will return true upon mission startup." + "sexpression will only be useful in conjunction with another sexpression like " + "has-time-elapsed. Used alone, it will return true upon mission startup.\r\n" "Returns a boolean value. Takes 1 argument...\r\n" "\t1:\tName of the event in the mission."}, @@ -38009,19 +38009,19 @@ SCP_vector Sexp_help = { { OP_EVENT_INCOMPLETE, "Mission Event Incomplete (Boolean operator)\r\n" "\tReturns true if the specified event in the this mission is incomplete. This " - "sexpression will only be useful in conjunction with another sexpression like" - "has-time-elapsed. Used alone, it will return true upon mission startup." + "sexpression will only be useful in conjunction with another sexpression like " + "has-time-elapsed. Used alone, it will return true upon mission startup.\r\n" "Returns a boolean value. Takes 1 argument...\r\n" "\t1:\tName of the event in the mission."}, { OP_RESET_EVENT, "Reset-Event (Action operator)\r\n" - "Clears all information associated with an event, resetting SEXP nodes and status flags so that it is as if the event had never been evaluated." + "Clears all information associated with an event, resetting SEXP nodes and status flags so that it is as if the event had never been evaluated. " "Takes 1 or more arguments...\r\n" "\tAll:\tName of the event" }, { OP_RESET_GOAL, "Reset-Goal (Action operator)\r\n" - "Clears all information associated with a goal, resetting SEXP nodes and status flags so that it is as if the goal had never been evaluated." + "Clears all information associated with a goal, resetting SEXP nodes and status flags so that it is as if the goal had never been evaluated. " "Takes 1 or more arguments...\r\n" "\tAll:\tName of the goal" }, @@ -38816,7 +38816,7 @@ SCP_vector Sexp_help = { "are defined in messages.tbl. For example, this can be used to make a cruiser send a Help message.\r\n\r\n" "Takes 4 or more arguments...\r\n" "\t1:\tThe type of message to send.\r\n" - "\t2:\tThe message's subject (used with message filters). If you don't know what this means, set it to ." + "\t2:\tThe message's subject (used with message filters). If you don't know what this means, set it to .\r\n" "\t3:\tPick a random sender? If this is false, the first available sender will be used.\r\n" "\tRest:\tWho should send the message - a ship, a wing, #Command, or .\r\n" }, @@ -38848,7 +38848,7 @@ SCP_vector Sexp_help = { { OP_SET_PERSONA, "Set Persona (Action operator)\r\n" "\tSets the persona of the supplied ship to the persona supplied\r\n" "Takes 2 or more arguments...\r\n" - "\t1:\tPersona to use." + "\t1:\tPersona to use.\r\n" "\tRest:\tName of the ship (ship must be in-mission)." }, { OP_SELF_DESTRUCT, "Self destruct (Action operator)\r\n" @@ -38886,8 +38886,8 @@ SCP_vector Sexp_help = { }, { OP_SABOTAGE_SUBSYSTEM, "Sabotage subystem (Action operator)\r\n" - "\tReduces the specified subsystem integrity by the specified percentage." - "If the percntage strength of the subsystem (after completion) is less than 0%," + "\tReduces the specified subsystem integrity by the specified percentage. " + "If the percentage strength of the subsystem (after completion) is less than 0%, the " "subsystem strength is set to 0%.\r\n\r\n" "Takes 3 arguments...\r\n" "\t1:\tName of ship subsystem is on (ship must be in-mission).\r\n" @@ -38895,8 +38895,8 @@ SCP_vector Sexp_help = { "\t3:\tPercentage to reduce subsystem integrity by." }, { OP_REPAIR_SUBSYSTEM, "Repair Subystem (Action operator)\r\n" - "\tIncreases the specified subsystem integrity by the specified percentage." - "If the percentage strength of the subsystem (after completion) is greater than 100%," + "\tIncreases the specified subsystem integrity by the specified percentage. " + "If the percentage strength of the subsystem (after completion) is greater than 100%, the " "subsystem strength is set to 100%.\r\n\r\n" "Takes 3 to 5 arguments...\r\n" "\t1:\tName of ship subsystem is on (ship must be in-mission).\r\n" @@ -38906,9 +38906,9 @@ SCP_vector Sexp_help = { "\t5:\tIf we are repairing submodels and an ancestor submodel was totally destroyed, repair that too. Optional argument that defaults to true.\r\n" }, { OP_SET_SUBSYSTEM_STRNGTH, "Set Subsystem Strength (Action operator)\r\n" - "\tSets the specified subsystem to the the specified percentage." + "\tSets the specified subsystem to the the specified percentage. " "If the percentage specified is < 0, strength is set to 0. If the percentage is " - "> 100 % the subsystem strength is set to 100%.\r\n\r\n" + "greater than 100%, the subsystem strength is set to 100%.\r\n\r\n" "Takes 3 to 5 arguments...\r\n" "\t1:\tName of ship subsystem is on (ship must be in-mission).\r\n" "\t2:\tName of subsystem to set strength.\r\n" @@ -38917,7 +38917,7 @@ SCP_vector Sexp_help = { "\t5:\tIf we are repairing submodels and an ancestor submodel was totally destroyed, repair that too. Optional argument that defaults to true.\r\n" }, { OP_DESTROY_SUBSYS_INSTANTLY, "destroy-subsys-instantly\r\n" - "\tDestroys the specified subsystems without effects." + "\tDestroys the specified subsystems without effects. " "Takes 2 or more arguments...\r\n" "\t1:\tName of ship subsystem is on (ship must be in-mission).\r\n" "\tRest:\tName of subsystem to destroy.\r\n"}, @@ -38939,7 +38939,7 @@ SCP_vector Sexp_help = { "character of the first argument a \"#\".\r\n\r\n" "Takes 3 or more arguments...\r\n" "\t1:\tName of who the message is from.\r\n" - "\t2:\tPriority of message (\"Low\", \"Normal\" or \"High\")." + "\t2:\tPriority of message (\"Low\", \"Normal\" or \"High\").\r\n" "\tRest:\tName of message (from message list)." }, { OP_TRANSFER_CARGO, "Transfer Cargo (Action operator)\r\n" @@ -39737,7 +39737,7 @@ SCP_vector Sexp_help = { "\tReturns true if all of the specified objects' cargo is known by the player (i.e. they " "have scanned each one.\r\n\r\n" "Returns a boolean value after seconds when all cargo is known. Takes 2 or more arguments...\r\n" - "\t1:\tDelay in seconds after which sexpression will return true when all cargo scanned." + "\t1:\tDelay in seconds after which sexpression will return true when all cargo scanned.\r\n" "\tRest:\tNames of ships/cargo to check for cargo known." }, { OP_WAS_PROMOTION_GRANTED, "Was promotion granted (Boolean operator)\r\n" @@ -39833,13 +39833,13 @@ SCP_vector Sexp_help = { { OP_CHANGE_PLAYER_SCORE, "Change Player Score (Action operator)\r\n" "\tThis operator allows direct alteration of the player's score for this mission.\r\n\r\n" - "Takes 2 or more arguments." + "Takes 2 or more arguments...\r\n" "\t1:\tAmount to alter the player's score by.\r\n" "\tRest:\tName of ship the player is flying."}, { OP_CHANGE_TEAM_SCORE, "Change Team Score (Action operator)\r\n" "\tThis operator allows direct alteration of the team's score for a TvT mission (Does nothing otherwise).\r\n\r\n" - "Takes 2 arguments." + "Takes 2 arguments...\r\n" "\t1:\tAmount to alter the team's score by.\r\n" "\t2:\tThe team to alter the score for. (0 will add the score to all teams!)"}, @@ -40029,15 +40029,15 @@ SCP_vector Sexp_help = { // Goober5000 { OP_FRIENDLY_STEALTH_INVISIBLE, "friendly-stealth-invisible\r\n" - "\tCauses the friendly ships listed in this sexpression to be invisible to radar, just like hostile stealth ships." - "It doesn't matter if the ship is friendly at the time this sexp executes: as long as it is a stealth ship, it will" + "\tCauses the friendly ships listed in this sexpression to be invisible to radar, just like hostile stealth ships. " + "It doesn't matter if the ship is friendly at the time this sexp executes: as long as it is a stealth ship, it will " "be invisible to radar both as hostile and as friendly.\r\n\r\n" "Takes 1 or more arguments...\r\n" "\tAll:\tName of ships (ships do not need to be in-mission)" }, // Goober5000 { OP_FRIENDLY_STEALTH_VISIBLE, "friendly-stealth-visible\r\n" - "\tCauses the friendly ships listed in this sexpression to resume their normal behavior of being visible to radar as" + "\tCauses the friendly ships listed in this sexpression to resume their normal behavior of being visible to radar as " "stealth friendlies. Does not affect their visibility as stealth hostiles.\r\n\r\n" "Takes 1 or more arguments...\r\n" "\tAll:\tName of ships (ships do not need to be in-mission)" }, @@ -40062,7 +40062,7 @@ SCP_vector Sexp_help = { "Takes 3 or more arguments...\r\n" "\t1:\tName of a ship (ship must be in-mission)\r\n" "\t2:\tTrue = Do not render or False = render if exists\r\n" - "\tRest: Name of the ship's subsystem(s)" + "\tRest: Name of the ship's subsystem(s)\r\n" "\tNote: If subsystem is already dead it will vanish or reappear out of thin air" }, @@ -40081,7 +40081,7 @@ SCP_vector Sexp_help = { "Takes 3 or more arguments...\r\n" "\t1:\tName of a ship (ship must be in-mission)\r\n" "\t2:\tTrue = vanish or False = don't vanish\r\n" - "\tRest: Name of the ship's subsystem(s)" + "\tRest: Name of the ship's subsystem(s)\r\n" "\tNote: Useful for replacing subsystems with actual docked models." }, // FUBAR @@ -40431,11 +40431,11 @@ SCP_vector Sexp_help = { "\tOnly really useful for multiplayer."}, { OP_CLEAR_WEAPONS, "clear-weapons\r\n" - "\tRemoves all live weapons currently in the mission" + "\tRemoves all live weapons currently in the mission.\r\n" "\t1: (Optional) Remove only this specific class of weapon\r\n"}, { OP_CLEAR_DEBRIS, "clear-debris\r\n" - "\tRemoves all ship debris currently in the mission" + "\tRemoves all ship debris currently in the mission.\r\n" "\t1: (Optional) Remove only debris from this specific class of ship\r\n"}, { OP_SET_RESPAWNS, "set-respawns\r\n" @@ -40549,7 +40549,7 @@ SCP_vector Sexp_help = { "\trest: Priorities to set (max 32) or blank for no priorities\r\n"}, { OP_TURRET_SET_INACCURACY, "turret-set-inaccuracy\r\n" - "\tMakes the specified turrets more inaccurate by firing their shots in a cone, like field of fire." + "\tMakes the specified turrets more inaccurate by firing their shots in a cone, like field of fire. " "This will only decrease their accuracy, it cannot make the weapons more accurate than normal.\r\n" "\tDoes not work on beams.\r\n" "\t1: Ship turret(s) are on (ship must be in-mission)\r\n" @@ -41268,10 +41268,10 @@ SCP_vector Sexp_help = { { OP_SET_POST_EFFECT, "set-post-effect\r\n" "\tConfigures a post-processing effect. Takes 2 arguments...\r\n" "\t1: Effect type\r\n" - "\t2: Effect intensity (0 - 100)." - "\t3: (Optional) Red (0 - 255)." - "\t4: (Optional) Green (0 - 255)." - "\t5: (Optional) Blue (0 - 255)." + "\t2: Effect intensity (0 - 100)\r\n" + "\t3: (Optional) Red (0 - 255)\r\n" + "\t4: (Optional) Green (0 - 255)\r\n" + "\t5: (Optional) Blue (0 - 255)\r\n" }, { OP_RESET_POST_EFFECTS, "reset-post-effects\r\n" @@ -41318,8 +41318,8 @@ SCP_vector Sexp_help = { { OP_HUD_DISPLAY_GAUGE, "hud-display-gauge \r\n" "\tCauses specified hud gauge to appear or disappear for so many milliseconds. Takes 1 argument...\r\n" - "\t1: Number of milliseconds that the warpout gauge should appear on the HUD." - " 0 will immediately cause the gauge to disappear.\r\n" + "\t1: Number of milliseconds that the warpout gauge should appear on the HUD. Zero " + "will immediately cause the gauge to disappear.\r\n" "\t2: Name of HUD element. Must be one of:\r\n" "\t\t" SEXP_HUD_GAUGE_WARPOUT " - the \"Subspace drive active\" box that appears above the viewscreen.\r\n" }, @@ -41336,9 +41336,10 @@ SCP_vector Sexp_help = { // Kestrellius { OP_SET_FRIENDLY_DAMAGE_CAPS, "set-friendly-damage-caps\r\n" - "\tSets limits on damage weapons and beams can do to friendly targets on the current difficulty level. Takes 1 to 3 arguments.\r\nArguments left blank will leave the values unmodified.\r\n" - "\t1:\tMaximum damage beams can do to targets on the same team as the firer. -1 means no limit." - "\t2:\tMaximum damage weapons (and their shockwaves) can do to targets on the same team as the firer. -1 means no limit." + "\tSets limits on damage weapons and beams can do to friendly targets on the current difficulty level.\r\n" + "Takes 1 to 3 arguments. Arguments left blank will leave the values unmodified.\r\n" + "\t1:\tMaximum damage beams can do to targets on the same team as the firer. -1 means no limit.\r\n" + "\t2:\tMaximum damage weapons (and their shockwaves) can do to targets on the same team as the firer. -1 means no limit.\r\n" "\t3:\tMaximum damage weapons (and their shockwaves) can do to their firer. -1 means no limit." }, @@ -41392,7 +41393,7 @@ SCP_vector Sexp_help = { "\tSets the text value of a given HUD gauge to a translated string and replaces variables.\r\n" "\tWorks for custom gauges only. Takes 3 arguments...\r\n" "\t1:\tHUD gauge to be modified\r\n" - "\t2:\tText to be set" + "\t2:\tText to be set\r\n" "\t3:\tXSTR ID to lookup" }, @@ -41602,9 +41603,9 @@ SCP_vector Sexp_help = { { OP_CUTSCENES_SET_CAMERA_HOST, "set-camera-host\r\n" "\tSets the object and subystem camera should view from. Camera position is offset from the host. " - "If the selected subsystem or one of its children has an eyepoint bound to it it will be used for the camera position and orientation." - "If the selected subsystem is a turret and has no eyepoint the camera will be at the first firing point and look along the firing direction." - "If a valid camera target is set the direction to the target will override any other orientation." + "If the selected subsystem or one of its children has an eyepoint bound to it it will be used for the camera position and orientation. " + "If the selected subsystem is a turret and has no eyepoint the camera will be at the first firing point and look along the firing direction. " + "If a valid camera target is set the direction to the target will override any other orientation. " "Takes 1 to 2 arguments...\r\n" "\t1:\tShip to mount camera on\r\n" "\t(optional)\r\n" @@ -41947,13 +41948,13 @@ SCP_vector Sexp_help = { }, {OP_SCRIPT_EVAL_BOOL, "script-eval-bool\r\n" - "\tEvaluates the concatenation of all arguments as a single script that returns a boolean" + "\tEvaluates the concatenation of all arguments as a single script that returns a boolean. " "Takes at least 1 argument...\r\n" "\tAll:\tScript\r\n" }, {OP_SCRIPT_EVAL_NUM, "script-eval-num\r\n" - "\tEvaluates the concatenation of all arguments as a single script that returns a number" + "\tEvaluates the concatenation of all arguments as a single script that returns a number. " "Takes at least 1 argument...\r\n" "\tAll:\tScript\r\n" }, @@ -42172,8 +42173,7 @@ SCP_vector Sexp_help = { }, { OP_OVERRIDE_MOTION_DEBRIS, "set-motion-debris-override\r\n" - "\tControls whether or not motion debris should be active.\r\n" - "\tThis overrides any choice made by the user through the -nomotiondebris commandline flag." + "\tControls whether or not motion debris should be active. This overrides any choice made by the user through the -nomotiondebris commandline flag. " "Takes 1 argument...\r\n" "\t1:\tBoolean: True will disable motion debris, False reenable it.\r\n" }, From 7811207ca922e997e1a6534bd477f20cd0981ae4 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Sun, 13 Jul 2025 17:19:32 -0400 Subject: [PATCH 248/466] fix decal shader compile error (#6829) On macOS the new decal shaders (#6813) fail to compile with the following message: "interpolation qualifier 'flat' must precede storage qualifiers". --- code/def_files/data/effects/decal-f.sdr | 10 +++++----- code/def_files/data/effects/decal-v.sdr | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/code/def_files/data/effects/decal-f.sdr b/code/def_files/data/effects/decal-f.sdr index 852e1d2b5bc..9b2e8bbbf18 100644 --- a/code/def_files/data/effects/decal-f.sdr +++ b/code/def_files/data/effects/decal-f.sdr @@ -10,11 +10,11 @@ out vec4 fragOut0; // Diffuse buffer out vec4 fragOut1; // Normal buffer out vec4 fragOut2; // Emissive buffer -in flat mat4 invModelMatrix; -in flat vec3 decalDirection; -in flat float normal_angle_cutoff; -in flat float angle_fade_start; -in flat float alpha_scale; +flat in mat4 invModelMatrix; +flat in vec3 decalDirection; +flat in float normal_angle_cutoff; +flat in float angle_fade_start; +flat in float alpha_scale; uniform sampler2D gDepthBuffer; uniform sampler2D gNormalBuffer; diff --git a/code/def_files/data/effects/decal-v.sdr b/code/def_files/data/effects/decal-v.sdr index cebe64844c6..6a62549bd62 100644 --- a/code/def_files/data/effects/decal-v.sdr +++ b/code/def_files/data/effects/decal-v.sdr @@ -2,11 +2,11 @@ in vec4 vertPosition; in mat4 vertModelMatrix; -out flat mat4 invModelMatrix; -out flat vec3 decalDirection; -out flat float normal_angle_cutoff; -out flat float angle_fade_start; -out flat float alpha_scale; +flat out mat4 invModelMatrix; +flat out vec3 decalDirection; +flat out float normal_angle_cutoff; +flat out float angle_fade_start; +flat out float alpha_scale; layout (std140) uniform decalGlobalData { mat4 viewMatrix; From 3be99b2480b70199a41c9c25574ec7d32f482e35 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 13 Jul 2025 20:02:06 -0400 Subject: [PATCH 249/466] fix play-sound-from-file with multiple variable-controlled streams Since this sexp was implemented, there has been a bug that prevented multiple variable-controlled audio streams from working. One default and one variable-controlled stream could coexist, but any subsequent variable-controlled stream would reuse a previous stream's handle if the variable's current value was a valid handle. This adjusts the code to properly create a new handle for every invocation of the sexp with a variable. The following cases have been tested and now all work properly: 1. The default handle by itself 2. One variable handle by itself 3. One variable handle with one default handle 4. Multiple variable handles 5. Multiple variable handles with one default handle Follow-up to #2115. --- code/parse/sexp.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 2daf908ea62..91565086b61 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -14059,17 +14059,19 @@ void sexp_load_music(const char *filename, int type = -1, int sexp_var = -1) if (Sexp_music_handles.empty()) Sexp_music_handles.push_back(-1); - int index = sexp_find_music_handle_index(sexp_var); - - // since we know the default index 0 exists, this means we have a variable without an index - if (index < 0) + // if a variable is supplied, we'll be creating a new handle to be stored in the variable + int index; + if (sexp_var >= 0) { - index = (int)Sexp_music_handles.size(); + index = static_cast(Sexp_music_handles.size()); Sexp_music_handles.push_back(-1); } - - // if we were previously playing music on this handle, stop it - audiostream_close_file(Sexp_music_handles[index]); + // otherwise we'll be reusing the default handle, so close anything that's already playing + else + { + index = 0; + audiostream_close_file(Sexp_music_handles[index]); + } // open the stream and save the handle in our list Sexp_music_handles[index] = audiostream_open(filename, type); From d58d924f3e69ce36bd83537d186453d97915ad95 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sun, 13 Jul 2025 19:23:02 -0500 Subject: [PATCH 250/466] Get, set, browse team colors --- code/scripting/api/libs/tables.cpp | 33 ++++++ code/scripting/api/objs/parse_object.cpp | 37 ++++--- code/scripting/api/objs/ship.cpp | 34 ++++++ code/scripting/api/objs/shipclass.cpp | 68 ++++++++---- code/scripting/api/objs/team_colors.cpp | 134 +++++++++++++++++++++++ code/scripting/api/objs/team_colors.h | 14 +++ code/source_groups.cmake | 2 + 7 files changed, 287 insertions(+), 35 deletions(-) create mode 100644 code/scripting/api/objs/team_colors.cpp create mode 100644 code/scripting/api/objs/team_colors.h diff --git a/code/scripting/api/libs/tables.cpp b/code/scripting/api/libs/tables.cpp index d6f150b65e4..66ee90a70f7 100644 --- a/code/scripting/api/libs/tables.cpp +++ b/code/scripting/api/libs/tables.cpp @@ -8,6 +8,7 @@ #include "scripting/api/objs/intelentry.h" #include "scripting/api/objs/shipclass.h" #include "scripting/api/objs/shiptype.h" +#include "scripting/api/objs/team_colors.h" #include "scripting/api/objs/weaponclass.h" #include "scripting/api/objs/wingformation.h" @@ -340,5 +341,37 @@ ADE_FUNC(__len, l_Tables_WingFormations, nullptr, "Number of wing formations", " return ade_set_args(L, "i", static_cast(Wing_formations.size()) + 1); } +//*****SUBLIBRARY: Tables/TeamColors +ADE_LIB_DERIV(l_Tables_TeamColors, "TeamColors", nullptr, nullptr, l_Tables); + +ADE_INDEXER(l_Tables_TeamColors, "number/string IndexOrName", "Array of team colors", "teamcolor", "Team color handle, or invalid handle if name is invalid") +{ + const char* name; + if (!ade_get_args(L, "*s", &name)) + return ade_set_error(L, "o", l_TeamColor.Set(-1)); + + // look up by name + for (int i = 0; i < static_cast(Team_Names.size()); ++i) { + if (!stricmp(Team_Names[i].c_str(), name)) { + return ade_set_args(L, "o", l_TeamColor.Set(i)); + } + } + + // look up by number + int idx = atoi(name); + if (idx > 0) { + idx--; // Lua --> C/C++ + } else { + return ade_set_args(L, "o", l_TeamColor.Set(-1)); + } + + return ade_set_args(L, "o", l_TeamColor.Set(idx)); +} + +ADE_FUNC(__len, l_Tables_TeamColors, nullptr, "Number of team colors", "number", "Number of team colors") +{ + return ade_set_args(L, "i", static_cast(Team_Names.size())); +} + } } diff --git a/code/scripting/api/objs/parse_object.cpp b/code/scripting/api/objs/parse_object.cpp index 426c803c46b..9ed1f8b5eb9 100644 --- a/code/scripting/api/objs/parse_object.cpp +++ b/code/scripting/api/objs/parse_object.cpp @@ -7,7 +7,8 @@ #include "vecmath.h" #include "weaponclass.h" #include "wing.h" -//#include "globalincs/alphacolors.h" //Needed for team colors +#include "team_colors.h" +#include "globalincs/alphacolors.h" //Needed for team colors #include "mission/missionparse.h" @@ -333,31 +334,35 @@ ADE_VIRTVAR(Team, l_ParseObject, "team", "The team of the parsed ship.", "team", return ade_set_args(L, "o", l_Team.Set(poh->getObject()->team)); } -ADE_VIRTVAR(TeamColor, l_ParseObject, "string", "The team color", "string", "The name of the team color or empty if not set or invalid.") +ADE_VIRTVAR(TeamColor, l_ParseObject, "teamcolor", "The team color. Setting the team color here will not be reflected in the mission if the ship is already created. You must do that on the Ship object instead.", "teamcolor", "The team color handle or nil if not set or invalid.") { parse_object_h* poh = nullptr; - const char* team_color = nullptr; - if (!ade_get_args(L, "o|s", l_ParseObject.GetPtr(&poh), &team_color)) - return ade_set_error(L, "s", ""); + int idx = -1; + if (!ade_get_args(L, "o|o", l_ParseObject.GetPtr(&poh), l_TeamColor.Get(&idx))) + return ADE_RETURN_NIL; if (!poh->isValid()) - return ade_set_error(L, "s", ""); - - //Set team color - if (ADE_SETTING_VAR && team_color != nullptr) { + return ADE_RETURN_NIL; + // Set team color + if (ADE_SETTING_VAR && SCP_vector_inbounds(Team_Names, idx)) { // Verify - /*if (Team_Colors.find(team_color) == Team_Colors.end()) { + const auto& it = Team_Colors.find(Team_Names[idx]); + if (it == Team_Colors.end()) { mprintf(("Invalid team color specified in mission file for ship %s. Not setting!\n", poh->getObject()->name)); } else { - poh->getObject()->team_color_setting = team_color; - }*/ + poh->getObject()->team_color_setting = Team_Names[idx]; + } + } - LuaError(L, "Setting team colors is not yet supported!"); - - } + // look up by name + for (int i = 0; i < static_cast(Team_Names.size()); ++i) { + if (lcase_equal(Team_Names[i], poh->getObject()->team_color_setting)) { + return ade_set_args(L, "o", l_TeamColor.Set(i)); + } + } - return ade_set_args(L, "s", poh->getObject()->team_color_setting); + return ADE_RETURN_NIL; } ADE_VIRTVAR(InitialHull, l_ParseObject, "number", "The initial hull percentage of this parsed ship.", "number", diff --git a/code/scripting/api/objs/ship.cpp b/code/scripting/api/objs/ship.cpp index 17d588ad124..cf2ef4fdd76 100644 --- a/code/scripting/api/objs/ship.cpp +++ b/code/scripting/api/objs/ship.cpp @@ -15,6 +15,7 @@ #include "shipclass.h" #include "subsystem.h" #include "team.h" +#include "team_colors.h" #include "texture.h" #include "vecmath.h" #include "weaponclass.h" @@ -867,6 +868,39 @@ ADE_VIRTVAR(Team, l_Ship, "team", "Ship's team", "team", "Ship team, or invalid return ade_set_args(L, "o", l_Team.Set(shipp->team)); } +ADE_VIRTVAR(TeamColor, l_Ship, "teamcolor", "The team color. Note that setting the team color here is instant. If you need a fade, then use the sexp.", "teamcolor", "The team color handle or nil if not set or invalid.") +{ + object_h* oh = nullptr; + int idx = -1; + if (!ade_get_args(L, "o|o", l_Ship.GetPtr(&oh), l_TeamColor.Get(&idx))) + return ADE_RETURN_NIL; + + if (!oh->isValid()) + return ADE_RETURN_NIL; + + ship* shipp = &Ships[oh->objp()->instance]; + + //Set team color + if (ADE_SETTING_VAR && SCP_vector_inbounds(Team_Names, idx)) { + // Verify + const auto& it = Team_Colors.find(Team_Names[idx]); + if (it == Team_Colors.end()) { + mprintf(("Invalid team color specified in mission file for ship %s. Not setting!\n", shipp->ship_name)); + } else { + shipp->team_name = Team_Names[idx]; + } + } + + // look up by name + for (int i = 0; i < static_cast(Team_Names.size()); ++i) { + if (lcase_equal(Team_Names[i], shipp->team_name)) { + return ade_set_args(L, "o", l_TeamColor.Set(i)); + } + } + + return ADE_RETURN_NIL; +} + ADE_VIRTVAR_DEPRECATED(PersonaIndex, l_Ship, "number", "Persona index", "number", "The index of the persona from messages.tbl, 0 if no persona is set", gameversion::version(25, 0), "Deprecated in favor of Persona") { object_h *objh; diff --git a/code/scripting/api/objs/shipclass.cpp b/code/scripting/api/objs/shipclass.cpp index caf9c022f98..cc1abd7fccd 100644 --- a/code/scripting/api/objs/shipclass.cpp +++ b/code/scripting/api/objs/shipclass.cpp @@ -8,6 +8,7 @@ #include "cockpit_display.h" #include "species.h" #include "shiptype.h" +#include "team_colors.h" #include "vecmath.h" #include "ship/ship.h" #include "playerman/player.h" @@ -1113,7 +1114,7 @@ ADE_FUNC(isInTechroom, l_Shipclass, NULL, "Gets whether or not the ship class is ADE_FUNC(renderTechModel, l_Shipclass, "number X1, number Y1, number X2, number Y2, [number RotationPercent =0, number PitchPercent =0, number " - "BankPercent=40, number Zoom=1.3, boolean Lighting=true, string TeamColor=nil]", + "BankPercent=40, number Zoom=1.3, boolean Lighting=true, teamcolor TeamColor=nil]", "Draws ship model as if in techroom. True for regular lighting, false for flat lighting.", "boolean", "Whether ship was rendered") @@ -1123,8 +1124,8 @@ ADE_FUNC(renderTechModel, int idx; float zoom = 1.3f; bool lighting = true; - const char* team_color = nullptr; - if(!ade_get_args(L, "oiiii|ffffbs", l_Shipclass.Get(&idx), &x1, &y1, &x2, &y2, &rot_angles.h, &rot_angles.p, &rot_angles.b, &zoom, &lighting, &team_color)) + int tc_idx = -1; + if(!ade_get_args(L, "oiiii|ffffbo", l_Shipclass.Get(&idx), &x1, &y1, &x2, &y2, &rot_angles.h, &rot_angles.p, &rot_angles.b, &zoom, &lighting, l_TeamColor.Get(&tc_idx))) return ade_set_error(L, "b", false); if(idx < 0 || idx >= ship_info_size()) @@ -1133,6 +1134,8 @@ ADE_FUNC(renderTechModel, if(x2 < x1 || y2 < y1) return ade_set_args(L, "b", false); + ship_info* sip = &Ship_info[idx]; + CLAMP(rot_angles.p, 0.0f, 100.0f); CLAMP(rot_angles.b, 0.0f, 100.0f); CLAMP(rot_angles.h, 0.0f, 100.0f); @@ -1147,20 +1150,26 @@ ADE_FUNC(renderTechModel, rot_angles.h = (rot_angles.h*0.01f) * PI2; vm_rotate_matrix_by_angles(&orient, &rot_angles); - SCP_string tcolor = team_color ? team_color : ""; + SCP_string tcolor = sip->default_team_name; + if (SCP_vector_inbounds(Team_Names, tc_idx)) { + const auto& it = Team_Colors.find(Team_Names[tc_idx]); + if (it != Team_Colors.end()) { + tcolor = Team_Names[tc_idx]; + } + } return ade_set_args(L, "b", render_tech_model(TECH_SHIP, x1, y1, x2, y2, zoom, lighting, idx, &orient, tcolor)); } // Nuke's alternate tech model rendering function -ADE_FUNC(renderTechModel2, l_Shipclass, "number X1, number Y1, number X2, number Y2, [orientation Orientation=nil, number Zoom=1.3, string TeamColor=nil]", "Draws ship model as if in techroom", "boolean", "Whether ship was rendered") +ADE_FUNC(renderTechModel2, l_Shipclass, "number X1, number Y1, number X2, number Y2, [orientation Orientation=nil, number Zoom=1.3, teamcolor TeamColor=nil]", "Draws ship model as if in techroom", "boolean", "Whether ship was rendered") { int x1,y1,x2,y2; int idx; float zoom = 1.3f; matrix_h *mh = nullptr; - const char* team_color = nullptr; - if(!ade_get_args(L, "oiiiio|fs", l_Shipclass.Get(&idx), &x1, &y1, &x2, &y2, l_Matrix.GetPtr(&mh), &zoom, &team_color)) + int tc_idx = -1; + if(!ade_get_args(L, "oiiiio|fo", l_Shipclass.Get(&idx), &x1, &y1, &x2, &y2, l_Matrix.GetPtr(&mh), &zoom, l_TeamColor.Get(&tc_idx))) return ade_set_error(L, "b", false); if(idx < 0 || idx >= ship_info_size()) @@ -1169,17 +1178,25 @@ ADE_FUNC(renderTechModel2, l_Shipclass, "number X1, number Y1, number X2, number if(x2 < x1 || y2 < y1) return ade_set_args(L, "b", false); + ship_info* sip = &Ship_info[idx]; + //Handle angles matrix *orient = mh->GetMatrix(); - SCP_string tcolor = team_color ? team_color : ""; + SCP_string tcolor = sip->default_team_name; + if (SCP_vector_inbounds(Team_Names, tc_idx)) { + const auto& it = Team_Colors.find(Team_Names[tc_idx]); + if (it != Team_Colors.end()) { + tcolor = Team_Names[tc_idx]; + } + } return ade_set_args(L, "b", render_tech_model(TECH_SHIP, x1, y1, x2, y2, zoom, true, idx, orient, tcolor)); } ADE_FUNC(renderSelectModel, l_Shipclass, - "boolean restart, number x, number y, [number width = 629, number height = 355, number currentEffectSetting = default, number zoom = 1.3, string TeamColor=nil]", + "boolean restart, number x, number y, [number width = 629, number height = 355, number currentEffectSetting = default, number zoom = 1.3, teamcolor TeamColor=nil]", "Draws the 3D select ship model with the chosen effect at the specified coordinates. Restart should " "be true on the first frame this is called and false on subsequent frames. Valid selection effects are 1 (fs1) or 2 (fs2), " "defaults to the mod setting or the model's setting. Zoom is a multiplier to the model's closeup_zoom value.", @@ -1194,8 +1211,8 @@ ADE_FUNC(renderSelectModel, int y2 = 355; int effect = -1; float zoom = 1.3f; - const char* team_color = nullptr; - if (!ade_get_args(L, "obii|iiifs", l_Shipclass.Get(&idx), &restart, &x1, &y1, &x2, &y2, &effect, &zoom, &team_color)) + int tc_idx = -1; + if (!ade_get_args(L, "obii|iiifo", l_Shipclass.Get(&idx), &restart, &x1, &y1, &x2, &y2, &effect, &zoom, l_TeamColor.Get(&tc_idx))) return ADE_RETURN_NIL; if (idx < 0 || idx >= ship_info_size()) @@ -1230,7 +1247,14 @@ ADE_FUNC(renderSelectModel, model_render_params render_info; if (sip->uses_team_colors) { - SCP_string tcolor = team_color ? team_color : sip->default_team_name; + SCP_string tcolor = sip->default_team_name; + + if (SCP_vector_inbounds(Team_Names, tc_idx)) { + const auto& it = Team_Colors.find(Team_Names[tc_idx]); + if (it != Team_Colors.end()) { + tcolor = Team_Names[tc_idx]; + } + } render_info.set_team_color(tcolor, "none", 0, 0); } @@ -1260,7 +1284,7 @@ ADE_FUNC(renderOverheadModel, "number x, number y, [number width = 467, number height = 362, number|table /* selectedSlot = -1 or empty table */, number selectedWeapon = -1, number hoverSlot = -1, " "number bank1_x = 170, number bank1_y = 203, number bank2_x = 170, number bank2_y = 246, number bank3_x = 170, number bank3_y = 290, " "number bank4_x = 552, number bank4_y = 203, number bank5_x = 552, number bank5_y = 246, number bank6_x = 552, number bank6_y = 290, " - "number bank7_x = 552, number bank7_y = 333, number style = 0, string TeamColor=nil]", + "number bank7_x = 552, number bank7_y = 333, number style = 0, teamcolor TeamColor=nil]", "Draws the 3D overhead ship model with the lines pointing from bank weapon selections to bank firepoints. SelectedSlot refers to loadout " "ship slots 1-12 where wing 1 is 1-4, wing 2 is 5-8, and wing 3 is 9-12. SelectedWeapon is the index into weapon classes. HoverSlot refers " "to the bank slots 1-7 where 1-3 are primaries and 4-6 are secondaries. Lines will be drawn from any bank containing the SelectedWeapon to " @@ -1301,12 +1325,12 @@ ADE_FUNC(renderOverheadModel, int weapon_list[MAX_SHIP_WEAPONS] = {-1, -1, -1, -1, -1, -1, -1}; - const char* team_color = nullptr; + int tc_idx = -1; if (lua_isnumber(L, 6)) { if (!ade_get_args(L, - "oii|iiiiiiiiiiiiiiiiiiiis", + "oii|iiiiiiiiiiiiiiiiiiiio", l_Shipclass.Get(&idx), &x1, &y1, @@ -1330,7 +1354,7 @@ ADE_FUNC(renderOverheadModel, &bank7_x, &bank7_y, &style, - &team_color)) + l_TeamColor.Get(&tc_idx))) return ADE_RETURN_NIL; // Convert this from the Lua index @@ -1344,7 +1368,7 @@ ADE_FUNC(renderOverheadModel, } } else { if (!ade_get_args(L, - "oii|iitiiiiiiiiiiiiiiiiis", + "oii|iitiiiiiiiiiiiiiiiiio", l_Shipclass.Get(&idx), &x1, &y1, @@ -1368,7 +1392,7 @@ ADE_FUNC(renderOverheadModel, &bank7_x, &bank7_y, &style, - &team_color)) + l_TeamColor.Get(&tc_idx))) return ADE_RETURN_NIL; int count = 0; @@ -1415,7 +1439,13 @@ ADE_FUNC(renderOverheadModel, ship_info* sip = &Ship_info[idx]; - SCP_string tcolor = team_color ? team_color : sip->default_team_name; + SCP_string tcolor = sip->default_team_name; + if (SCP_vector_inbounds(Team_Names, tc_idx)) { + const auto& it = Team_Colors.find(Team_Names[tc_idx]); + if (it != Team_Colors.end()) { + tcolor = Team_Names[tc_idx]; + } + } int modelNum = model_load(sip->pof_file, sip); model_page_in_textures(modelNum, idx); diff --git a/code/scripting/api/objs/team_colors.cpp b/code/scripting/api/objs/team_colors.cpp new file mode 100644 index 00000000000..4a293978c8b --- /dev/null +++ b/code/scripting/api/objs/team_colors.cpp @@ -0,0 +1,134 @@ +// +// + +#include "team_colors.h" +#include "globalincs/alphacolors.h" +#include "scripting/api/objs/color.h" + +namespace scripting { +namespace api { + +//**********HANDLE: TeamColor +ADE_OBJ(l_TeamColor, int, "teamcolor", "Team color handle"); + +ADE_FUNC(__tostring, l_TeamColor, nullptr, "Team color name", "string", "Team color name, or an empty string if handle is invalid") +{ + int idx; + if (!ade_get_args(L, "o", l_TeamColor.Get(&idx))) + return ade_set_error(L, "s", ""); + + if (!SCP_vector_inbounds(Team_Names, idx)) + return ade_set_error(L, "s", ""); + + return ade_set_args(L, "s", Team_Names[idx].c_str()); +} + +ADE_FUNC(__eq, l_TeamColor, "teamcolor, teamcolor", "Checks if the two team colors are equal", "boolean", "true if equal, false otherwise") +{ + int idx1, idx2; + if (!ade_get_args(L, "oo", l_TeamColor.Get(&idx1), l_TeamColor.Get(&idx2))) + return ade_set_error(L, "b", false); + + if (!SCP_vector_inbounds(Team_Names, idx1)) + return ade_set_error(L, "b", false); + + if (!SCP_vector_inbounds(Team_Names, idx2)) + return ade_set_error(L, "b", false); + + return ade_set_args(L, "b", idx1 == idx2); +} + +ADE_VIRTVAR(Name, l_TeamColor, nullptr, "The team color name", "string", "Team color name, or empty string if handle is invalid") +{ + int idx; + if (!ade_get_args(L, "o", l_TeamColor.Get(&idx))) + return ade_set_error(L, "s", ""); + + if (!SCP_vector_inbounds(Team_Names, idx)) + return ade_set_error(L, "s", ""); + + const auto& it = Team_Colors.find(Team_Names[idx]); + if (it == Team_Colors.end()) { + return ade_set_error(L, "s", ""); + } + + if (ADE_SETTING_VAR) { + LuaError(L, "Setting Team Color Name is not supported"); + } + + return ade_set_args(L, "s", Team_Names[idx].c_str()); +} + +ADE_VIRTVAR(BaseColor, l_TeamColor, nullptr, "Team color base color", "color", "Team color base color, or nil if handle is invalid") +{ + int idx; + if (!ade_get_args(L, "o", l_TeamColor.Get(&idx))) + return ADE_RETURN_NIL; + + if (!SCP_vector_inbounds(Team_Names, idx)) + return ADE_RETURN_NIL; + + const auto& it = Team_Colors.find(Team_Names[idx]); + if (it == Team_Colors.end()) { + return ADE_RETURN_NIL; + } + + if (ADE_SETTING_VAR) { + LuaError(L, "Setting Team Color Base is not supported"); + } + + const auto& color_values = it->second.base; + + color cur; + + gr_init_alphacolor(&cur, static_cast(color_values.r), static_cast(color_values.g), static_cast(color_values.b), 255); + + return ade_set_args(L, "o", l_Color.Set(cur)); +} + +ADE_VIRTVAR(StripeColor, l_TeamColor, nullptr, "Team color stripe color", "color", "Team color stripe color, or nil if handle is invalid") +{ + int idx; + if (!ade_get_args(L, "o", l_TeamColor.Get(&idx))) + return ADE_RETURN_NIL; + + if (!SCP_vector_inbounds(Team_Names, idx)) + return ADE_RETURN_NIL; + + const auto& it = Team_Colors.find(Team_Names[idx]); + if (it == Team_Colors.end()) { + return ADE_RETURN_NIL; + } + + if (ADE_SETTING_VAR) { + LuaError(L, "Setting Team Color Stripe is not supported"); + } + + const auto& color_values = it->second.stripe; + + color cur; + + gr_init_alphacolor(&cur, static_cast(color_values.r), static_cast(color_values.g), static_cast(color_values.b), 255); + + return ade_set_args(L, "o", l_Color.Set(cur)); +} + +ADE_FUNC(isValid, l_TeamColor, nullptr, "Detects whether handle is valid", "boolean", "true if valid, false if handle is invalid, nil if a syntax/type error occurs") +{ + int idx; + if (!ade_get_args(L, "o", l_TeamColor.Get(&idx))) + return ADE_RETURN_NIL; + + if (!SCP_vector_inbounds(Team_Names, idx)) + return ADE_RETURN_FALSE; + + const auto& it = Team_Colors.find(Team_Names[idx]); + if (it == Team_Colors.end()) { + return ADE_RETURN_FALSE; + } + + return ADE_RETURN_TRUE; +} + +} +} diff --git a/code/scripting/api/objs/team_colors.h b/code/scripting/api/objs/team_colors.h new file mode 100644 index 00000000000..9ec87bd0157 --- /dev/null +++ b/code/scripting/api/objs/team_colors.h @@ -0,0 +1,14 @@ +#pragma once + +#include "globalincs/pstypes.h" + +#include "scripting/ade.h" +#include "scripting/ade_api.h" + +namespace scripting { +namespace api { + +DECLARE_ADE_OBJ(l_TeamColor, int); + +} +} // namespace scripting diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 601ae93841a..552d889370f 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -1499,6 +1499,8 @@ add_file_folder("Scripting\\\\Api\\\\Objs" scripting/api/objs/subsystem.h scripting/api/objs/team.cpp scripting/api/objs/team.h + scripting/api/objs/team_colors.cpp + scripting/api/objs/team_colors.h scripting/api/objs/techroom.cpp scripting/api/objs/techroom.h scripting/api/objs/texture.cpp From ef631f9fe0d77b80b0e4c8d7e09faeb560447b30 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sun, 13 Jul 2025 19:44:44 -0500 Subject: [PATCH 251/466] nested namespaces --- code/scripting/api/objs/team_colors.cpp | 4 +--- code/scripting/api/objs/team_colors.h | 6 ++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/code/scripting/api/objs/team_colors.cpp b/code/scripting/api/objs/team_colors.cpp index 4a293978c8b..da21ac50223 100644 --- a/code/scripting/api/objs/team_colors.cpp +++ b/code/scripting/api/objs/team_colors.cpp @@ -5,8 +5,7 @@ #include "globalincs/alphacolors.h" #include "scripting/api/objs/color.h" -namespace scripting { -namespace api { +namespace scripting::api { //**********HANDLE: TeamColor ADE_OBJ(l_TeamColor, int, "teamcolor", "Team color handle"); @@ -131,4 +130,3 @@ ADE_FUNC(isValid, l_TeamColor, nullptr, "Detects whether handle is valid", "bool } } -} diff --git a/code/scripting/api/objs/team_colors.h b/code/scripting/api/objs/team_colors.h index 9ec87bd0157..0790106215e 100644 --- a/code/scripting/api/objs/team_colors.h +++ b/code/scripting/api/objs/team_colors.h @@ -5,10 +5,8 @@ #include "scripting/ade.h" #include "scripting/ade_api.h" -namespace scripting { -namespace api { +namespace scripting::api { DECLARE_ADE_OBJ(l_TeamColor, int); -} -} // namespace scripting +} // namespace scripting::api From 53394093a49bcacd595a327b99a4fa7496c9a2b2 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Mon, 14 Jul 2025 15:14:40 +0200 Subject: [PATCH 252/466] Limit the number of threads spawned (#6833) * Limit the number of threads spawned * Conversion on msvc --- code/utils/threading.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/utils/threading.cpp b/code/utils/threading.cpp index 69e576b5ae2..c4a10059db7 100644 --- a/code/utils/threading.cpp +++ b/code/utils/threading.cpp @@ -136,7 +136,10 @@ namespace threading { void init_task_pool() { if (Cmdline_multithreading == 0) { - num_threads = get_number_of_physical_cores() - 1; + //At least given the current collision-detection threading, 8 cores (if available) seems like a sweetspot, with more cores adding too much overhead. + //This could be improved in the future. + //This could also be made task-dependant, if stuff like parallelized loading benefits from more cores. + num_threads = std::min(get_number_of_physical_cores() - 1, static_cast(7)); } else { num_threads = Cmdline_multithreading - 1; From 4c80ad7243e1e339d2e26df36d4e1cf0ffa97f35 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:01:24 +0200 Subject: [PATCH 253/466] Fix Thread Count Detection on Windows (#6834) --- code/utils/threading.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/utils/threading.cpp b/code/utils/threading.cpp index c4a10059db7..53c50accb02 100644 --- a/code/utils/threading.cpp +++ b/code/utils/threading.cpp @@ -68,7 +68,7 @@ namespace threading { SCP_vector infoBuffer(length / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION)); DWORD error = glpi(infoBuffer.data(), &length); - if (error != 0) + if (error == 0) return get_number_of_physical_cores_fallback(); size_t num_cores = 0; From d055d03e537a5e0bd2918dc251b238b80d5f53b8 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 14 Jul 2025 13:41:36 -0400 Subject: [PATCH 254/466] get proper number of physical cpu cores on macOS (#6835) * get proper number of physical cpu cores on macOS macOS has an easy way to get the physical cpu count so let's use it. Also with Apple Silicon there are performance and efficiency cores so we need to make sure we ignore those low-perf cores when getting the count. * improve error handling --- code/utils/threading.cpp | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/code/utils/threading.cpp b/code/utils/threading.cpp index 53c50accb02..5c71228235e 100644 --- a/code/utils/threading.cpp +++ b/code/utils/threading.cpp @@ -11,6 +11,8 @@ #ifdef WIN32 #include +#elif defined(__APPLE__) +#include #endif namespace threading { @@ -85,6 +87,27 @@ namespace threading { return num_cores; } } +#elif defined __APPLE__ + static size_t get_number_of_physical_cores() { + int rval = 0; + int num = 0; + size_t numSize = sizeof(num); + + // apple silicon (performance cores only) + rval = sysctlbyname("hw.perflevel0.physicalcpu", &num, &numSize, nullptr, 0); + + // intel + if (rval != 0) { + rval = sysctlbyname("hw.physicalcpu", &num, &numSize, nullptr, 0); + } + + if (rval == 0 && num > 0) { + return num; + } else { + // invalid results, try fallback + return get_number_of_physical_cores_fallback(); + } + } #elif defined SCP_UNIX static size_t get_number_of_physical_cores() { try { @@ -170,4 +193,4 @@ namespace threading { size_t get_num_workers() { return worker_threads.size(); } -} \ No newline at end of file +} From a2247365431ab709ee294fe7cf14bb6c2e9a6897 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Mon, 14 Jul 2025 13:44:18 -0400 Subject: [PATCH 255/466] Slots for custom post processing effects (#6827) * Slots for custom post processing effects Post processing effects such as tint or dithering have their uniforms hardcoded, which means the only want to create new post-process effects for a mod is to overwrite an existing effect. Requested for DEM2, this PR creates 4 blank slots for mods to create new post processing effects--2 floats and 2 vec3. This approach to create blank slots for modders follows the successful strategy used for Custom_Controls 1-5. Note, this PR also adds `tint` to the default `post_processing.tbl` which had it missing (even though tint is a valid effect with a valid uniform buffer already). Happy to discuss more or edit however. * fix ordering --- code/def_files/data/effects/post-f.sdr | 8 ++++++++ code/def_files/data/tables/post_processing.tbl | 11 ++++++++++- code/graphics/opengl/gropenglpostprocessing.cpp | 12 ++++++++++++ code/graphics/post_processing.cpp | 8 ++++++++ code/graphics/post_processing.h | 4 ++++ code/graphics/util/uniform_structs.h | 6 ++++++ 6 files changed, 48 insertions(+), 1 deletion(-) diff --git a/code/def_files/data/effects/post-f.sdr b/code/def_files/data/effects/post-f.sdr index 26b8a70066c..08eafe5899e 100644 --- a/code/def_files/data/effects/post-f.sdr +++ b/code/def_files/data/effects/post-f.sdr @@ -18,6 +18,14 @@ layout (std140) uniform genericData { vec3 tint; float dither; + + // these are blank, valid slots for modders to create custom effects + // that can be defined in post_processing.tbl and coded below + vec3 custom_effect_vec3_a; + float custom_effect_float_a; + + vec3 custom_effect_vec3_b; + float custom_effect_float_b; }; void main() diff --git a/code/def_files/data/tables/post_processing.tbl b/code/def_files/data/tables/post_processing.tbl index 43cd359bc26..1e77c9fdf87 100644 --- a/code/def_files/data/tables/post_processing.tbl +++ b/code/def_files/data/tables/post_processing.tbl @@ -63,5 +63,14 @@ $AlwaysOn: false $Default: 0.0 $Div: 50 $Add: 0 - + +$Name: tint +$Uniform: tint +$Define: FLAG_TINT +$AlwaysOn: false +$Default: 0.0 +$Div: 1 +$Add: 0 +$RGB: 0.2,0.2,0.2 + #End diff --git a/code/graphics/opengl/gropenglpostprocessing.cpp b/code/graphics/opengl/gropenglpostprocessing.cpp index 3e42fb6da3b..67f3cd9aab9 100644 --- a/code/graphics/opengl/gropenglpostprocessing.cpp +++ b/code/graphics/opengl/gropenglpostprocessing.cpp @@ -597,6 +597,18 @@ void gr_opengl_post_process_end() case graphics::PostEffectUniformType::Tint: data->tint = postEffects[idx].rgb; break; + case graphics::PostEffectUniformType::CustomEffectVEC3A: + data->custom_effect_vec3_a = postEffects[idx].rgb; + break; + case graphics::PostEffectUniformType::CustomEffectFloatA: + data->custom_effect_float_a = value; + break; + case graphics::PostEffectUniformType::CustomEffectVEC3B: + data->custom_effect_vec3_b = postEffects[idx].rgb; + break; + case graphics::PostEffectUniformType::CustomEffectFloatB: + data->custom_effect_float_b = value; + break; } } } diff --git a/code/graphics/post_processing.cpp b/code/graphics/post_processing.cpp index a823c812582..1ccc7d3cc49 100644 --- a/code/graphics/post_processing.cpp +++ b/code/graphics/post_processing.cpp @@ -30,6 +30,14 @@ PostEffectUniformType mapUniformNameToType(const SCP_string& uniform_name) return PostEffectUniformType::Dither; } else if (!stricmp(uniform_name.c_str(), "tint")) { return PostEffectUniformType::Tint; + } else if (!stricmp(uniform_name.c_str(), "custom_effect_vec3_a")) { + return PostEffectUniformType::CustomEffectVEC3A; + } else if (!stricmp(uniform_name.c_str(), "custom_effect_float_a")) { + return PostEffectUniformType::CustomEffectFloatA; + } else if (!stricmp(uniform_name.c_str(), "custom_effect_vec3_b")) { + return PostEffectUniformType::CustomEffectVEC3B; + } else if (!stricmp(uniform_name.c_str(), "custom_effect_float_b")) { + return PostEffectUniformType::CustomEffectFloatB; } else { error_display(0, "Unknown uniform name '%s'!", uniform_name.c_str()); return PostEffectUniformType::Invalid; diff --git a/code/graphics/post_processing.h b/code/graphics/post_processing.h index 2787e456096..4db262cf128 100644 --- a/code/graphics/post_processing.h +++ b/code/graphics/post_processing.h @@ -17,6 +17,10 @@ enum class PostEffectUniformType { Cutoff, Tint, Dither, + CustomEffectVEC3A, + CustomEffectFloatA, + CustomEffectVEC3B, + CustomEffectFloatB, }; struct post_effect_t { diff --git a/code/graphics/util/uniform_structs.h b/code/graphics/util/uniform_structs.h index 455a207c6c5..5c3411e03e5 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -398,6 +398,12 @@ struct post_data { vec3d tint; float dither; + + vec3d custom_effect_vec3_a; + float custom_effect_float_a; + + vec3d custom_effect_vec3_b; + float custom_effect_float_b; }; struct irrmap_data { From 83282b6427fed5fb2d0141edc2cb2d1cb2bee727 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Wed, 16 Jul 2025 03:06:54 +0200 Subject: [PATCH 256/466] Actually take the power of 3 (#6837) --- code/weapon/weapons.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 185f1110185..bf3d9eb2570 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -680,7 +680,7 @@ static particle::ParticleEffectHandle convertLegacyPspewBuffer(const pspew_legac switch (pspew_buffer.particle_spew_type) { case PSPEW_DEFAULT: - position_vol = std::make_unique(::util::UniformFloatRange(-PI_2, PI_2), 3.f * pspew_buffer.particle_spew_scale); + position_vol = std::make_unique(::util::UniformFloatRange(-PI_2, PI_2), powf(pspew_buffer.particle_spew_scale, 3.0f)); direction = particle::ParticleEffect::ShapeDirection::REVERSE; break; case PSPEW_HELIX: { From be467060817a7635e3ba5419944ba8ec70dc8566 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 16 Jul 2025 22:32:28 -0400 Subject: [PATCH 257/466] make the `check_for_*` functions return bool Small cleanup. --- code/parse/parselo.cpp | 32 ++++++++++---------------------- code/parse/parselo.h | 10 +++++----- 2 files changed, 15 insertions(+), 27 deletions(-) diff --git a/code/parse/parselo.cpp b/code/parse/parselo.cpp index 6d77989cd3f..07d0d034405 100644 --- a/code/parse/parselo.cpp +++ b/code/parse/parselo.cpp @@ -532,15 +532,12 @@ int required_string(const char *pstr) return 1; } -int check_for_eof_raw() +bool check_for_eof_raw() { - if (*Mp == '\0') - return 1; - - return 0; + return (*Mp == '\0'); } -int check_for_eof() +bool check_for_eof() { ignore_white_space(); @@ -548,37 +545,28 @@ int check_for_eof() } /** -Returns 1 if it finds a newline character precded by any amount of grayspace. +Returns true if it finds a newline character precded by any amount of grayspace. */ -int check_for_eoln() +bool check_for_eoln() { ignore_gray_space(); - if(*Mp == EOLN) - return 1; - else - return 0; + return (*Mp == EOLN); } // similar to optional_string, but just checks if next token is a match. // It doesn't advance Mp except to skip past white space. -int check_for_string(const char *pstr) +bool check_for_string(const char *pstr) { ignore_white_space(); - if (!strnicmp(pstr, Mp, strlen(pstr))) - return 1; - - return 0; + return check_for_string_raw(pstr); } // like check for string, but doesn't skip past any whitespace -int check_for_string_raw(const char *pstr) +bool check_for_string_raw(const char *pstr) { - if (!strnicmp(pstr, Mp, strlen(pstr))) - return 1; - - return 0; + return (strnicmp(pstr, Mp, strlen(pstr)) == 0); } int string_lookup(const char* str1, const SCP_vector& strlist, const char* description, bool say_errors, bool print_list) diff --git a/code/parse/parselo.h b/code/parse/parselo.h index d05e2c0733c..653c74f7284 100644 --- a/code/parse/parselo.h +++ b/code/parse/parselo.h @@ -329,11 +329,11 @@ void stuff_boolean_flag(Flagset& destination, Flags flag, bool a_to_eol = true) destination.set(flag, temp); } -extern int check_for_string(const char *pstr); -extern int check_for_string_raw(const char *pstr); -extern int check_for_eof(); -extern int check_for_eof_raw(); -extern int check_for_eoln(); +extern bool check_for_string(const char *pstr); +extern bool check_for_string_raw(const char *pstr); +extern bool check_for_eof(); +extern bool check_for_eof_raw(); +extern bool check_for_eoln(); // from aicode.cpp extern void parse_float_list(float *plist, size_t size); From 07db2aabf582c13d8b129e1948be89eecb3373b2 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 16 Jul 2025 22:32:50 -0400 Subject: [PATCH 258/466] allow modular nebula tables to not define bitmaps This allows the first half of a modular -neb.tbm to be skipped, allowing just poofs to be specified without the need for a preceding empty bitmap section. It's a follow-up to #5128 which allowed just bitmaps to be specified without the need for a subsequent empty poof section. --- code/nebula/neb.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/code/nebula/neb.cpp b/code/nebula/neb.cpp index 86cdd219e69..4d8a8ba670f 100644 --- a/code/nebula/neb.cpp +++ b/code/nebula/neb.cpp @@ -204,8 +204,13 @@ void parse_nebula_table(const char* filename) read_file_text(filename, CF_TYPE_TABLES); reset_parse(); + // allow modular tables to not define bitmaps + bool skip_background_bitmaps = false; + if (Parsing_modular_table && (check_for_string("+Poof:") || check_for_string("$Name:"))) + skip_background_bitmaps = true; + // background bitmaps - while (!optional_string("#end")) { + while (!skip_background_bitmaps && !optional_string("#end")) { // nebula optional_string("+Nebula:"); stuff_string(name, F_NAME, MAX_FILENAME_LEN); @@ -222,7 +227,7 @@ void parse_nebula_table(const char* filename) if (Parsing_modular_table && check_for_eof()) return; - // poofs + // poofs (see also check_for_string above) while (required_string_one_of(3, "#end", "+Poof:", "$Name:")) { bool create_if_not_found = true; poof_info pooft; From cb20a05967775c6b3ad049065059cb3bee8a5276 Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Thu, 17 Jul 2025 07:59:01 -0700 Subject: [PATCH 259/466] Fix inverted piercing impact logic (#6843) * fix piercing sign error * more robust solution --- code/weapon/weapons.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index bf3d9eb2570..43b3922bb27 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -7949,7 +7949,7 @@ void maybe_play_conditional_impacts(const std::arrayimpact_weapon_expl_effect.isValid() && armed_weapon) { - auto particleSource = particle::ParticleManager::get()->createSource(wip->impact_weapon_expl_effect); + auto particleSource = particle::ParticleManager::get()->createSource(wip->impact_weapon_expl_effect); particleSource->setHost(weapon_hit_make_effect_host(weapon_objp, impacted_objp, submodel, hitpos, local_hitpos)); particleSource->setTriggerRadius(weapon_objp->radius * radius_mult); @@ -7973,7 +7973,7 @@ void maybe_play_conditional_impacts(const std::arrayfinishCreation(); } - if (impacted_objp != nullptr && impact_data[static_cast>(HitType::SHIELD)].has_value() && (!valid_conditional_impact && wip->piercing_impact_effect.isValid() && armed_weapon)) { + if (impacted_objp != nullptr && (impact_data[static_cast>(HitType::HULL)].has_value() || impact_data[static_cast>(HitType::SUBSYS)].has_value()) && (!valid_conditional_impact && wip->piercing_impact_effect.isValid() && armed_weapon)) { if ((impacted_objp->type == OBJ_SHIP) || (impacted_objp->type == OBJ_DEBRIS)) { int ok_to_draw = 1; From 26e9ecab0e7b992b862cd496abdff0ee0d834d6e Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Sat, 19 Jul 2025 18:24:49 -0400 Subject: [PATCH 260/466] Allow mods to set threshold at which shield stops taking damage (#6847) * Allow mods to set threshold at which shield stops taking damage FSO logic dictates that shields collisions and any related sound or visuals related to shields are skipped if the shields are under 10%. This is rather unintuitive especially for mods, so this PR creates a game setting value that allows mods to set this value. Also, this PR fixes a bug where the HUD shield gauge would try and incorporate this feature but did not do it correctly. Specifically, the gauge skips shield segment rendering if the raw shield strength was 0.1 not the percent. Incorrectly using the raw value results in HUD shields being rendered (albeit faintly) even if the damage was skipping the shield and directly damaging the hull. In other words, if the shield segment was 8%, the shield segment would show faintly even the shield is skipping damage. Tested and works as expected. Also happy to discuss or answer any questions! * update local variable name for clarity * rename `ship_quadrant_shield_strength` * cleanup with updated get percent function * more cleanup * wording and safety tuning * use correct default number --- code/hud/hudshield.cpp | 10 +++++----- code/mod_table/mod_table.cpp | 13 +++++++++++++ code/mod_table/mod_table.h | 1 + code/object/objectshield.cpp | 12 +++++++++++- code/object/objectshield.h | 9 +++++++++ code/ship/shield.cpp | 4 ++-- code/ship/ship.cpp | 36 +----------------------------------- code/ship/ship.h | 3 --- code/ship/shiphit.cpp | 18 +++++++++--------- code/weapon/weapons.cpp | 13 +++++++------ 10 files changed, 58 insertions(+), 61 deletions(-) diff --git a/code/hud/hudshield.cpp b/code/hud/hudshield.cpp index 02846dc7c00..80b2778dbcb 100644 --- a/code/hud/hudshield.cpp +++ b/code/hud/hudshield.cpp @@ -390,7 +390,7 @@ void hud_shield_show_mini(const object *objp, int x_force, int y_force, int x_hu else num = i; - if (objp->shield_quadrant[num] < 0.1f ) { + if ( (max_shield > 0.0f) && (objp->shield_quadrant[num]/max_shield < Shield_percent_skips_damage) ) { continue; } @@ -738,12 +738,12 @@ void HudGaugeShield::showShields(const object *objp, ShieldGaugeType mode, bool break; } - if (!config) { + if ( (!config) && (max_shield > 0.0f) ) { if (!(sip->flags[Ship::Info_Flags::Model_point_shields])) { - if (objp->shield_quadrant[Quadrant_xlate[i]] < 0.1f) + if (objp->shield_quadrant[Quadrant_xlate[i]]/max_shield < Shield_percent_skips_damage) continue; } else { - if (objp->shield_quadrant[i] < 0.1f) + if (objp->shield_quadrant[i]/max_shield < Shield_percent_skips_damage) continue; } } @@ -1085,7 +1085,7 @@ void HudGaugeShieldMini::showMiniShields(const object *objp, bool config) else num = i; - if (!config && objp->shield_quadrant[num] < 0.1f ) { + if ( (!config) && (max_shield > 0.0f) && (objp->shield_quadrant[num]/max_shield < Shield_percent_skips_damage) ) { continue; } diff --git a/code/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index ffec4fcaa41..20cf1a65a95 100644 --- a/code/mod_table/mod_table.cpp +++ b/code/mod_table/mod_table.cpp @@ -172,6 +172,7 @@ bool Fix_asteroid_bounding_box_check; bool Disable_intro_movie; bool Show_locked_status_scramble_missions; bool Disable_expensive_turret_target_check; +float Shield_percent_skips_damage; #ifdef WITH_DISCORD @@ -1539,6 +1540,17 @@ void parse_mod_table(const char *filename) stuff_boolean(&Disable_expensive_turret_target_check); } + if (optional_string("$Threshold below which shield skips damage:")) { + float threshold; + stuff_float(&threshold); + if ((threshold >= 0.0f) && (threshold <= 1.0f)) { + Shield_percent_skips_damage = threshold; + } else { + mprintf(("Game Settings Table: '$Threshold below which shield skips damage' value of %.2f is not between 0 and 1. Using default value of 0.10.\n", threshold)); + Shield_percent_skips_damage = 0.1f; + } + } + // end of options ---------------------------------------- // if we've been through once already and are at the same place, force a move @@ -1775,6 +1787,7 @@ void mod_table_reset() Disable_intro_movie = false; Show_locked_status_scramble_missions = false; Disable_expensive_turret_target_check = false; + Shield_percent_skips_damage = 0.1f; } void mod_table_set_version_flags() diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index 244a1094eab..541cf51d392 100644 --- a/code/mod_table/mod_table.h +++ b/code/mod_table/mod_table.h @@ -187,6 +187,7 @@ extern bool Fix_asteroid_bounding_box_check; extern bool Disable_intro_movie; extern bool Show_locked_status_scramble_missions; extern bool Disable_expensive_turret_target_check; +extern float Shield_percent_skips_damage; void mod_table_init(); void mod_table_post_process(); diff --git a/code/object/objectshield.cpp b/code/object/objectshield.cpp index a5ec762457c..00afefd8474 100644 --- a/code/object/objectshield.cpp +++ b/code/object/objectshield.cpp @@ -178,7 +178,7 @@ void shield_apply_healing(object* objp, float healing) } // if the shields are approximately equal give to all quads equally - if (max_shield - min_shield < shield_get_max_strength(objp) * 0.1f) { + if (max_shield - min_shield < shield_get_max_strength(objp) * Shield_percent_skips_damage) { for (int i = 0; i < n_quadrants; i++) shield_add_quad(objp, i, healing / n_quadrants); } else { // else give to weakest @@ -353,6 +353,16 @@ float shield_get_quad(const object *objp, int quadrant_num) return objp->shield_quadrant[quadrant_num]; } +float shield_get_quad_percent(const object* objp, int quadrant_num) +{ + float max_quad = shield_get_max_quad(objp); + if (max_quad > 0.0f) { + return shield_get_quad(objp, quadrant_num) / max_quad; + } else { + return 0.0f; + } +} + float shield_get_strength(const object *objp) { Assert(objp); diff --git a/code/object/objectshield.h b/code/object/objectshield.h index 9bbb91ed481..f3a72e2311d 100644 --- a/code/object/objectshield.h +++ b/code/object/objectshield.h @@ -75,6 +75,15 @@ void shield_add_strength(object *objp, float delta); */ float shield_get_quad(const object *objp, int quadrant_num); +/** + * Return the shield strength of the specified quadrant on hit_objp + * + * @param objp object pointer to ship object + * @param quadrant_num shield quadrant to check + * @return strength of shields in the checked quadrant as a percentage, between 0 and 1.0 + */ +float shield_get_quad_percent(const object* objp, int quadrant_num); + /** * @brief Sets the strength (in HP) of a shield quadrant/sector * diff --git a/code/ship/shield.cpp b/code/ship/shield.cpp index 9bf985c4131..ad987d29630 100644 --- a/code/ship/shield.cpp +++ b/code/ship/shield.cpp @@ -912,14 +912,14 @@ int ship_is_shield_up( const object *obj, int quadrant ) { if ( (quadrant >= 0) && (quadrant < static_cast(obj->shield_quadrant.size()))) { // Just check one quadrant - if (shield_get_quad(obj, quadrant) > MAX(2.0f, 0.1f * shield_get_max_quad(obj))) { + if (shield_get_quad(obj, quadrant) > MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(obj))) { return 1; } } else { // Check all quadrants float strength = shield_get_strength(obj); - if ( strength > MAX(2.0f*4.0f, 0.1f * shield_get_max_strength(obj)) ) { + if ( strength > MAX(2.0f*4.0f, Shield_percent_skips_damage * shield_get_max_strength(obj)) ) { return 1; } } diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 238626cbba2..901d31f3237 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -17266,40 +17266,6 @@ const char *ship_subsys_get_canonical_name(const ship_subsys *ss) return ss->system_info->subobj_name; } -/** - * Return the shield strength of the specified quadrant on hit_objp - * - * @param hit_objp object pointer to ship getting hit - * @param quadrant_num shield quadrant that was hit - * @return strength of shields in the quadrant that was hit as a percentage, between 0 and 1.0 - */ -float ship_quadrant_shield_strength(const object *hit_objp, int quadrant_num) -{ - float max_quadrant; - - // If ship doesn't have shield mesh, then return - if ( hit_objp->flags[Object::Object_Flags::No_shields] ) { - return 0.0f; - } - - // If shields weren't hit, return 0 - if ( quadrant_num < 0 ) - return 0.0f; - - max_quadrant = shield_get_max_quad(hit_objp); - if ( max_quadrant <= 0 ) { - return 0.0f; - } - - Assertion(quadrant_num < static_cast(hit_objp->shield_quadrant.size()), "ship_quadrant_shield_strength() called with a quadrant of %d on a ship with " SIZE_T_ARG " quadrants; get a coder!\n", quadrant_num, hit_objp->shield_quadrant.size()); - - if(hit_objp->shield_quadrant[quadrant_num] > max_quadrant) - mprintf(("Warning: \"%s\" has shield quadrant strength of %f out of %f\n", - Ships[hit_objp->instance].ship_name, hit_objp->shield_quadrant[quadrant_num], max_quadrant)); - - return hit_objp->shield_quadrant[quadrant_num]/max_quadrant; -} - // Determine if a ship is threatened by any dumbfire projectiles (laser or missile) // input: sp => pointer to ship that might be threatened // exit: 0 => no dumbfire threats @@ -20715,7 +20681,7 @@ void ArmorType::ParseData() no_content = false; } - adt.piercing_start_pct = 0.1f; + adt.piercing_start_pct = Shield_percent_skips_damage; adt.piercing_type = -1; if(optional_string("+Weapon Piercing Effect Start Limit:")) { diff --git a/code/ship/ship.h b/code/ship/ship.h index 5a6e3ae02da..9d324ebc4a1 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1868,9 +1868,6 @@ extern int Show_shield_mesh; extern int Ship_auto_repair; // flag to indicate auto-repair of subsystem should occur #endif -void ship_subsystem_delete(ship *shipp); -float ship_quadrant_shield_strength(const object *hit_objp, int quadrant_num); - int ship_dumbfire_threat(ship *sp); int ship_lock_threat(ship *sp); diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 6d412278e0f..97f2b83ca47 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -2424,9 +2424,9 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi ImpactCondition(shipp->shield_armor_type_idx), HitType::SHIELD, 0.0f, - // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last ten percent of a shield doesn't matter - MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp))), - shield_get_max_quad(ship_objp) - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp)), + // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last X percent of a shield doesn't matter + MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp))), + shield_get_max_quad(ship_objp) - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp)), }; } else { impact_data[static_cast>(HitType::HULL)] = ConditionData { @@ -2455,9 +2455,9 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi ImpactCondition(shipp->shield_armor_type_idx), HitType::SHIELD, 0.0f, - // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last ten percent of a shield doesn't matter - MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp))), - shield_get_max_quad(ship_objp) - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp)), + // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last X percent of a shield doesn't matter + MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp))), + shield_get_max_quad(ship_objp) - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp)), }; if ( damage > 0.0f ) { @@ -2873,9 +2873,9 @@ void ship_apply_local_damage(object *ship_objp, object *other_obj, const vec3d * ImpactCondition(ship_p->shield_armor_type_idx), HitType::SHIELD, 0.0f, - // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last ten percent of a shield doesn't matter - MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp))), - shield_get_max_quad(ship_objp) - MAX(2.0f, 0.1f * shield_get_max_quad(ship_objp)), + // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last X percent of a shield doesn't matter + MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp))), + shield_get_max_quad(ship_objp) - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp)), }; } else { impact_data[static_cast>(HitType::HULL)] = ConditionData { diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 43b3922bb27..64cce0f3ff2 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -38,6 +38,7 @@ #include "network/multiutil.h" #include "object/objcollide.h" #include "object/objectdock.h" +#include "object/objectshield.h" #include "object/objectsnd.h" #include "parse/parsehi.h" #include "parse/parselo.h" @@ -7328,8 +7329,6 @@ void weapon_play_impact_sound(const weapon_info *wip, const vec3d *hitpos, bool */ void weapon_hit_do_sound(const object *hit_obj, const weapon_info *wip, const vec3d *hitpos, bool is_armed, int quadrant) { - float shield_str; - // If non-missiles (namely lasers) expire without hitting a ship, don't play impact sound if ( wip->subtype != WP_MISSILE ) { if ( !hit_obj ) { @@ -7366,14 +7365,16 @@ void weapon_hit_do_sound(const object *hit_obj, const weapon_info *wip, const ve if ( timestamp_elapsed(Weapon_impact_timer) ) { + float shield_percent; + if ( hit_obj->type == OBJ_SHIP && quadrant >= 0 ) { - shield_str = ship_quadrant_shield_strength(hit_obj, quadrant); + shield_percent = shield_get_quad_percent(hit_obj, quadrant); } else { - shield_str = 0.0f; + shield_percent = 0.0f; } - // play a shield hit if shields are above 10% max in this quadrant - if ( shield_str > 0.1f ) { + // play a shield hit if shields are above X% max in this quadrant + if ( shield_percent > Shield_percent_skips_damage ) { // Play a shield impact sound effect if ( !(Use_weapon_class_sounds_for_hits_to_player) && (hit_obj == Player_obj)) { snd_play_3d( gamesnd_get_game_sound(GameSounds::SHIELD_HIT_YOU), hitpos, &Eye_position ); From 199c7b79efc13c273c8109b4d20ed1bcd11a2955 Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Sun, 20 Jul 2025 04:45:27 -0700 Subject: [PATCH 261/466] Make sure global damage doesn't spawn impact effects (#6842) * check whether damage is global * use static * remove extraneous arguments --- code/ship/shiphit.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 97f2b83ca47..53adcc38b0c 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -2437,7 +2437,9 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi shipp->ship_max_hull_strength, }; } - maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); + if (!global_damage) { + maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); + } shiphit_hit_after_death(ship_objp, (damage * difficulty_scale_factor)); return; @@ -2681,7 +2683,9 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi } } - maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); + if (!global_damage) { + maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); + } // handle weapon and afterburner leeching here if(other_obj_is_weapon || other_obj_is_beam) { @@ -3033,7 +3037,7 @@ void ship_apply_global_damage(object *ship_objp, object *other_obj, const vec3d int n_quadrants = static_cast(ship_objp->shield_quadrant.size()); for (int i=0; i Date: Sun, 20 Jul 2025 15:00:47 -0400 Subject: [PATCH 262/466] Fix Edge-case with `$Preload briefing icon models:` (#6850) Found a very interesting edge case bug with `$Preload briefing icon models:.` Situation: Mission 1 has a briefing which has a ship class A as the class (and thus loads the model), but the ship class A is not actually in that mission so no subsystems are loaded. Then in mission 2, the ship class A is actually in the mission but FSO tries to reuse the slot that was loaded, but realizes no subsystems were loaded and makes an error message. I that is because the preload code for the briefing icons on `missionparse.cpp` line 6664 just calls `model_load` but does not specify the subsystem argument. I've also attached a retail mod reproducible campaign (2 missions, first is 5 seconds long). When the second mission loads the incorrect subsystem warning appears. This PR properly loads the subsystems and fixes the bug. Tested and works as expected. --- code/mission/missionparse.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 97c40da847e..d48678ed6d9 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6661,7 +6661,8 @@ bool post_process_mission(mission *pm) for (i = 0; i < Briefings[team].num_stages; i++) { const auto &stage = br[i]; for (int j = 0; j < stage.num_icons; j++) { - stage.icons[j].modelnum = model_load(Ship_info[stage.icons[j].ship_class].pof_file); + ship_info *sip = &Ship_info[stage.icons[j].ship_class]; + stage.icons[j].modelnum = model_load(sip->pof_file, sip); } } } From c53daa325c68c4dbce2f22a57d27c8dd923b924b Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 21 Jul 2025 15:32:25 -0500 Subject: [PATCH 263/466] Remove rocket ani assertion --- code/scpui/RocketRenderingInterface.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/scpui/RocketRenderingInterface.cpp b/code/scpui/RocketRenderingInterface.cpp index e5aa264b3a7..5a7a3b859ad 100644 --- a/code/scpui/RocketRenderingInterface.cpp +++ b/code/scpui/RocketRenderingInterface.cpp @@ -335,7 +335,9 @@ int RocketRenderingInterface::getBitmapNum(Rocket::Core::TextureHandle handle) void RocketRenderingInterface::advanceAnimation(Rocket::Core::TextureHandle handle, float advanceTime) { Assertion(handle != 0, "Invalid handle for setAnimationFrame"); - Assertion(get_texture(handle)->is_animation, "Tried to use advanceAnimation with a non-animation!"); + if (!get_texture(handle)->is_animation) { + return; + } auto tex = get_texture(handle); From 9b82daeefcf02daf3fa0de3ffea4404e66049be9 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Tue, 22 Jul 2025 00:17:22 +0200 Subject: [PATCH 264/466] Proper Ambient Light (#6849) * Remove ambient handling from amin / decal pass * Add ambient light to deferred pass * Make decals work and fix double insignia rendering * Fix decal vertex shader * render insignias as decals * Cleanup of old insignia stuff * Move insignia post-processing to model load * perform envmap lighting in deferred shader * Remove Envmapping from main shader * Fix MSVC warning * exclude def_files from clang tidy --- ci/linux/clang_tidy.sh | 2 +- code/decals/decals.cpp | 177 +++++++++--------- code/decals/decals.h | 21 +++ code/def_files/data/effects/decal-f.sdr | 13 -- code/def_files/data/effects/decal-v.sdr | 5 +- .../data/effects/deferred-clear-f.sdr | 2 +- code/def_files/data/effects/deferred-f.sdr | 121 +++++++++--- code/def_files/data/effects/deferred-v.sdr | 2 +- code/def_files/data/effects/lighting.sdr | 1 + code/def_files/data/effects/main-f.sdr | 80 +------- code/def_files/data/effects/main-g.sdr | 17 -- code/def_files/data/effects/main-v.sdr | 11 -- .../data/effects/model_shader_flags.h | 23 ++- code/graphics/2d.h | 2 + code/graphics/decal_draw_list.cpp | 11 -- code/graphics/light.cpp | 10 + code/graphics/material.cpp | 2 - code/graphics/opengl/gropengldeferred.cpp | 74 +++++--- code/graphics/opengl/gropenglshader.cpp | 2 + code/graphics/opengl/gropengltnl.cpp | 10 - code/graphics/uniforms.cpp | 10 - code/graphics/util/uniform_structs.h | 6 +- code/lighting/lighting.h | 3 +- code/math/vecmat.cpp | 13 ++ code/math/vecmat.h | 4 + code/model/model.h | 16 +- code/model/modelread.cpp | 93 +++++---- code/model/modelrender.cpp | 139 +++----------- code/model/modelrender.h | 5 - code/object/objectsort.cpp | 1 - code/ship/ship.cpp | 4 +- 31 files changed, 399 insertions(+), 481 deletions(-) diff --git a/ci/linux/clang_tidy.sh b/ci/linux/clang_tidy.sh index 4ec7a6a7191..8e514ef40fe 100755 --- a/ci/linux/clang_tidy.sh +++ b/ci/linux/clang_tidy.sh @@ -26,5 +26,5 @@ git diff -U0 --no-color "$BASE_COMMIT..$2" | \ -extra-arg="-DWITH_VULKAN" \ -extra-arg="-DVULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1" \ -extra-arg="-DVK_NO_PROTOTYPES" \ - -regex '(code(?!((\/graphics\/shaders\/compiled)|(\/globalincs\/windebug)))|freespace2|qtfred|test\/src|build|tools)\/.*\.(cpp|h)' \ + -regex '(code(?!((\/graphics\/shaders\/compiled)|(\/globalincs\/windebug)|(\/def_files\/data)))|freespace2|qtfred|test\/src|build|tools)\/.*\.(cpp|h)' \ -clang-tidy-binary /usr/bin/clang-tidy-16 -j$(nproc) -export-fixes "$(pwd)/clang-fixes.yaml" diff --git a/code/decals/decals.cpp b/code/decals/decals.cpp index 20d5a5c2da1..424c3807932 100644 --- a/code/decals/decals.cpp +++ b/code/decals/decals.cpp @@ -181,66 +181,52 @@ void parse_decals_table(const char* filename) { } } -struct Decal { - int definition_handle = -1; - object_h object; - int orig_obj_type = OBJ_NONE; - int submodel = -1; - - float creation_time = -1.0f; //!< The mission time at which this decal was created - float lifetime = -1.0f; //!< The time this decal is active. When negative it never expires - - vec3d position = vmd_zero_vector; - vec3d scale; - matrix orientation = vmd_identity_matrix; +Decal::Decal() { + vm_vec_make(&scale, 1.f, 1.f, 1.f); +} - Decal() { - vm_vec_make(&scale, 1.f, 1.f, 1.f); +bool Decal::isValid() const { + if (!object.isValid()) { + return false; + } + if (object.objp()->flags[Object::Object_Flags::Should_be_dead]) { + return false; } - bool isValid() const { - if (!object.isValid()) { - return false; - } - if (object.objp()->flags[Object::Object_Flags::Should_be_dead]) { - return false; - } + if (orig_obj_type != object.objp()->type) { + mprintf(("Decal object type for object %d has changed from %s to %s. Please let m!m know about this\n", + object.objnum, Object_type_names[orig_obj_type], Object_type_names[object.objp()->type])); + return false; + } - if (orig_obj_type != object.objp()->type) { - mprintf(("Decal object type for object %d has changed from %s to %s. Please let m!m know about this\n", - object.objnum, Object_type_names[orig_obj_type], Object_type_names[object.objp()->type])); + if (lifetime > 0.0f) { + if (f2fl(Missiontime) >= creation_time + lifetime) { + // Decal has expired return false; } + } - if (lifetime > 0.0f) { - if (f2fl(Missiontime) >= creation_time + lifetime) { - // Decal has expired - return false; - } - } - - auto objp = object.objp(); - if (objp->type == OBJ_SHIP) { - auto shipp = &Ships[objp->instance]; - auto model_instance = model_get_instance(shipp->model_instance_num); + auto objp = object.objp(); + if (objp->type == OBJ_SHIP) { + auto shipp = &Ships[objp->instance]; + auto model_instance = model_get_instance(shipp->model_instance_num); - Assertion(submodel >= 0 && submodel < object_get_model(objp)->n_models, - "Invalid submodel number detected!"); - auto smi = &model_instance->submodel[submodel]; + Assertion(submodel >= 0 && submodel < object_get_model(objp)->n_models, + "Invalid submodel number detected!"); + auto smi = &model_instance->submodel[submodel]; - if (smi->blown_off) { - return false; - } - } else { - Assertion(false, "Only ships are currently supported for decals!"); + if (smi->blown_off) { return false; } - - return true; + } else { + Assertion(false, "Only ships are currently supported for decals!"); + return false; } -}; -SCP_vector active_decals; + return true; +} + +SCP_vector active_decals, active_single_frame_decals; bool required_string_if_new(const char* token, bool new_entry) { if (!new_entry) { @@ -374,7 +360,7 @@ void initializeMission() { const float DECAL_ANGLE_CUTOFF = fl_radians(45.f); const float DECAL_ANGLE_FADE_START = fl_radians(30.f); -static matrix4 getDecalTransform(Decal& decal, float alpha) { +static matrix4 getDecalTransform(const Decal& decal, float alpha) { Assertion(decal.object.objp()->type == OBJ_SHIP, "Only ships are currently supported for decals!"); auto objp = decal.object.objp(); @@ -418,6 +404,51 @@ static matrix4 getDecalTransform(Decal& decal, float alpha) { return mat4; } +inline static void renderDecal(graphics::decal_draw_list& draw_list, const Decal& decal) { + auto mission_time = f2fl(Missiontime); + + int diffuse_bm = -1; + int glow_bm = -1; + int normal_bm = -1; + + auto decal_time = mission_time - decal.creation_time; + auto progress = decal_time / decal.lifetime; + + float alpha = 1.0f; + if (progress > 0.8) { + // Fade the decal out for the last 20% of its lifetime + alpha = 1.0f - smoothstep(0.8f, 1.0f, progress); + } + + if (std::holds_alternative(decal.definition_handle)) { + int definition_handle = std::get(decal.definition_handle); + Assertion(definition_handle >= 0 && definition_handle < (int) DecalDefinitions.size(), + "Invalid decal handle detected!"); + auto &decalDef = DecalDefinitions[definition_handle]; + + if (decalDef.getDiffuseBitmap() >= 0) { + diffuse_bm = decalDef.getDiffuseBitmap() + + + bm_get_anim_frame(decalDef.getDiffuseBitmap(), decal_time, 0.0f, decalDef.isDiffuseLooping()); + } + + if (decalDef.getGlowBitmap() >= 0) { + glow_bm = decalDef.getGlowBitmap() + + bm_get_anim_frame(decalDef.getGlowBitmap(), decal_time, 0.0f, decalDef.isGlowLooping()); + } + + if (decalDef.getNormalBitmap() >= 0) { + normal_bm = decalDef.getNormalBitmap() + + bm_get_anim_frame(decalDef.getNormalBitmap(), decal_time, 0.0f, decalDef.isNormalLooping()); + } + } + else { + std::tie(diffuse_bm, glow_bm, normal_bm) = std::get>(decal.definition_handle); + } + + draw_list.add_decal(diffuse_bm, glow_bm, normal_bm, decal_time, getDecalTransform(decal, alpha)); +} + void renderAll() { if (!Decal_system_active || !Decal_option_active || !gr_is_capable(gr_capability::CAPABILITY_INSTANCED_RENDERING)) { return; @@ -442,51 +473,21 @@ void renderAll() { ++iter; } - if (active_decals.empty()) { + if (active_decals.empty() && active_single_frame_decals.empty()) { return; } - auto mission_time = f2fl(Missiontime); - - graphics::decal_draw_list draw_list; - for (auto& decal : active_decals) { - - Assertion(decal.definition_handle >= 0 && decal.definition_handle < (int)DecalDefinitions.size(), - "Invalid decal handle detected!"); - auto& decalDef = DecalDefinitions[decal.definition_handle]; - int diffuse_bm = -1; - int glow_bm = -1; - int normal_bm = -1; - auto decal_time = mission_time - decal.creation_time; - auto progress = decal_time / decal.lifetime; - - float alpha = 1.0f; - if (progress > 0.8) { - // Fade the decal out for the last 20% of its lifetime - alpha = 1.0f - smoothstep(0.8f, 1.0f, progress); - } - - if (decalDef.getDiffuseBitmap() >= 0) { - diffuse_bm = decalDef.getDiffuseBitmap() - + bm_get_anim_frame(decalDef.getDiffuseBitmap(), decal_time, 0.0f, decalDef.isDiffuseLooping()); - } - - if (decalDef.getGlowBitmap() >= 0) { - glow_bm = decalDef.getGlowBitmap() - + bm_get_anim_frame(decalDef.getGlowBitmap(), decal_time, 0.0f, decalDef.isGlowLooping()); - } - - if (decalDef.getNormalBitmap() >= 0) { - normal_bm = decalDef.getNormalBitmap() - + bm_get_anim_frame(decalDef.getNormalBitmap(), decal_time, 0.0f, decalDef.isNormalLooping()); - } - - draw_list.add_decal(diffuse_bm, glow_bm, normal_bm, decal_time, getDecalTransform(decal, alpha)); - } + graphics::decal_draw_list draw_list; + for (auto& decal : active_decals) + renderDecal(draw_list, decal); + for (auto& decal : active_single_frame_decals) + renderDecal(draw_list, decal); draw_list.render(); + + active_single_frame_decals.clear(); } void addDecal(creation_info& info, const object* host, int submodel, const vec3d& local_pos, const matrix& local_orient) { @@ -531,4 +532,8 @@ void addDecal(creation_info& info, const object* host, int submodel, const vec3d active_decals.push_back(newDecal); } +void addSingleFrameDecal(Decal&& info) { + active_single_frame_decals.push_back(info); +} + } diff --git a/code/decals/decals.h b/code/decals/decals.h index a205b67c255..338a4fa14e4 100644 --- a/code/decals/decals.h +++ b/code/decals/decals.h @@ -50,6 +50,25 @@ class DecalDefinition { bool isNormalLooping() const; }; +struct Decal { + //DecalDefinition idx vs immediate diffuse/glow/normal + std::variant> definition_handle = -1; + object_h object; + int orig_obj_type = OBJ_NONE; + int submodel = -1; + + float creation_time = -1.0f; //!< The mission time at which this decal was created + float lifetime = -1.0f; //!< The time this decal is active. When negative it never expires + + vec3d position = vmd_zero_vector; + vec3d scale; + matrix orientation = vmd_identity_matrix; + + Decal(); + + bool isValid() const; +}; + extern SCP_vector DecalDefinitions; extern bool Decal_system_active; extern bool Decal_option_active; @@ -139,4 +158,6 @@ void addDecal(creation_info& info, const vec3d& local_pos, const matrix& local_orient); +void addSingleFrameDecal(Decal&& info); + } diff --git a/code/def_files/data/effects/decal-f.sdr b/code/def_files/data/effects/decal-f.sdr index 9b2e8bbbf18..138caae0ec5 100644 --- a/code/def_files/data/effects/decal-f.sdr +++ b/code/def_files/data/effects/decal-f.sdr @@ -29,9 +29,6 @@ layout (std140) uniform decalGlobalData { mat4 invViewMatrix; mat4 invProjMatrix; - vec3 ambientLight; - float pad0; - vec2 viewportSize; }; @@ -135,16 +132,6 @@ void main() { // Additive blending diffuse_out = vec4(color.rgb * alpha, 1.0); } - - // The main model shader applies ambient lighting by drawing the ambient part of the texture into the emissive - // texture. We do the same here to make sure the decal material is applied correctly - if (glow_blend_mode == 0) { - // Normal alpha blending - emissive_out = vec4(color.rgb * ambientLight, color.a * alpha); - } else { - // Additive blending - emissive_out = vec4(alpha * color.rgb * ambientLight, 1.0); - } } if (glow_index >= 0) { diff --git a/code/def_files/data/effects/decal-v.sdr b/code/def_files/data/effects/decal-v.sdr index 6a62549bd62..bd3511e0c7e 100644 --- a/code/def_files/data/effects/decal-v.sdr +++ b/code/def_files/data/effects/decal-v.sdr @@ -14,9 +14,6 @@ layout (std140) uniform decalGlobalData { mat4 invViewMatrix; mat4 invProjMatrix; - vec3 ambientLight; - float pad0; - vec2 viewportSize; }; @@ -40,6 +37,6 @@ void main() { modelMatrix[2][3] = 0.0; invModelMatrix = inverse(modelMatrix); - decalDirection = mat3(viewMatrix) * vec3(modelMatrix[0][2], modelMatrix[1][2], modelMatrix[2][2]); + decalDirection = mat3(viewMatrix) * modelMatrix[2].xyz; gl_Position = projMatrix * viewMatrix * modelMatrix * vertPosition; } diff --git a/code/def_files/data/effects/deferred-clear-f.sdr b/code/def_files/data/effects/deferred-clear-f.sdr index ed15e5516b0..4d49695ef90 100644 --- a/code/def_files/data/effects/deferred-clear-f.sdr +++ b/code/def_files/data/effects/deferred-clear-f.sdr @@ -7,7 +7,7 @@ out vec4 fragOut5; void main() { fragOut0 = vec4(0.0, 0.0, 0.0, 1.0); // color - fragOut1 = vec4(0.0, 0.0, -1000000.0, 1.0); // position + fragOut1 = vec4(0.0, 0.0, -1000000.0, 0.0); // position fragOut2 = vec4(0.0, 0.0, 0.0, 1.0); // normal fragOut3 = vec4(0.0, 0.0, 0.0, 0.0); // specular fragOut4 = vec4(0.0, 0.0, 0.0, 1.0); // emissive diff --git a/code/def_files/data/effects/deferred-f.sdr b/code/def_files/data/effects/deferred-f.sdr index 3953b00440d..95349253f6f 100644 --- a/code/def_files/data/effects/deferred-f.sdr +++ b/code/def_files/data/effects/deferred-f.sdr @@ -1,6 +1,6 @@ //? #version 150 #include "lighting.sdr" //! #include "lighting.sdr" - +#include "gamma.sdr" //! #include "gamma.sdr" #include "shadows.sdr" //! #include "shadows.sdr" out vec4 fragOut0; @@ -11,6 +11,11 @@ uniform sampler2D PositionBuffer; uniform sampler2D SpecBuffer; uniform sampler2DArray shadow_map; +#ifdef ENV_MAP +uniform samplerCube sEnvmap; +uniform samplerCube sIrrmap; +#endif + layout (std140) uniform globalDeferredData { mat4 shadow_mv_matrix; mat4 shadow_proj_matrix[4]; @@ -97,7 +102,7 @@ void GetLightInfo(vec3 position, in float alpha, in vec3 reflectDir, out vec3 li discard; } attenuation = 1.0 - clamp(sqrt(dist / lightRadius), 0.0, 1.0); - } + } else if (lightType == LT_TUBE) { // Tube light vec3 beamVec = vec3(modelViewMatrix * vec4(0.0, 0.0, -scale.z, 0.0)); vec3 beamDir = normalize(beamVec); @@ -138,7 +143,7 @@ void GetLightInfo(vec3 position, in float alpha, in vec3 reflectDir, out vec3 li discard; } attenuation = 1.0 - clamp(sqrt(dist / lightRadius), 0.0, 1.0); - } + } else if (lightType == LT_CONE) { lightDirOut = lightPosition - position.xyz; float coneDot = dot(normalize(-lightDirOut), coneDir); @@ -165,46 +170,108 @@ void GetLightInfo(vec3 position, in float alpha, in vec3 reflectDir, out vec3 li } } +#ifdef ENV_MAP +void ComputeEnvLight(float alpha, float ao, vec3 light_dir, vec3 eyeDir, vec3 normal, vec4 baseColor, vec4 specColor, out vec3 envLight) { + // Bit of a hack here, pretend we're doing blinn-phong shading and use a (modified version of) the derivation from + // Plausible Blinn-Phong Reflection of Standard Cube MIP-Maps - McGuire et al. + // to determine the mip bias. + // We use the specular term listed at http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html + // 1/(pi*alpha^2) * NdotM^((2/alpha^2)-2) + // so let s = (2/alpha^2 - 2) + // 1/2 log(s + 1) = 1/2 log(2/alpha^2 -1) + + const float ENV_REZ = 512; // Ideally this would be #define'd and shader recompiled with envmap rez changes + const float REZ_BIAS = log2(ENV_REZ * sqrt(3)); + + float alphaSqr = alpha * alpha; + float rough_bias = 0.5 * log2(2/alphaSqr - 1); + float mip_bias = REZ_BIAS - rough_bias; + + // Sample light, using mip bias to blur it. + vec3 env_light_dir = vec3(modelViewMatrix * vec4(light_dir, 0.0)); + vec4 specEnvColour = srgb_to_linear(textureLod(sEnvmap, env_light_dir, mip_bias)); + + vec3 halfVec = normal; + + // Lots of hacks here to get things to look right. We aren't loading a BRDF split-sum integral texture + // or properly calculating IBL levels. + // Fresnel calculation as with standard lights. + + vec3 fresnel = mix(specColor.rgb, FresnelSchlick(halfVec, eyeDir, specColor.rgb), specColor.a); + + // Pseudo-IBL, so use k_IBL + float k = alpha * alpha/ 2.0f; + + float NdotL = max(dot(light_dir, normal),0); + + float g1vNL = GeometrySchlickGGX(NdotL, k); + vec3 specEnvLighting = specEnvColour.rgb * fresnel * g1vNL; + + vec3 kD = vec3(1.0)-fresnel; + kD *= (vec3(1.0) - specColor.rgb); + vec3 diffEnvColor = srgb_to_linear(texture(sIrrmap, vec3(modelViewMatrix * vec4(normal, 0.0))).rgb); + vec3 diffEnvLighting = kD * baseColor.rgb * diffEnvColor * ao; + envLight = (specEnvLighting + diffEnvLighting) * baseColor.a; +} +#endif + void main() { vec2 screenPos = gl_FragCoord.xy * vec2(invScreenWidth, invScreenHeight); - vec3 position = texture(PositionBuffer, screenPos).xyz; + vec4 position_buffer = texture(PositionBuffer, screenPos); + vec3 position = position_buffer.xyz; if(abs(dot(position, position)) < nearPlane * nearPlane) discard; - vec3 diffColor = texture(ColorBuffer, screenPos).rgb; + vec4 diffuse = texture(ColorBuffer, screenPos); + vec3 diffColor = diffuse.rgb; vec4 normalData = texture(NormalBuffer, screenPos); - vec4 specColor = texture(SpecBuffer, screenPos); // The vector in the normal buffer could be longer than the unit vector since decal rendering only adds to the normal buffer vec3 normal = normalize(normalData.xyz); float gloss = normalData.a; float roughness = clamp(1.0f - gloss, 0.0f, 1.0f); float alpha = roughness * roughness; - float fresnel = specColor.a; vec3 eyeDir = normalize(-position); - - vec3 lightDir; - float attenuation; - float area_normalisation; vec3 reflectDir = reflect(-eyeDir, normal); - GetLightInfo(position, alpha, reflectDir, lightDir, attenuation, area_normalisation); - - if (enable_shadows) { - vec4 fragShadowPos = shadow_mv_matrix * inv_view_matrix * vec4(position, 1.0); - vec4 fragShadowUV[4]; - fragShadowUV[0] = transformToShadowMap(shadow_proj_matrix[0], 0, fragShadowPos); - fragShadowUV[1] = transformToShadowMap(shadow_proj_matrix[1], 1, fragShadowPos); - fragShadowUV[2] = transformToShadowMap(shadow_proj_matrix[2], 2, fragShadowPos); - fragShadowUV[3] = transformToShadowMap(shadow_proj_matrix[3], 3, fragShadowPos); - - attenuation *= getShadowValue(shadow_map, -position.z, fragShadowPos.z, fragShadowUV, fardist, middist, - neardist, veryneardist); - } + vec4 specColor = texture(SpecBuffer, screenPos); - vec3 halfVec = normalize(lightDir + eyeDir); - float NdotL = clamp(dot(normal, lightDir), 0.0, 1.0); vec4 fragmentColor = vec4(1.0); - fragmentColor.rgb = computeLighting(specColor.rgb, diffColor, lightDir, normal.xyz, halfVec, eyeDir, roughness, fresnel, NdotL).rgb * diffuseLightColor * attenuation * area_normalisation; + + if (lightType == LT_AMBIENT) { + float ao = position_buffer.w; + fragmentColor.rgb = diffuseLightColor * diffColor * ao; + +#ifdef ENV_MAP + vec3 envLight; + ComputeEnvLight(alpha, ao, reflectDir, eyeDir, normal, diffuse, specColor, envLight); + fragmentColor.rgb += envLight; +#endif + } + else { + float fresnel = specColor.a; + + vec3 lightDir; + float attenuation; + float area_normalisation; + GetLightInfo(position, alpha, reflectDir, lightDir, attenuation, area_normalisation); + + if (enable_shadows) { + vec4 fragShadowPos = shadow_mv_matrix * inv_view_matrix * vec4(position, 1.0); + vec4 fragShadowUV[4]; + fragShadowUV[0] = transformToShadowMap(shadow_proj_matrix[0], 0, fragShadowPos); + fragShadowUV[1] = transformToShadowMap(shadow_proj_matrix[1], 1, fragShadowPos); + fragShadowUV[2] = transformToShadowMap(shadow_proj_matrix[2], 2, fragShadowPos); + fragShadowUV[3] = transformToShadowMap(shadow_proj_matrix[3], 3, fragShadowPos); + + attenuation *= getShadowValue(shadow_map, -position.z, fragShadowPos.z, fragShadowUV, fardist, middist, + neardist, veryneardist); + } + + vec3 halfVec = normalize(lightDir + eyeDir); + float NdotL = clamp(dot(normal, lightDir), 0.0, 1.0); + fragmentColor.rgb = computeLighting(specColor.rgb, diffColor, lightDir, normal.xyz, halfVec, eyeDir, roughness, fresnel, NdotL).rgb * diffuseLightColor * attenuation * area_normalisation; + } + fragOut0 = max(fragmentColor, vec4(0.0)); } diff --git a/code/def_files/data/effects/deferred-v.sdr b/code/def_files/data/effects/deferred-v.sdr index b454a42a02e..f1633569875 100644 --- a/code/def_files/data/effects/deferred-v.sdr +++ b/code/def_files/data/effects/deferred-v.sdr @@ -29,7 +29,7 @@ layout (std140) uniform lightData { void main() { - if (lightType == LT_DIRECTIONAL) { + if (lightType == LT_DIRECTIONAL || lightType == LT_AMBIENT) { gl_Position = vec4(vertPosition.xyz, 1.0); } else { gl_Position = projMatrix * modelViewMatrix * vec4(vertPosition.xyz * scale, 1.0); diff --git a/code/def_files/data/effects/lighting.sdr b/code/def_files/data/effects/lighting.sdr index 969ff908422..799617b52eb 100644 --- a/code/def_files/data/effects/lighting.sdr +++ b/code/def_files/data/effects/lighting.sdr @@ -3,6 +3,7 @@ const int LT_DIRECTIONAL = 0; // A light like a sun const int LT_POINT = 1; // A point light, like an explosion const int LT_TUBE = 2; // A tube light, like a fluorescent light const int LT_CONE = 3; // A cone light, like a flood light +const int LT_AMBIENT = 4; // Directionless ambient light const float SPEC_FACTOR_NO_SPEC_MAP = 0.1; const float GLOW_MAP_INTENSITY = 1.5; diff --git a/code/def_files/data/effects/main-f.sdr b/code/def_files/data/effects/main-f.sdr index 26a769eab78..e60324df170 100644 --- a/code/def_files/data/effects/main-f.sdr +++ b/code/def_files/data/effects/main-f.sdr @@ -32,7 +32,6 @@ layout (std140) uniform modelData { mat4 textureMatrix; mat4 shadow_mv_matrix; mat4 shadow_proj_matrix[4]; - mat4 envMatrix; vec4 color; @@ -50,6 +49,7 @@ layout (std140) uniform modelData { int n_lights; float defaultGloss; + //EXCLUSIVELY used for non-deferred rendering vec3 ambientFactor; int desaturate; @@ -85,22 +85,16 @@ layout (std140) uniform modelData { float fardist; int sGlowmapIndex; - int sSpecmapIndex; int sNormalmapIndex; int sAmbientmapIndex; - int sMiscmapIndex; + int sMiscmapIndex; float alphaMult; - int flags; }; in VertexOutput { -#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_ENV - vec3 envReflect; -#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_ENV - mat3 tangentMatrix; #prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_FOG @@ -126,10 +120,6 @@ uniform sampler2DArray sGlowmap; #prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_SPEC uniform sampler2DArray sSpecmap; #prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_SPEC -#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_ENV -uniform samplerCube sEnvmap; -uniform samplerCube sIrrmap; -#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_ENV #prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_NORMAL uniform sampler2DArray sNormalmap; #prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_NORMAL @@ -187,7 +177,7 @@ void GetLightInfo(int i, out vec3 lightDir, out float attenuation) vec3 CalculateLighting(vec3 normal, vec3 diffuseMaterial, vec3 specularMaterial, float gloss, float fresnel, float shadow, float aoFactor) { vec3 eyeDir = vec3(normalize(-vertIn.position).xyz); - vec3 lightAmbient = (emissionFactor + ambientFactor * ambientFactor) * aoFactor; // ambientFactor^2 due to legacy OpenGL compatibility behavior + vec3 lightAmbient = ambientFactor * aoFactor; vec3 lightDiffuse = vec3(0.0, 0.0, 0.0); vec3 lightSpecular = vec3(0.0, 0.0, 0.0); #pragma optionNV unroll all @@ -342,19 +332,7 @@ void main() #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_MISC // Lights aren't applied when we are rendering to the G-buffers since that gets handled later - #prereplace IF_FLAG MODEL_SDR_FLAG_DEFERRED - #prereplace IF_FLAG MODEL_SDR_FLAG_LIGHT - // Ambient lighting still needs to be done since that counts as an "emissive" color - vec3 lightAmbient = (emissionFactor + ambientFactor * ambientFactor) * aoFactors.x; // ambientFactor^2 due to legacy OpenGL compatibility behavior - emissiveColor.rgb += baseColor.rgb * lightAmbient; - #prereplace ELSE_FLAG //MODEL_SDR_FLAG_LIGHT - #prereplace IF_FLAG MODEL_SDR_FLAG_SPEC - baseColor.rgb += pow(1.0 - clamp(dot(eyeDir, normal), 0.0, 1.0), 5.0 * clamp(glossData, 0.01, 1.0)) * specColor.rgb; - #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_SPEC - // If there is no lighting then we copy the color data so far into the - emissiveColor.rgb += baseColor.rgb; - #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_LIGHT - #prereplace ELSE_FLAG //MODEL_SDR_FLAG_DEFERRED + #prereplace IF_NOT_FLAG MODEL_SDR_FLAG_DEFERRED #prereplace IF_FLAG MODEL_SDR_FLAG_LIGHT float shadow = 1.0; #prereplace IF_FLAG MODEL_SDR_FLAG_SHADOWS @@ -368,54 +346,6 @@ void main() #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_LIGHT #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_DEFERRED - #prereplace IF_FLAG MODEL_SDR_FLAG_ENV - #prereplace IF_FLAG MODEL_SDR_FLAG_LIGHT - // Bit of a hack here, pretend we're doing blinn-phong shading and use a (modified version of) the derivation from - // Plausible Blinn-Phong Reflection of Standard Cube MIP-Maps - McGuire et al. - // to determine the mip bias. - // We use the specular term listed at http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html - // 1/(pi*alpha^2) * NdotM^((2/alpha^2)-2) - // so let s = (2/alpha^2 - 2) - // 1/2 log(s + 1) = 1/2 log(2/alpha^2 -1) - - const float ENV_REZ = 512; // Ideally this would be #define'd and shader recompiled with envmap rez changes - const float REZ_BIAS = log2(ENV_REZ * sqrt(3)); - - float roughness = clamp(1.0f - glossData, 0.0f, 1.0f); - float alpha = roughness * roughness; - float alphaSqr = alpha * alpha; - float rough_bias = 0.5 * log2(2/alphaSqr - 1); - float mip_bias = REZ_BIAS - rough_bias; - - // Sample light, using mip bias to blur it. - vec3 light_dir = reflect(-eyeDir, normal); - vec3 env_light_dir = vec3(envMatrix * vec4(light_dir, 0.0)); - vec4 specEnvColour = srgb_to_linear(textureLod(sEnvmap, env_light_dir, mip_bias)); - - vec3 halfVec = normal; - - // Lots of hacks here to get things to look right. We aren't loading a BRDF split-sum integral texture - // or properly calculating IBL levels. - // Fresnel calculation as with standard lights. - - vec3 fresnel = mix(specColor.rgb, FresnelSchlick(halfVec, eyeDir, specColor.rgb), specColor.a); - - // Pseudo-IBL, so use k_IBL - float k = alpha * alpha/ 2.0f; - - float NdotL = max(dot(light_dir, normal),0); - - float g1vNL = GeometrySchlickGGX(NdotL, k); - vec3 specEnvLighting = specEnvColour.rgb * fresnel * g1vNL; - - vec3 kD = vec3(1.0)-fresnel; - kD *= (vec3(1.0) - specColor.rgb); - vec3 diffEnvColor = srgb_to_linear(texture(sIrrmap, vec3(envMatrix * vec4(normal, 0.0))).rgb); - vec3 diffEnvLighting = kD * baseColor.rgb * diffEnvColor * aoFactors.x; - emissiveColor.rgb += (specEnvLighting + diffEnvLighting) * baseColor.a; - #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_LIGHT - #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_ENV - #prereplace IF_FLAG MODEL_SDR_FLAG_GLOW vec3 glowColor = texture(sGlowmap, vec3(texCoord, float(sGlowmapIndex))).rgb; #prereplace IF_FLAG MODEL_SDR_FLAG_MISC @@ -486,7 +416,7 @@ void main() fragOut0 = baseColor; #prereplace IF_FLAG MODEL_SDR_FLAG_DEFERRED - fragOut1 = vec4(vertIn.position.xyz, 1.0); + fragOut1 = vec4(vertIn.position.xyz, aoFactors.x); fragOut2 = vec4(normal, glossData); fragOut3 = vec4(specColor.rgb, fresnelFactor); fragOut4 = emissiveColor; diff --git a/code/def_files/data/effects/main-g.sdr b/code/def_files/data/effects/main-g.sdr index b869ea5f46d..24db6200630 100644 --- a/code/def_files/data/effects/main-g.sdr +++ b/code/def_files/data/effects/main-g.sdr @@ -39,7 +39,6 @@ layout (std140) uniform modelData { mat4 textureMatrix; mat4 shadow_mv_matrix; mat4 shadow_proj_matrix[4]; - mat4 envMatrix; vec4 color; @@ -103,10 +102,6 @@ layout (std140) uniform modelData { }; in VertexOutput { -#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_ENV - vec3 envReflect; -#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_ENV - mat3 tangentMatrix; #prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_FOG @@ -131,10 +126,6 @@ in VertexOutput { } vertIn[]; out VertexOutput { -#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_ENV - vec3 envReflect; -#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_ENV - mat3 tangentMatrix; #prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_FOG @@ -181,10 +172,6 @@ out VertexOutput { gl_Layer = instanceID; - #prereplace IF_FLAG MODEL_SDR_FLAG_ENV - vertOut.envReflect = vertIn[vert].envReflect; - #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_ENV - #prereplace IF_FLAG MODEL_SDR_FLAG_NORMAL vertOut.tangentMatrix = vertIn[vert].tangentMatrix; #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_NORMAL @@ -239,10 +226,6 @@ out VertexOutput { vertOut.normal = vertIn[vert].normal; vertOut.texCoord = vertIn[vert].texCoord; - #prereplace IF_FLAG MODEL_SDR_FLAG_ENV - vertOut.envReflect = vertIn[vert].envReflect; - #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_ENV - #prereplace IF_FLAG MODEL_SDR_FLAG_NORMAL vertOut.tangentMatrix = vertIn[vert].tangentMatrix; #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_NORMAL diff --git a/code/def_files/data/effects/main-v.sdr b/code/def_files/data/effects/main-v.sdr index eaa3185d25c..2033c944165 100644 --- a/code/def_files/data/effects/main-v.sdr +++ b/code/def_files/data/effects/main-v.sdr @@ -34,7 +34,6 @@ layout (std140) uniform modelData { mat4 textureMatrix; mat4 shadow_mv_matrix; mat4 shadow_proj_matrix[4]; - mat4 envMatrix; vec4 color; @@ -102,10 +101,6 @@ uniform samplerBuffer transform_tex; #prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_TRANSFORM out VertexOutput { -#prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_ENV - vec3 envReflect; -#prereplace ENDIF_FLAG_COMPILED MODEL_SDR_FLAG_ENV - mat3 tangentMatrix; #prereplace IF_FLAG_COMPILED MODEL_SDR_FLAG_FOG @@ -193,12 +188,6 @@ void main() vec3 b = cross(normal, t) * vertTangent.w; vertOut.tangentMatrix = mat3(t, b, normal); - #prereplace IF_FLAG MODEL_SDR_FLAG_ENV - // Environment mapping reflection vector. - vec3 envReflect = reflect(normalize(position.xyz), normal); - vertOut.envReflect = vec3(envMatrix * vec4(envReflect, 0.0)); - #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_ENV - #prereplace IF_FLAG MODEL_SDR_FLAG_FOG vertOut.fogDist = clamp((gl_Position.z - fogStart) * 0.75 * fogScale, 0.0, 1.0); #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_FOG diff --git a/code/def_files/data/effects/model_shader_flags.h b/code/def_files/data/effects/model_shader_flags.h index 51a08ee9f52..a2ebe122e35 100644 --- a/code/def_files/data/effects/model_shader_flags.h +++ b/code/def_files/data/effects/model_shader_flags.h @@ -14,22 +14,21 @@ SDR_FLAG(MODEL_SDR_FLAG_HDR , (1 << 2) , false) SDR_FLAG(MODEL_SDR_FLAG_DIFFUSE , (1 << 3) , false) SDR_FLAG(MODEL_SDR_FLAG_GLOW , (1 << 4) , false) SDR_FLAG(MODEL_SDR_FLAG_SPEC , (1 << 5) , false) -SDR_FLAG(MODEL_SDR_FLAG_ENV , (1 << 6) , false) -SDR_FLAG(MODEL_SDR_FLAG_NORMAL , (1 << 7) , false) -SDR_FLAG(MODEL_SDR_FLAG_AMBIENT , (1 << 8) , false) -SDR_FLAG(MODEL_SDR_FLAG_MISC , (1 << 9) , false) -SDR_FLAG(MODEL_SDR_FLAG_TEAMCOLOR , (1 << 10), false) -SDR_FLAG(MODEL_SDR_FLAG_FOG , (1 << 11), false) -SDR_FLAG(MODEL_SDR_FLAG_TRANSFORM , (1 << 12), false) -SDR_FLAG(MODEL_SDR_FLAG_SHADOWS , (1 << 13), false) -SDR_FLAG(MODEL_SDR_FLAG_THRUSTER , (1 << 14), false) -SDR_FLAG(MODEL_SDR_FLAG_ALPHA_MULT , (1 << 15), false) +SDR_FLAG(MODEL_SDR_FLAG_NORMAL , (1 << 6) , false) +SDR_FLAG(MODEL_SDR_FLAG_AMBIENT , (1 << 7) , false) +SDR_FLAG(MODEL_SDR_FLAG_MISC , (1 << 8) , false) +SDR_FLAG(MODEL_SDR_FLAG_TEAMCOLOR , (1 << 9), false) +SDR_FLAG(MODEL_SDR_FLAG_FOG , (1 << 10), false) +SDR_FLAG(MODEL_SDR_FLAG_TRANSFORM , (1 << 11), false) +SDR_FLAG(MODEL_SDR_FLAG_SHADOWS , (1 << 12), false) +SDR_FLAG(MODEL_SDR_FLAG_THRUSTER , (1 << 13), false) +SDR_FLAG(MODEL_SDR_FLAG_ALPHA_MULT , (1 << 14), false) #ifndef MODEL_SDR_FLAG_MODE_GLSL //The following ones are used ONLY as compile-time flags, but they still need to be defined here to ensure no conflict occurs //But since these are checked with ifdefs even for the large shader, they must never be available in GLSL mode -SDR_FLAG(MODEL_SDR_FLAG_SHADOW_MAP , (1 << 16), true) -SDR_FLAG(MODEL_SDR_FLAG_THICK_OUTLINES, (1 << 17), true) +SDR_FLAG(MODEL_SDR_FLAG_SHADOW_MAP , (1 << 15), true) +SDR_FLAG(MODEL_SDR_FLAG_THICK_OUTLINES, (1 << 16), true) #endif \ No newline at end of file diff --git a/code/graphics/2d.h b/code/graphics/2d.h index 7158fb8075e..b701bdeefc6 100644 --- a/code/graphics/2d.h +++ b/code/graphics/2d.h @@ -239,6 +239,8 @@ enum shader_type { #define SDR_FLAG_TONEMAPPING_LINEAR_OUT (1 << 0) +#define SDR_FLAG_ENV_MAP (1 << 0) + enum class uniform_block_type { Lights = 0, diff --git a/code/graphics/decal_draw_list.cpp b/code/graphics/decal_draw_list.cpp index bcb84f70fab..b0f81848da6 100644 --- a/code/graphics/decal_draw_list.cpp +++ b/code/graphics/decal_draw_list.cpp @@ -82,17 +82,6 @@ void decal_draw_list::prepare_global_data() { header->viewportSize.x = (float) gr_screen.max_w; header->viewportSize.y = (float) gr_screen.max_h; - gr_get_ambient_light(&header->ambientLight); - - // Square the ambient part of the light to match the formula used in the main model shader - header->ambientLight.xyz.x *= header->ambientLight.xyz.x; - header->ambientLight.xyz.y *= header->ambientLight.xyz.y; - header->ambientLight.xyz.z *= header->ambientLight.xyz.z; - - header->ambientLight.xyz.x += gr_light_emission[0]; - header->ambientLight.xyz.y += gr_light_emission[1]; - header->ambientLight.xyz.z += gr_light_emission[2]; - for (auto& [batch_info, draw_info] : _draws) { auto info = aligner.addTypedElement(); info->diffuse_index = batch_info.diffuse < 0 ? -1 : bm_get_array_index(batch_info.diffuse); diff --git a/code/graphics/light.cpp b/code/graphics/light.cpp index 77acf272b39..c499fa24f2a 100644 --- a/code/graphics/light.cpp +++ b/code/graphics/light.cpp @@ -344,6 +344,16 @@ void gr_get_ambient_light(vec3d* light_vector) { light_vector->xyz.x = over.handle(abv.handle(gr_light_ambient[0])); light_vector->xyz.y = over.handle(abv.handle(gr_light_ambient[1])); light_vector->xyz.z = over.handle(abv.handle(gr_light_ambient[2])); + + //AmbientFactor^2 due to legacy OpenGL behaviour + *light_vector *= *light_vector; + + //For some reason, emissive is in here as well... + if (Cmdline_emissive) { + light_vector->xyz.x += gr_light_emission[0]; + light_vector->xyz.y += gr_light_emission[1]; + light_vector->xyz.z += gr_light_emission[2]; + } } void gr_lighting_fill_uniforms(void* data_out, size_t buffer_size) { diff --git a/code/graphics/material.cpp b/code/graphics/material.cpp index 6c6ab8ae6d6..d4e05cba1a1 100644 --- a/code/graphics/material.cpp +++ b/code/graphics/material.cpp @@ -774,8 +774,6 @@ int model_material::get_shader_runtime_flags() const { flags |= MODEL_SDR_FLAG_GLOW; if (get_texture_map(TM_SPECULAR_TYPE) > 0 || get_texture_map(TM_SPEC_GLOSS_TYPE) > 0) flags |= MODEL_SDR_FLAG_SPEC; - if (ENVMAP > 0) - flags |= MODEL_SDR_FLAG_ENV; if (get_texture_map(TM_NORMAL_TYPE) > 0) flags |= MODEL_SDR_FLAG_NORMAL; if (get_texture_map(TM_AMBIENT_TYPE) > 0) diff --git a/code/graphics/opengl/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp index cce19f69bfe..c571d20a7ee 100644 --- a/code/graphics/opengl/gropengldeferred.cpp +++ b/code/graphics/opengl/gropengldeferred.cpp @@ -9,6 +9,7 @@ #include "gropengltnl.h" #include "graphics/2d.h" +#include "graphics/light.h" #include "graphics/matrix.h" #include "graphics/util/UniformAligner.h" #include "graphics/util/UniformBuffer.h" @@ -223,7 +224,7 @@ void gr_opengl_deferred_lighting_finish() // GL_state.DepthFunc(GL_GREATER); // GL_state.DepthMask(GL_FALSE); - opengl_shader_set_current(gr_opengl_maybe_create_shader(SDR_TYPE_DEFERRED_LIGHTING, 0)); + opengl_shader_set_current(gr_opengl_maybe_create_shader(SDR_TYPE_DEFERRED_LIGHTING, ENVMAP > 0 ? SDR_FLAG_ENV_MAP : 0)); // Render on top of the composite buffer texture glDrawBuffer(GL_COLOR_ATTACHMENT5); @@ -247,6 +248,16 @@ void gr_opengl_deferred_lighting_finish() GL_state.Texture.Enable(4, GL_TEXTURE_2D_ARRAY, Shadow_map_texture); } + if (ENVMAP > 0) { + Current_shader->program->Uniforms.setTextureUniform("sEnvmap", 5); + Current_shader->program->Uniforms.setTextureUniform("sIrrmap", 6); + float u_scale, v_scale; + uint32_t array_index; + gr_opengl_tcache_set(ENVMAP, TCACHE_TYPE_CUBEMAP, &u_scale, &v_scale, &array_index, 5); + gr_opengl_tcache_set(IRRMAP, TCACHE_TYPE_CUBEMAP, &u_scale, &v_scale, &array_index, 6); + Assertion(array_index == 0, "Cube map arrays are not supported yet!"); + } + // We need to use stable sorting here to make sure that the relative ordering of the same light types is the same as // the rest of the code. Otherwise the shadow mapping would be applied while rendering the wrong light which would // lead to flickering lights in some circumstances @@ -254,7 +265,7 @@ void gr_opengl_deferred_lighting_finish() using namespace graphics; // We need to precompute how many elements we are going to need - size_t num_data_elements = Lights.size(); + size_t num_data_elements = Lights.size() + 1; // Get a uniform buffer for our data auto light_buffer = gr_get_uniform_buffer(uniform_block_type::Lights, num_data_elements); @@ -286,6 +297,8 @@ void gr_opengl_deferred_lighting_finish() case Light_Type::Tube: cylinder_lights.push_back(l); break; + case Light_Type::Ambient: + UNREACHABLE("Multiple ambient lights are not supported!"); } } { @@ -312,35 +325,51 @@ void gr_opengl_deferred_lighting_finish() header->invScreenHeight = 1.0f / gr_screen.max_h; header->nearPlane = gr_near_plane; + { + //Prepare ambient light + light& l = full_frame_lights.emplace_back(); + vec3d ambient; + gr_get_ambient_light(&ambient); + l.r = ambient.xyz.x; + l.g = ambient.xyz.y; + l.b = ambient.xyz.z; + l.type = Light_Type::Ambient; + l.intensity = 1.f; + l.source_radius = 0.f; + } + // Only the first directional light uses shaders so we need to know when we already saw that light bool first_directional = true; for (auto& l : full_frame_lights) { auto light_data = prepare_light_uniforms(l, light_uniform_aligner); - if (Shadow_quality != ShadowQuality::Disabled) { - light_data->enable_shadows = first_directional ? 1 : 0; - } - // Global light direction should match shadow light direction - if (first_directional) { - global_light = &l; - global_light_diffuse = light_data->diffuseLightColor; - } + if (l.type == Light_Type::Directional ) { + if (Shadow_quality != ShadowQuality::Disabled) { + light_data->enable_shadows = first_directional ? 1 : 0; + } + + // Global light direction should match shadow light direction + if (first_directional) { + global_light = &l; + global_light_diffuse = light_data->diffuseLightColor; - vec4 light_dir; - light_dir.xyzw.x = -l.vec.xyz.x; - light_dir.xyzw.y = -l.vec.xyz.y; - light_dir.xyzw.z = -l.vec.xyz.z; - light_dir.xyzw.w = 0.0f; - vec4 view_dir; + first_directional = false; + } - vm_vec_transform(&view_dir, &light_dir, &gr_view_matrix); + vec4 light_dir; + light_dir.xyzw.x = -l.vec.xyz.x; + light_dir.xyzw.y = -l.vec.xyz.y; + light_dir.xyzw.z = -l.vec.xyz.z; + light_dir.xyzw.w = 0.0f; + vec4 view_dir; - light_data->lightDir.xyz.x = view_dir.xyzw.x; - light_data->lightDir.xyz.y = view_dir.xyzw.y; - light_data->lightDir.xyz.z = view_dir.xyzw.z; + vm_vec_transform(&view_dir, &light_dir, &gr_view_matrix); - first_directional = false; + light_data->lightDir.xyz.x = view_dir.xyzw.x; + light_data->lightDir.xyz.y = view_dir.xyzw.y; + light_data->lightDir.xyz.z = view_dir.xyzw.z; + } } for (auto& l : sphere_lights) { auto light_data = prepare_light_uniforms(l, light_uniform_aligner); @@ -389,7 +418,8 @@ void gr_opengl_deferred_lighting_finish() { for (size_t i = 0; i(); + auto matrix_data = matrix_uniform_aligner.addTypedElement(); + matrix_data->modelViewMatrix = gr_env_texture_matrix; } for (auto& l : sphere_lights) { auto matrix_data = matrix_uniform_aligner.addTypedElement(); diff --git a/code/graphics/opengl/gropenglshader.cpp b/code/graphics/opengl/gropenglshader.cpp index 9c1dca46b97..f17cd83ee45 100644 --- a/code/graphics/opengl/gropenglshader.cpp +++ b/code/graphics/opengl/gropenglshader.cpp @@ -190,6 +190,8 @@ static opengl_shader_variant_t GL_shader_variants[] = { {SDR_TYPE_EFFECT_PARTICLE, true, SDR_FLAG_PARTICLE_POINT_GEN, "FLAG_EFFECT_GEOMETRY", {opengl_vert_attrib::UVEC}, "Geometry shader point-based particles"}, + {SDR_TYPE_DEFERRED_LIGHTING, false, SDR_FLAG_ENV_MAP, "ENV_MAP", {}, "Render ambient light with env and irrmaps"}, + {SDR_TYPE_POST_PROCESS_BLUR, false, SDR_FLAG_BLUR_HORIZONTAL, "PASS_0", {}, "Horizontal blur pass"}, {SDR_TYPE_POST_PROCESS_BLUR, false, SDR_FLAG_BLUR_VERTICAL, "PASS_1", {}, "Vertical blur pass"}, diff --git a/code/graphics/opengl/gropengltnl.cpp b/code/graphics/opengl/gropengltnl.cpp index db51f62b7bc..70f97fd3654 100644 --- a/code/graphics/opengl/gropengltnl.cpp +++ b/code/graphics/opengl/gropengltnl.cpp @@ -856,10 +856,6 @@ void opengl_tnl_set_model_material(model_material *material_info) Current_shader->program->Uniforms.setTextureUniform("sGlowmap", 1); if (setAllUniforms || (flags & MODEL_SDR_FLAG_SPEC)) Current_shader->program->Uniforms.setTextureUniform("sSpecmap", 2); - if (setAllUniforms || (flags & MODEL_SDR_FLAG_ENV)) { - Current_shader->program->Uniforms.setTextureUniform("sEnvmap", 3); - Current_shader->program->Uniforms.setTextureUniform("sIrrmap", 11); - } if (setAllUniforms || (flags & MODEL_SDR_FLAG_NORMAL)) Current_shader->program->Uniforms.setTextureUniform("sNormalmap", 4); if (setAllUniforms || (flags & MODEL_SDR_FLAG_AMBIENT)) @@ -912,12 +908,6 @@ void opengl_tnl_set_model_material(model_material *material_info) } } - if (ENVMAP > 0) { - gr_opengl_tcache_set(ENVMAP, TCACHE_TYPE_CUBEMAP, &u_scale, &v_scale, &array_index, 3); - gr_opengl_tcache_set(IRRMAP, TCACHE_TYPE_CUBEMAP, &u_scale, &v_scale, &array_index, 11); - Assertion(array_index == 0, "Cube map arrays are not supported yet!"); - } - if (material_info->get_texture_map(TM_NORMAL_TYPE) > 0) { gr_opengl_tcache_set(material_info->get_texture_map(TM_NORMAL_TYPE), TCACHE_TYPE_NORMAL, diff --git a/code/graphics/uniforms.cpp b/code/graphics/uniforms.cpp index fa7b27258c3..70cef3ea4e9 100644 --- a/code/graphics/uniforms.cpp +++ b/code/graphics/uniforms.cpp @@ -151,16 +151,6 @@ void convert_model_material(model_uniform_data* data_out, data_out->gammaSpec = 0; data_out->alphaGloss = 0; } - - if (ENVMAP > 0) { - if (material.get_texture_map(TM_SPEC_GLOSS_TYPE) > 0) { - data_out->envGloss = 1; - } else { - data_out->envGloss = 0; - } - - data_out->envMatrix = gr_env_texture_matrix; - } if (material.get_texture_map(TM_NORMAL_TYPE) > 0) { data_out->sNormalmapIndex = bm_get_array_index(material.get_texture_map(TM_NORMAL_TYPE)); diff --git a/code/graphics/util/uniform_structs.h b/code/graphics/util/uniform_structs.h index 5c3411e03e5..391e6d36fa5 100644 --- a/code/graphics/util/uniform_structs.h +++ b/code/graphics/util/uniform_structs.h @@ -81,7 +81,6 @@ struct model_uniform_data { matrix4 textureMatrix; matrix4 shadow_mv_matrix; matrix4 shadow_proj_matrix[4]; - matrix4 envMatrix; vec4 color; @@ -139,7 +138,7 @@ struct model_uniform_data { int sMiscmapIndex; float alphaMult; int flags; - int pad[1]; + float pad; }; const size_t model_uniform_data_size = sizeof(model_uniform_data); @@ -181,9 +180,6 @@ struct decal_globals { matrix4 invViewMatrix; matrix4 invProjMatrix; - vec3d ambientLight; - float pad0; - vec2d viewportSize; float pad1[2]; }; diff --git a/code/lighting/lighting.h b/code/lighting/lighting.h index 56e7e4ce714..0fcbc673213 100644 --- a/code/lighting/lighting.h +++ b/code/lighting/lighting.h @@ -32,7 +32,8 @@ enum class Light_Type : int { Directional = 0,// A light like a sun Point = 1, // A point light, like an explosion Tube = 2, // A tube light, like a fluorescent light - Cone = 3 // A cone light, like a flood light + Cone = 3, // A cone light, like a flood light + Ambient = 4 // A directionless and positionless ambient light }; typedef struct light { diff --git a/code/math/vecmat.cpp b/code/math/vecmat.cpp index 526baf08693..46261d851bc 100644 --- a/code/math/vecmat.cpp +++ b/code/math/vecmat.cpp @@ -276,6 +276,19 @@ vec3d *vm_vec_avg_n(vec3d *dest, int n, const vec3d src[]) return dest; } +//Calculates the componentwise minimum of the two vectors +void vm_vec_min(vec3d* dest, const vec3d* src0, const vec3d* src1) { + dest->xyz.x = std::min(src0->xyz.x, src1->xyz.x); + dest->xyz.y = std::min(src0->xyz.y, src1->xyz.y); + dest->xyz.z = std::min(src0->xyz.z, src1->xyz.z); +} + +//Calculates the componentwise maximum of the two vectors +void vm_vec_max(vec3d* dest, const vec3d* src0, const vec3d* src1) { + dest->xyz.x = std::max(src0->xyz.x, src1->xyz.x); + dest->xyz.y = std::max(src0->xyz.y, src1->xyz.y); + dest->xyz.z = std::max(src0->xyz.z, src1->xyz.z); +} //averages two vectors. returns ptr to dest //dest can equal either source diff --git a/code/math/vecmat.h b/code/math/vecmat.h index 2a6c9cffe34..a0a7f619a6e 100755 --- a/code/math/vecmat.h +++ b/code/math/vecmat.h @@ -145,7 +145,11 @@ void vm_vec_sub2(vec3d *dest, const vec3d *src); //averages n vectors vec3d *vm_vec_avg_n(vec3d *dest, int n, const vec3d src[]); +//Calculates the componentwise minimum of the two vectors +void vm_vec_min(vec3d* dest, const vec3d* src0, const vec3d* src1); +//Calculates the componentwise maximum of the two vectors +void vm_vec_max(vec3d* dest, const vec3d* src0, const vec3d* src1); //averages two vectors. returns ptr to dest //dest can equal either source vec3d *vm_vec_avg(vec3d *dest, const vec3d *src0, const vec3d *src1); diff --git a/code/model/model.h b/code/model/model.h index 65a6c092bfe..9ca939a602c 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -724,13 +724,9 @@ typedef struct cross_section { #define MAX_INS_FACES 128 typedef struct insignia { int detail_level; - int num_faces; - int faces[MAX_INS_FACES][MAX_INS_FACE_VECS]; // indices into the vecs array - float u[MAX_INS_FACES][MAX_INS_FACE_VECS]; // u tex coords on a per-face-per-vertex basis - float v[MAX_INS_FACES][MAX_INS_FACE_VECS]; // v tex coords on a per-face-per-vertex bases - vec3d vecs[MAX_INS_VECS]; // vertex list - vec3d offset; // global position offset for this insignia - vec3d norm[MAX_INS_VECS] ; //normal of the insignia-Bobboau + vec3d position; + matrix orientation; + float diameter; } insignia; #define PM_FLAG_ALLOW_TILING (1<<0) // Allow texture tiling @@ -806,7 +802,7 @@ class polymodel n_view_positions(0), rad(0.0f), core_radius(0.0f), n_textures(0), submodel(NULL), n_guns(0), n_missiles(0), n_docks(0), n_thrusters(0), gun_banks(NULL), missile_banks(NULL), docking_bays(NULL), thrusters(NULL), ship_bay(NULL), shield(), shield_collision_tree(NULL), sldc_size(0), n_paths(0), paths(NULL), mass(0), num_xc(0), xc(NULL), num_split_plane(0), - num_ins(0), used_this_mission(0), n_glow_point_banks(0), glow_point_banks(nullptr), + used_this_mission(0), n_glow_point_banks(0), glow_point_banks(nullptr), vert_source() { filename[0] = 0; @@ -819,7 +815,6 @@ class polymodel memset(&bounding_box, 0, 8 * sizeof(vec3d)); memset(&view_positions, 0, MAX_EYES * sizeof(eye)); memset(&split_plane, 0, MAX_SPLIT_PLANE * sizeof(float)); - memset(&ins, 0, MAX_MODEL_INSIGNIAS * sizeof(insignia)); #ifndef NDEBUG ram_used = 0; @@ -894,8 +889,7 @@ class polymodel int num_split_plane; // number of split planes float split_plane[MAX_SPLIT_PLANE]; // actual split plane z coords (for big ship explosions) - insignia ins[MAX_MODEL_INSIGNIAS]; - int num_ins; + SCP_vector ins; #ifndef NDEBUG int ram_used; // How much RAM this model uses diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index bc26c7c40e1..66c24c479c5 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -1657,8 +1657,8 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename, memset( &pm->view_positions, 0, sizeof(pm->view_positions) ); - // reset insignia counts - pm->num_ins = 0; + // reset insignia + pm->ins.clear(); // reset glow points!! - Goober5000 pm->n_glow_point_banks = 0; @@ -2806,62 +2806,81 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename, } break; - case ID_INSG: - int num_ins, num_verts, num_faces, idx, idx2, idx3; - + case ID_INSG: { // get the # of insignias - num_ins = cfread_int(fp); - pm->num_ins = num_ins; - + int num_ins = cfread_int(fp); + pm->ins = SCP_vector(num_ins); + // read in the insignias - for(idx=0; idxins[idx]; + // get the detail level - pm->ins[idx].detail_level = cfread_int(fp); - if (pm->ins[idx].detail_level < 0) { - Warning(LOCATION, "Model '%s': insignia uses an invalid LOD (%i)\n", pm->filename, pm->ins[idx].detail_level); + ins.detail_level = cfread_int(fp); + if (ins.detail_level < 0) { + Warning(LOCATION, "Model '%s': insignia uses an invalid LOD (%i)\n", pm->filename, ins.detail_level); } // # of faces - num_faces = cfread_int(fp); - pm->ins[idx].num_faces = num_faces; - Assert(num_faces <= MAX_INS_FACES); + int num_faces = cfread_int(fp); // # of vertices - num_verts = cfread_int(fp); - Assert(num_verts <= MAX_INS_VECS); + int num_verts = cfread_int(fp); + SCP_vector vertices(num_verts); // read in all the vertices - for(idx2=0; idx2ins[idx].vecs[idx2], fp); + for(int idx2 = 0; idx2 < num_verts; idx2++){ + cfread_vector(&vertices[idx2], fp); } + vec3d offset; // read in world offset - cfread_vector(&pm->ins[idx].offset, fp); + cfread_vector(&offset, fp); + + vec3d min {{{FLT_MAX, FLT_MAX, FLT_MAX}}}; + vec3d max {{{-FLT_MAX, -FLT_MAX, -FLT_MAX}}}; + vec3d avg_total = ZERO_VECTOR; + vec3d avg_normal = ZERO_VECTOR; // read in all the faces - for(idx2=0; idx2ins[idx].num_faces; idx2++){ + for(int idx2 = 0; idx2 < num_faces; idx2++){ + std::array faces; // read in 3 vertices - for(idx3=0; idx3<3; idx3++){ - pm->ins[idx].faces[idx2][idx3] = cfread_int(fp); - pm->ins[idx].u[idx2][idx3] = cfread_float(fp); - pm->ins[idx].v[idx2][idx3] = cfread_float(fp); - } - vec3d tempv; - - //get three points (rotated) and compute normal + for(int idx3 = 0; idx3 < 3; idx3++){ + faces[idx3] = cfread_int(fp); - vm_vec_perp(&tempv, - &pm->ins[idx].vecs[pm->ins[idx].faces[idx2][0]], - &pm->ins[idx].vecs[pm->ins[idx].faces[idx2][1]], - &pm->ins[idx].vecs[pm->ins[idx].faces[idx2][2]]); + //UV coords are no longer needed + cfread_float(fp); + cfread_float(fp); + } - vm_vec_normalize_safe(&tempv); + const vec3d& v1 = vertices[faces[0]]; + const vec3d& v2 = vertices[faces[1]]; + const vec3d& v3 = vertices[faces[2]]; - pm->ins[idx].norm[idx2] = tempv; + vec3d normal; + //get three points (rotated) and compute normal + vm_vec_perp(&normal, &v1, &v2, &v3); + + vm_vec_min(&min, &min, &v1); + vm_vec_min(&min, &min, &v2); + vm_vec_min(&min, &min, &v3); + vm_vec_max(&max, &max, &v1); + vm_vec_max(&max, &max, &v2); + vm_vec_max(&max, &max, &v3); + + vec3d avg = (v1 + v2 + v3) * (1.0f / 3.0f); + avg_total += avg; + avg_normal += normal; // mprintf(("insignorm %.2f %.2f %.2f\n",pm->ins[idx].norm[idx2].xyz.x, pm->ins[idx].norm[idx2].xyz.y, pm->ins[idx].norm[idx2].xyz.z)); - } - } + + ins.position = avg_total / static_cast(num_faces) + offset; + vec3d bb = max - min; + ins.diameter = std::max({bb.xyz.x, bb.xyz.y, bb.xyz.z}); + vm_vector_2_matrix(&ins.orientation, &avg_normal, &vmd_z_vector); + } + } break; // autocentering info diff --git a/code/model/modelrender.cpp b/code/model/modelrender.cpp index f72cb518e66..545db0023ce 100644 --- a/code/model/modelrender.cpp +++ b/code/model/modelrender.cpp @@ -639,51 +639,6 @@ void model_draw_list::render_arcs() gr_zbuffer_set(mode); } -void model_draw_list::add_insignia(const model_render_params *params, const polymodel *pm, int detail_level, int bitmap_num) -{ - insignia_draw_data new_insignia; - - new_insignia.transform = Transformations.get_transform(); - new_insignia.pm = pm; - new_insignia.detail_level = detail_level; - new_insignia.bitmap_num = bitmap_num; - - new_insignia.clip = params->is_clip_plane_set(); - new_insignia.clip_normal = params->get_clip_plane_normal(); - new_insignia.clip_position = params->get_clip_plane_pos(); - - Insignias.push_back(new_insignia); -} - -void model_draw_list::render_insignia(const insignia_draw_data &insignia_info) -{ - if ( insignia_info.clip ) { - vec3d tmp; - vec3d pos; - - vm_matrix4_get_offset(&pos, &insignia_info.transform); - vm_vec_sub(&tmp, &pos, &insignia_info.clip_position); - vm_vec_normalize(&tmp); - - if ( vm_vec_dot(&tmp, &insignia_info.clip_normal) < 0.0f) { - return; - } - } - - g3_start_instance_matrix(&insignia_info.transform); - - model_render_insignias(&insignia_info); - - g3_done_instance(true); -} - -void model_draw_list::render_insignias() -{ - for ( size_t i = 0; i < Insignias.size(); ++i ) { - render_insignia(Insignias[i]); - } -} - void model_draw_list::add_outline(const vertex* vert_array, int n_verts, const color *clr) { outline_draw draw_info; @@ -2425,74 +2380,6 @@ void model_queue_render_thrusters(const model_render_params *interp, const polym } } -void model_render_insignias(const insignia_draw_data *insignia_data) -{ - auto pm = insignia_data->pm; - int detail_level = insignia_data->detail_level; - int bitmap_num = insignia_data->bitmap_num; - - // if the model has no insignias, or we don't have a texture, then bail - if ( (pm->num_ins <= 0) || (bitmap_num < 0) ) - return; - - int idx, s_idx; - vertex vecs[3]; - vec3d t1, t2, t3; - int i1, i2, i3; - - material insignia_material; - insignia_material.set_depth_bias(1); - - // set the proper texture - material_set_unlit(&insignia_material, bitmap_num, 0.65f, true, true); - - if ( insignia_data->clip ) { - insignia_material.set_clip_plane(insignia_data->clip_normal, insignia_data->clip_position); - } - - // otherwise render them - for(idx=0; idxnum_ins; idx++){ - // skip insignias not on our detail level - if(pm->ins[idx].detail_level != detail_level){ - continue; - } - - for(s_idx=0; s_idxins[idx].num_faces; s_idx++){ - // get vertex indices - i1 = pm->ins[idx].faces[s_idx][0]; - i2 = pm->ins[idx].faces[s_idx][1]; - i3 = pm->ins[idx].faces[s_idx][2]; - - // transform vecs and setup vertices - vm_vec_add(&t1, &pm->ins[idx].vecs[i1], &pm->ins[idx].offset); - vm_vec_add(&t2, &pm->ins[idx].vecs[i2], &pm->ins[idx].offset); - vm_vec_add(&t3, &pm->ins[idx].vecs[i3], &pm->ins[idx].offset); - - g3_transfer_vertex(&vecs[0], &t1); - g3_transfer_vertex(&vecs[1], &t2); - g3_transfer_vertex(&vecs[2], &t3); - - // setup texture coords - vecs[0].texture_position.u = pm->ins[idx].u[s_idx][0]; - vecs[0].texture_position.v = pm->ins[idx].v[s_idx][0]; - - vecs[1].texture_position.u = pm->ins[idx].u[s_idx][1]; - vecs[1].texture_position.v = pm->ins[idx].v[s_idx][1]; - - vecs[2].texture_position.u = pm->ins[idx].u[s_idx][2]; - vecs[2].texture_position.v = pm->ins[idx].v[s_idx][2]; - - light_apply_rgb( &vecs[0].r, &vecs[0].g, &vecs[0].b, &pm->ins[idx].vecs[i1], &pm->ins[idx].norm[i1], 1.5f ); - light_apply_rgb( &vecs[1].r, &vecs[1].g, &vecs[1].b, &pm->ins[idx].vecs[i2], &pm->ins[idx].norm[i2], 1.5f ); - light_apply_rgb( &vecs[2].r, &vecs[2].g, &vecs[2].b, &pm->ins[idx].vecs[i3], &pm->ins[idx].norm[i3], 1.5f ); - vecs[0].a = vecs[1].a = vecs[2].a = 255; - - // draw the polygon - g3_render_primitives_colored_textured(&insignia_material, vecs, 3, PRIM_TYPE_TRIFAN, false); - } - } -} - SCP_vector Arc_segment_points; void model_render_arc(const vec3d *v1, const vec3d *v2, const SCP_vector *persistent_arc_points, const color *primary, const color *secondary, float arc_width, ubyte depth_limit) @@ -2652,7 +2539,6 @@ void model_render_immediate(const model_render_params* render_info, int model_nu } model_list.render_outlines(); - model_list.render_insignias(); model_list.render_arcs(); gr_zbias(0); @@ -2970,8 +2856,29 @@ void model_render_queue(const model_render_params* interp, model_draw_list* scen } // MARKED! - if ( !( model_flags & MR_NO_TEXTURING ) && !( model_flags & MR_NO_INSIGNIA) ) { - scene->add_insignia(interp, pm, detail_level, interp->get_insignia_bitmap()); + if ( !( model_flags & MR_NO_TEXTURING ) && !( model_flags & MR_NO_INSIGNIA) && objnum >= 0 ) { + int bitmap_num = interp->get_insignia_bitmap(); + if ( (!pm->ins.empty()) && (bitmap_num >= 0) ) { + + for (const auto& ins : pm->ins) { + // skip insignias not on our detail level + if (ins.detail_level != detail_level) { + continue; + } + + decals::Decal decal; + decal.object = &Objects[objnum]; + decal.position = ins.position; + decal.submodel = -1; + decal.scale = vec3d{{{ins.diameter, ins.diameter, ins.diameter}}}; + decal.orig_obj_type = OBJ_SHIP; + decal.creation_time = f2fl(Missiontime); + decal.lifetime = 1.0f; + decal.orientation = ins.orientation; + decal.definition_handle = std::make_tuple(bitmap_num, -1, -1); + decals::addSingleFrameDecal(std::move(decal)); + } + } } if ( (model_flags & MR_AUTOCENTER) && (set_autocen) ) { diff --git a/code/model/modelrender.h b/code/model/modelrender.h index 64f041a15ef..1b120fe1d19 100644 --- a/code/model/modelrender.h +++ b/code/model/modelrender.h @@ -254,7 +254,6 @@ class model_draw_list SCP_vector Render_keys; SCP_vector Arcs; - SCP_vector Insignias; SCP_vector Outlines; graphics::util::UniformBuffer _dataBuffer; @@ -287,9 +286,6 @@ class model_draw_list void add_arc(const vec3d *v1, const vec3d *v2, const SCP_vector *persistent_arc_points, const color *primary, const color *secondary, float arc_width, ubyte segment_depth); void render_arcs(); - void add_insignia(const model_render_params *params, const polymodel *pm, int detail_level, int bitmap_num); - void render_insignias(); - void add_outline(const vertex* vert_array, int n_verts, const color *clr); void render_outlines(); @@ -312,7 +308,6 @@ void submodel_render_queue(const model_render_params* render_info, model_draw_li void model_render_buffers(model_draw_list* scene, model_material* rendering_material, const model_render_params* interp, const vertex_buffer* buffer, const polymodel* pm, int mn, int detail_level, uint tmap_flags); bool model_render_check_detail_box(const vec3d* view_pos, const polymodel* pm, int submodel_num, uint64_t flags); void model_render_arc(const vec3d* v1, const vec3d* v2, const SCP_vector *persistent_arc_points, const color* primary, const color* secondary, float arc_width, ubyte depth_limit); -void model_render_insignias(const insignia_draw_data* insignia); void model_render_set_wireframe_color(const color* clr); bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int y2, float zoom, bool lighting, int class_idx, const matrix* orient, const SCP_string& pof_filename = "", float closeup_zoom = 0, const vec3d* closeup_pos = &vmd_zero_vector, const SCP_string& tcolor = ""); diff --git a/code/object/objectsort.cpp b/code/object/objectsort.cpp index 0107c0de30e..f438936d6b6 100644 --- a/code/object/objectsort.cpp +++ b/code/object/objectsort.cpp @@ -388,7 +388,6 @@ void obj_render_queue_all() // render electricity effects and insignias scene.render_outlines(); - scene.render_insignias(); scene.render_arcs(); gr_zbuffer_set(ZBUFFER_TYPE_READ); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 901d31f3237..addb3f0355e 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -8185,8 +8185,7 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix const bool renderShipModel = ( sip->flags[Ship::Info_Flags::Show_ship_model]) && (!Show_ship_only_if_cockpits_enabled || Cockpit_active) - && (!Viewer_mode || (Viewer_mode & VM_PADLOCK_ANY) || (Viewer_mode & VM_OTHER_SHIP) || (Viewer_mode & VM_TRACK) - || !(Viewer_mode & VM_EXTERNAL)); + && (!Viewer_mode || (Viewer_mode & VM_PADLOCK_ANY) || (Viewer_mode & VM_OTHER_SHIP) || (Viewer_mode & VM_TRACK)); Cockpit_active = renderCockpitModel; //Nothing to do @@ -8320,6 +8319,7 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix uint64_t render_flags = MR_NORMAL; render_flags |= MR_NO_FOGGING; + render_flags |= MR_NO_INSIGNIA; if (shipp->flags[Ship::Ship_Flags::Glowmaps_disabled]) { render_flags |= MR_NO_GLOWMAPS; From 8be619db7e3c20b77ba8d210f0564a22cd4113c6 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 22 Jul 2025 19:19:20 -0500 Subject: [PATCH 265/466] prevent string ops on nullptr char* --- code/scripting/api/libs/base.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/code/scripting/api/libs/base.cpp b/code/scripting/api/libs/base.cpp index 6f50522e628..a29916d6cd8 100644 --- a/code/scripting/api/libs/base.cpp +++ b/code/scripting/api/libs/base.cpp @@ -667,9 +667,14 @@ ADE_FUNC(getVersionString, l_Base, nullptr, } ADE_FUNC(getModRootName, l_Base, nullptr, - "Returns the name of the current mod's root folder.", "string", "The mod root") + "Returns the name of the current mod's root folder.", "string", "The mod root or empty string if the mod runs without a -mod line") { - SCP_string str = Cmdline_mod; + const char* mod = Cmdline_mod; + if (mod == nullptr) { + mod = ""; + } + + SCP_string str = mod; // Trim any trailing folders so we get just the name of the root mod folder str = str.substr(0, str.find_first_of(DIR_SEPARATOR_CHAR)); From 365d9c57e381a8c427d937c8be523eed551f70bf Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Wed, 23 Jul 2025 05:07:42 -0700 Subject: [PATCH 266/466] gate hull and subsys impacts (#6855) --- code/ship/shiphit.cpp | 24 ++++++++++++++---------- code/ship/shiphit.h | 2 +- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 53adcc38b0c..16606dc7d99 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -662,7 +662,7 @@ void do_subobj_heal_stuff(const object* ship_objp, const object* other_obj, cons // //WMC - hull_should_apply armor means that the initial subsystem had no armor, so the hull should apply armor instead. -std::pair, float> do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3d *hitpos, int submodel_num, float damage, bool *hull_should_apply_armor, float hit_dot) +std::pair, float> do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3d *hitpos, int submodel_num, float damage, bool *hull_should_apply_armor, float hit_dot, bool shield_hit) { vec3d g_subobj_pos; float damage_left, damage_if_hull; @@ -1003,7 +1003,7 @@ std::pair, float> do_subobj_hit_stuff(object *ship_ damage_to_apply *= ss_dif_scale; } - if (j == 0) { + if (j == 0 && !shield_hit) { subsys_impact = ConditionData { ImpactCondition(subsystem->armor_type_idx), HitType::SUBSYS, @@ -2503,8 +2503,10 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi if ( (damage > 0.0f) ) { bool apply_hull_armor = true; + bool shield_hit = quadrant >= 0; + // apply damage to subsystems, and get back any remaining damage that needs to go to the hull - auto damage_pair = do_subobj_hit_stuff(ship_objp, other_obj, hitpos, submodel_num, damage, &apply_hull_armor, hit_dot); + auto damage_pair = do_subobj_hit_stuff(ship_objp, other_obj, hitpos, submodel_num, damage, &apply_hull_armor, hit_dot, shield_hit); damage = damage_pair.second; @@ -2563,13 +2565,15 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi } } - impact_data[static_cast>(HitType::HULL)] = ConditionData { - ImpactCondition(shipp->armor_type_idx), - HitType::HULL, - damage, - ship_objp->hull_strength, - shipp->ship_max_hull_strength, - }; + if (!shield_hit) { + impact_data[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(shipp->armor_type_idx), + HitType::HULL, + damage, + ship_objp->hull_strength, + shipp->ship_max_hull_strength, + }; + } // multiplayer clients don't do damage if (((Game_mode & GM_MULTIPLAYER) && MULTIPLAYER_CLIENT)) { diff --git a/code/ship/shiphit.h b/code/ship/shiphit.h index fed27dc1ebd..ba7b2f6292a 100644 --- a/code/ship/shiphit.h +++ b/code/ship/shiphit.h @@ -35,7 +35,7 @@ constexpr float DEATHROLL_ROTVEL_CAP = 6.3f; // maximum added deathroll rotve // function to destroy a subsystem. Called internally and from multiplayer messaging code extern void do_subobj_destroyed_stuff( ship *ship_p, ship_subsys *subsys, const vec3d *hitpos, bool no_explosion = false ); -std::pair, float> do_subobj_hit_stuff(object *ship_obj, const object *other_obj, const vec3d *hitpos, int submodel_num, float damage, bool *hull_should_apply_armor, float hit_dot = 1.f); +std::pair, float> do_subobj_hit_stuff(object *ship_obj, const object *other_obj, const vec3d *hitpos, int submodel_num, float damage, bool *hull_should_apply_armor, float hit_dot = 1.f, bool shield_hit = false); // Goober5000 // (it might be possible to make `target` const, but that would set off another const-cascade) From 168b3310abf9accf13ddead83e4e59d4dee78ed8 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Thu, 24 Jul 2025 08:39:58 -0400 Subject: [PATCH 267/466] Cleanup of Shield Hitpoint Threshold (#6851) * Cleanup of Shield Hitpoint Threshold Overall cleanup that does a few things: 1) Changes `ship_is_shield_up` from integer to bool. 2) `ship_is_shield_up` was only ever called to look at one quadrant, never for all quadrants, so took the opportunity to update the 'all-quadrant' block to account for ships with a non-standard number of shield quadrants. 3) ` MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp)` was used many times throughout the code (and will be used at least 2 more times with #6848), so simply consolidated all of those uses into a new function called `ship_shield_hitpoint_threshold`. Happy to edit the name however. 4) Removed un-needed redundant comments from `ship_is_shield_up` definition in `ship.h` since those comments were already present in the actual function within `shield.cpp`, and all the other functions used comments in that file instead. Again happy to tune or edit with any other choices. * use proper default arg style * actually commit the updated files * appease modern clang * clang round 2 --- code/ship/shield.cpp | 30 ++++++++++++++++++++---------- code/ship/ship.h | 7 ++----- code/ship/shiphit.cpp | 12 ++++++------ 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/code/ship/shield.cpp b/code/ship/shield.cpp index ad987d29630..7006b720e47 100644 --- a/code/ship/shield.cpp +++ b/code/ship/shield.cpp @@ -903,27 +903,37 @@ void create_shield_explosion_all(object *objp) } } +/** + * Returns the lowest threshold of shield hitpoints that triggers a shield hit + * + * @return If all_quadrants is true, looks at entire shield, otherwise just one quadrant + */ +float ship_shield_hitpoint_threshold(const object* obj, bool all_quadrants) +{ + if (all_quadrants) { + // All quadrants + auto num_quads = static_cast(obj->shield_quadrant.size()); + return MAX(2.0f * num_quads, Shield_percent_skips_damage * shield_get_max_strength(obj)); + } else { + // Just one quadrant + return MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(obj)); + } +} + /** * Returns true if the shield presents any opposition to something trying to force through it. * * @return If quadrant is -1, looks at entire shield, otherwise just one quadrant */ -int ship_is_shield_up( const object *obj, int quadrant ) +bool ship_is_shield_up(const object *obj, int quadrant) { if ( (quadrant >= 0) && (quadrant < static_cast(obj->shield_quadrant.size()))) { // Just check one quadrant - if (shield_get_quad(obj, quadrant) > MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(obj))) { - return 1; - } + return ( shield_get_quad(obj, quadrant) > ship_shield_hitpoint_threshold(obj, false) ); } else { // Check all quadrants - float strength = shield_get_strength(obj); - - if ( strength > MAX(2.0f*4.0f, Shield_percent_skips_damage * shield_get_max_strength(obj)) ) { - return 1; - } + return ( shield_get_strength(obj) > ship_shield_hitpoint_threshold(obj, true) ); } - return 0; // no shield strength } // return quadrant containing hit_pnt. diff --git a/code/ship/ship.h b/code/ship/ship.h index 9d324ebc4a1..ee101922973 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1771,11 +1771,8 @@ extern void add_shield_point_multi(int objnum, int tri_num, vec3d *hit_pos); extern void shield_point_multi_setup(); extern void shield_hit_close(); -// Returns true if the shield presents any opposition to something -// trying to force through it. -// If quadrant is -1, looks at entire shield, otherwise -// just one quadrant -int ship_is_shield_up( const object *obj, int quadrant ); +float ship_shield_hitpoint_threshold(const object* obj, bool all_quadrants = false); +bool ship_is_shield_up(const object *obj, int quadrant); //================================================= void ship_model_replicate_submodels(object *objp); diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 16606dc7d99..505de505073 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -2425,8 +2425,8 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi HitType::SHIELD, 0.0f, // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last X percent of a shield doesn't matter - MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp))), - shield_get_max_quad(ship_objp) - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp)), + MAX(0.0f, ship_objp->shield_quadrant[quadrant] - ship_shield_hitpoint_threshold(ship_objp)), + shield_get_max_quad(ship_objp) - ship_shield_hitpoint_threshold(ship_objp), }; } else { impact_data[static_cast>(HitType::HULL)] = ConditionData { @@ -2458,8 +2458,8 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi HitType::SHIELD, 0.0f, // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last X percent of a shield doesn't matter - MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp))), - shield_get_max_quad(ship_objp) - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp)), + MAX(0.0f, ship_objp->shield_quadrant[quadrant] - ship_shield_hitpoint_threshold(ship_objp)), + shield_get_max_quad(ship_objp) - ship_shield_hitpoint_threshold(ship_objp), }; if ( damage > 0.0f ) { @@ -2882,8 +2882,8 @@ void ship_apply_local_damage(object *ship_objp, object *other_obj, const vec3d * HitType::SHIELD, 0.0f, // we have to do this annoying thing where we reduce the shield health a bit because it turns out the last X percent of a shield doesn't matter - MAX(0.0f, ship_objp->shield_quadrant[quadrant] - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp))), - shield_get_max_quad(ship_objp) - MAX(2.0f, Shield_percent_skips_damage * shield_get_max_quad(ship_objp)), + MAX(0.0f, ship_objp->shield_quadrant[quadrant] - ship_shield_hitpoint_threshold(ship_objp)), + shield_get_max_quad(ship_objp) - ship_shield_hitpoint_threshold(ship_objp), }; } else { impact_data[static_cast>(HitType::HULL)] = ConditionData { From 755c4c4e29b41a106f6f336b890fd5218ac58e25 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:33:37 +0200 Subject: [PATCH 268/466] Fix Volumetric Nebula Bugs and Add Smoothing (#6854) * Fix 3d tex generation * Fix blending and emissive * Add Smoothing * Add smoothing parameter * Correctly calculate fractional smoothing --- code/def_files/data/effects/volumetric-f.sdr | 10 +++---- code/nebula/volumetrics.cpp | 28 +++++++++++++++----- code/nebula/volumetrics.h | 4 +++ fred2/missionsave.cpp | 2 ++ qtfred/src/mission/missionsave.cpp | 2 ++ 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/code/def_files/data/effects/volumetric-f.sdr b/code/def_files/data/effects/volumetric-f.sdr index 010707e6a67..d511fea68a9 100644 --- a/code/def_files/data/effects/volumetric-f.sdr +++ b/code/def_files/data/effects/volumetric-f.sdr @@ -100,8 +100,6 @@ void main() vec3 sampleposition = position / nebSize + 0.5; vec4 volume_sample = textureGrad(volume_tex, sampleposition, gradX, gradY); - float stepsize_current = min(max(stepsize, volume_sample.x * udfScale), mintMax - stept); - #ifdef DO_EDGE_SMOOTHING //Average 3D texel with texels on corner, in an attempt to reduce jagged edges. float stepcolor_alpha = volume_sample.a; @@ -121,6 +119,8 @@ void main() float stepcolor_alpha = volume_sample.a; #endif + float stepsize_current = min(max(stepsize, step(stepcolor_alpha, 0.01) * volume_sample.x * udfScale), mintMax - stept); + float stepalpha = -(pow(alphalim, 1.0 / (opacitydistance / stepsize_current)) - 1.0f) * stepcolor_alpha; //All the following computations are just required if we have a stepcoloralpha that is non-zero. if(stepcolor_alpha > 0.01) @@ -151,7 +151,7 @@ void main() //Emissive cumnebdist += stepcolor_alpha * stepsize_current; vec3 emissive_lod = textureLod(emissive, fragTexCoord.xy, clamp(cumnebdist * emissiveSpreadFactor, 0, float(textureQueryLevels(emissive) - 1))).rgb; - vec3 stepcolor_emissive = emissive_lod.rgb * pow(alphalim, 1.0 / (opacitydistance / ((depth - stept) * emissiveFalloff + 0.01))) * emissiveIntensity; + vec3 stepcolor_emissive = clamp(emissive_lod.rgb * pow(alphalim, 1.0 / (opacitydistance / ((depth - stept) * emissiveFalloff + 0.01))) * emissiveIntensity, 0, 1); //Step finish vec3 stepcolor = clamp(stepcolor_diffuse + stepcolor_emissive, 0, 1); @@ -165,7 +165,5 @@ void main() break; } - cumcolor += cumOMAlpha * color_in.rgb; - - fragOut0 = vec4(cumcolor, 1); + fragOut0 = vec4(cumOMAlpha * color_in.rgb + ((1.0 - cumOMAlpha) * cumcolor), 1); } diff --git a/code/nebula/volumetrics.cpp b/code/nebula/volumetrics.cpp index 60b6e6cd8aa..0d216e3d507 100644 --- a/code/nebula/volumetrics.cpp +++ b/code/nebula/volumetrics.cpp @@ -55,6 +55,10 @@ volumetric_nebula& volumetric_nebula::parse_volumetric_nebula() { stuff_int(&oversampling); } + if (optional_string("+Smoothing:")) { + stuff_float(&smoothing); + } + //Lighting settings if (optional_string("+Heyney Greenstein Coefficient:")) { stuff_float(&henyeyGreensteinCoeff); @@ -155,6 +159,10 @@ const std::tuple& volumetric_nebula::getNebulaColor() const return nebulaColor; } +int volumetric_nebula::getVolumeBitmapSmoothingSteps() const { + return std::max(1, static_cast(static_cast(1 << (resolution + oversampling - 1)) * std::min(smoothing, 0.5f))); +} + bool volumetric_nebula::getEdgeSmoothing() const { return Detail.nebula_detail == MAX_DETAIL_VALUE || doEdgeSmoothing; //Only for highest setting, or when the lab has an override. } @@ -208,7 +216,7 @@ float volumetric_nebula::getGlobalLightDistanceFactor() const { } float volumetric_nebula::getGlobalLightStepsize() const { - return getOpacityDistance() / static_cast(getGlobalLightSteps()) * getGlobalLightDistanceFactor(); + return getOpacityDistance() * static_cast(getVolumeBitmapSmoothingSteps()) / static_cast(getGlobalLightSteps()) * getGlobalLightDistanceFactor(); } bool volumetric_nebula::getNoiseActive() const { @@ -354,16 +362,22 @@ void volumetric_nebula::renderVolumeBitmap() { //Sample the nebula values from the binary cubegrid. volumeBitmapData = make_unique(n * n * n * 4); - int oversamplingCount = (1 << (oversampling - 1)) + 1; - float oversamplingDivisor = 255.1f / static_cast(oversamplingCount); + int oversamplingCount = (1 << (oversampling - 1)); + + int smoothing_steps = getVolumeBitmapSmoothingSteps(); + float oversamplingDivisor = 255.1f / (static_cast(oversamplingCount + smoothing_steps) * static_cast(oversamplingCount + smoothing_steps) * static_cast(oversamplingCount + smoothing_steps)); + int smoothStart = smoothing_steps / 2; + int smoothStop = (smoothing_steps / 2 + (1 & smoothing_steps)); + for (int x = 0; x < n; x++) { for (int y = 0; y < n; y++) { for (int z = 0; z < n; z++) { int sum = 0; - for (int sx = x * oversampling; sx <= (x + 1) * oversampling; sx++) { - for (int sy = y * oversampling; sy <= (y + 1) * oversampling; sy++) { - for (int sz = z * oversampling; sz <= (z + 1) * oversampling; sz++) { - if (volumeSampleCache[sx * nSample * nSample + sy * nSample + sz]) + for (int sx = x * oversamplingCount - smoothStart; sx < (x + 1) * oversamplingCount + smoothStop; sx++) { + for (int sy = y * oversamplingCount - smoothStart; sy < (y + 1) * oversamplingCount + smoothStop; sy++) { + for (int sz = z * oversamplingCount - smoothStart; sz < (z + 1) * oversamplingCount + smoothStop; sz++) { + if (sx >= 0 && sx < nSample && sy >= 0 && sy < nSample && sz >= 0 && sz < nSample && + volumeSampleCache[sx * nSample * nSample + sy * nSample + sz]) sum++; } } diff --git a/code/nebula/volumetrics.h b/code/nebula/volumetrics.h index deeaeddbdb4..d80900935bd 100644 --- a/code/nebula/volumetrics.h +++ b/code/nebula/volumetrics.h @@ -34,6 +34,8 @@ class volumetric_nebula { int resolution = 6; //Oversampling of 3D-Texture. Will quadruple loading computation time for each increment, but improves banding especially at lower resolutions. 1 - 3. Mostly Loading time cost. int oversampling = 2; + //How much the edge of the POF should be smoothed to be less hard + float smoothing = 0.f; //Resolution of Noise 3D-Texture as 2^n. 5 - 8 recommended. Mostly VRAM cost int noiseResolution = 5; @@ -95,6 +97,8 @@ class volumetric_nebula { const vec3d& getSize() const; const std::tuple& getNebulaColor() const; + int getVolumeBitmapSmoothingSteps() const; + bool getEdgeSmoothing() const; int getSteps() const; int getGlobalLightSteps() const; diff --git a/fred2/missionsave.cpp b/fred2/missionsave.cpp index 39316b27b41..242d2e04591 100644 --- a/fred2/missionsave.cpp +++ b/fred2/missionsave.cpp @@ -2794,6 +2794,7 @@ int CFred_mission_save::save_mission_info() FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Steps:", 1, ";;FSO 23.1.0;;", 15, " %d", The_mission.volumetrics->steps); FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Resolution:", 1, ";;FSO 23.1.0;;", 6, " %d", The_mission.volumetrics->resolution); FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Oversampling:", 1, ";;FSO 23.1.0;;", 2, " %d", The_mission.volumetrics->oversampling); + FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Smoothing:", 1, ";;FSO 25.0.0;;", 0.f, " %f", The_mission.volumetrics->smoothing); FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+Heyney Greenstein Coefficient:", 1, ";;FSO 23.1.0;;", 0.2f, " %f", The_mission.volumetrics->henyeyGreensteinCoeff); FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+Sun Falloff Factor:", 1, ";;FSO 23.1.0;;", 1.0f, " %f", The_mission.volumetrics->globalLightDistanceFactor); FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Sun Steps:", 1, ";;FSO 23.1.0;;", 6, " %d", The_mission.volumetrics->globalLightSteps); @@ -2831,6 +2832,7 @@ int CFred_mission_save::save_mission_info() bypass_comment(";;FSO 23.1.0;; +Steps:"); bypass_comment(";;FSO 23.1.0;; +Resolution:"); bypass_comment(";;FSO 23.1.0;; +Oversampling:"); + bypass_comment(";;FSO 25.0.0;; +Smoothing:"); bypass_comment(";;FSO 23.1.0;; +Heyney Greenstein Coefficient:"); bypass_comment(";;FSO 23.1.0;; +Sun Falloff Factor:"); bypass_comment(";;FSO 23.1.0;; +Sun Steps:"); diff --git a/qtfred/src/mission/missionsave.cpp b/qtfred/src/mission/missionsave.cpp index 0b9c6fecc30..5df81680e8b 100644 --- a/qtfred/src/mission/missionsave.cpp +++ b/qtfred/src/mission/missionsave.cpp @@ -2684,6 +2684,7 @@ int CFred_mission_save::save_mission_info() FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Steps:", 1, ";;FSO 23.1.0;;", 15, " %d", The_mission.volumetrics->steps); FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Resolution:", 1, ";;FSO 23.1.0;;", 6, " %d", The_mission.volumetrics->resolution); FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Oversampling:", 1, ";;FSO 23.1.0;;", 2, " %d", The_mission.volumetrics->oversampling); + FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Smoothing:", 1, ";;FSO 25.0.0;;", 0.f, " %f", The_mission.volumetrics->smoothing); FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+Heyney Greenstein Coefficient:", 1, ";;FSO 23.1.0;;", 0.2f, " %f", The_mission.volumetrics->henyeyGreensteinCoeff); FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT_F("+Sun Falloff Factor:", 1, ";;FSO 23.1.0;;", 1.0f, " %f", The_mission.volumetrics->globalLightDistanceFactor); FRED_ENSURE_PROPERTY_VERSION_WITH_DEFAULT("+Sun Steps:", 1, ";;FSO 23.1.0;;", 6, " %d", The_mission.volumetrics->globalLightSteps); @@ -2721,6 +2722,7 @@ int CFred_mission_save::save_mission_info() bypass_comment(";;FSO 23.1.0;; +Steps:"); bypass_comment(";;FSO 23.1.0;; +Resolution:"); bypass_comment(";;FSO 23.1.0;; +Oversampling:"); + bypass_comment(";;FSO 25.0.0;; +Smoothing:"); bypass_comment(";;FSO 23.1.0;; +Heyney Greenstein Coefficient:"); bypass_comment(";;FSO 23.1.0;; +Sun Falloff Factor:"); bypass_comment(";;FSO 23.1.0;; +Sun Steps:"); From f76bf1b8c47623d19da697d04e353e2616bc0a37 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Thu, 24 Jul 2025 09:33:52 -0400 Subject: [PATCH 269/466] Fix 'show-ship` not working (#6860) Small one-line fix that follows up #6849. That PR removed the `!(Viewer_mode & VM_EXTERNAL))` check within a section of `ship_render_player_ship` to optimize insignia rendering, but turns out that check is needed to ensure `show-ship` properly works. This PR restores that check to fix this bug, and ideally the thoughts about slightly optimizing insignias can be considered later. Tested and works as expected. --- code/ship/ship.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index addb3f0355e..85e956799df 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -8185,7 +8185,7 @@ void ship_render_player_ship(object* objp, const vec3d* cam_offset, const matrix const bool renderShipModel = ( sip->flags[Ship::Info_Flags::Show_ship_model]) && (!Show_ship_only_if_cockpits_enabled || Cockpit_active) - && (!Viewer_mode || (Viewer_mode & VM_PADLOCK_ANY) || (Viewer_mode & VM_OTHER_SHIP) || (Viewer_mode & VM_TRACK)); + && (!Viewer_mode || (Viewer_mode & VM_PADLOCK_ANY) || (Viewer_mode & VM_OTHER_SHIP) || (Viewer_mode & VM_TRACK) || !(Viewer_mode & VM_EXTERNAL)); Cockpit_active = renderCockpitModel; //Nothing to do From 487cac645644b591b76fd2a1ad60b5d3030101d8 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 24 Jul 2025 15:51:14 -0500 Subject: [PATCH 270/466] Lua func to check if in game options are enabled (#6862) --- code/scripting/api/libs/options.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/code/scripting/api/libs/options.cpp b/code/scripting/api/libs/options.cpp index 3022be42eef..ed242d5ab62 100644 --- a/code/scripting/api/libs/options.cpp +++ b/code/scripting/api/libs/options.cpp @@ -7,6 +7,7 @@ #include "network/multiui.h" #include "scripting/api/objs/option.h" #include "scripting/lua/LuaTable.h" +#include "mod_table/mod_table.h" namespace scripting { namespace api { @@ -163,5 +164,10 @@ ADE_FUNC(verifyIPAddress, l_Options, "string", "Verifies if a string is a valid return ade_set_args(L, "b", psnet_is_valid_ip_string(ip)); } +ADE_FUNC(isInGameOptionsEnabled, l_Options, nullptr, "Returns whether or not in-game options flag is enabled.", "boolean", "True if enabled, false otherwise") +{ + return ade_set_args(L, "b", Using_in_game_options); +} + } // namespace api } // namespace scripting From 021eb8e49a1fe143bed3a42af8bfa1f52676dd7f Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 24 Jul 2025 15:51:45 -0500 Subject: [PATCH 271/466] Fix two HUD Config preset issues (#6830) * always use exact match * custom gauges should use the type for colors with old preset files --- code/hud/hudconfig.cpp | 33 +++++++++++++++++++++++++++++++-- code/hud/hudconfig.h | 5 ++--- code/pilotfile/plr_hudprefs.cpp | 2 +- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/code/hud/hudconfig.cpp b/code/hud/hudconfig.cpp index e1855e614d1..32fd548cf51 100644 --- a/code/hud/hudconfig.cpp +++ b/code/hud/hudconfig.cpp @@ -1709,6 +1709,8 @@ void hud_config_color_load(const char *name) HUD_config.set_gauge_color(gauge.first, clr); } + SCP_vector> gauge_color_list; + // Now read in the color values for the gauges int version = 1; if (optional_string("+VERSION 2")) { @@ -1732,12 +1734,12 @@ void hud_config_color_load(const char *name) case 1: { SCP_string gauge = gauge_map.get_string_id_from_hcf_id(str); if (!gauge.empty()) { - HUD_config.set_gauge_color(gauge, clr); + gauge_color_list.emplace_back(gauge, clr); } break; } case 2: { - HUD_config.set_gauge_color(str, clr); + gauge_color_list.emplace_back(str, clr); break; } default: { @@ -1745,6 +1747,33 @@ void hud_config_color_load(const char *name) } } } + + auto is_builtin = [](auto const& p) { + return p.first.rfind("Builtin::", 0) == 0; + }; + + // Move all builtin:: items to the front, keeping original order + std::stable_partition(gauge_color_list.begin(), gauge_color_list.end(), is_builtin); + + // Add the colors to the HUD_config + for (const auto& gauge_color : gauge_color_list) { + HUD_config.set_gauge_color(gauge_color.first, gauge_color.second); + + // If this is a builtin gauge, we also need to set the color for all other gauges of the same type + // Builtin gauges are handled first so that any defintions for custom gauges will override the builtin ones later + if (is_builtin(gauge_color)) { + int type = gauge_map.get_numeric_id_from_string_id(gauge_color.first); + + for (const auto& this_gauge : HC_gauge_map) { + if (this_gauge.first == gauge_color.first) { + continue; + } + if (this_gauge.second->getConfigType() == type) { + HUD_config.set_gauge_color(this_gauge.first, gauge_color.second); + } + } + } + } } catch (const parse::ParseException& e) { diff --git a/code/hud/hudconfig.h b/code/hud/hudconfig.h index e6cf7796ef3..56b80a5333d 100644 --- a/code/hud/hudconfig.h +++ b/code/hud/hudconfig.h @@ -182,13 +182,12 @@ typedef struct HUD_CONFIG_TYPE { } // Get the gauge color, the color based on its type, or white if the gauge is not found - color get_gauge_color(const SCP_string& gauge_id, bool check_exact_match = true) const + color get_gauge_color(const SCP_string& gauge_id) const { auto it = gauge_colors.find(gauge_id); // Got a match? Return it - // but only if we are using the exact match - if (check_exact_match && it != gauge_colors.end()) { + if (it != gauge_colors.end()) { return it->second; } diff --git a/code/pilotfile/plr_hudprefs.cpp b/code/pilotfile/plr_hudprefs.cpp index edc51da6f81..a4c45a6f7c4 100644 --- a/code/pilotfile/plr_hudprefs.cpp +++ b/code/pilotfile/plr_hudprefs.cpp @@ -49,7 +49,7 @@ void hud_config_save_player_prefs(const char* callsign) if (HUD_config.is_gauge_shown_in_config(gauge_id)) { clr = pair.second; } else { - clr = HUD_config.get_gauge_color(gauge_id, false); + clr = HUD_config.get_gauge_color(gauge_id); HUD_config.set_gauge_color(gauge_id, clr); } From d6cb9892de06a71d6bae4f11d20259b844e2443a Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:01:05 -0700 Subject: [PATCH 272/466] account for add state (#6858) --- code/weapon/weapons.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 64cce0f3ff2..8a4571a9793 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -8878,8 +8878,13 @@ void weapon_render(object* obj, model_draw_list *scene) float offset_z_mult = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_OFFSET_Z_MULT, *wp, &wp->modular_curves_instance); float switch_ang_mult = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_HEADON_SWITCH_ANG_MULT, *wp, &wp->modular_curves_instance); float switch_rate_mult = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_HEADON_SWITCH_RATE_MULT, *wp, &wp->modular_curves_instance); - bool anim_has_curve = wip->weapon_curves.has_curve(weapon_info::WeaponCurveOutputs::LASER_ANIM_STATE); - float anim_state = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_ANIM_STATE, *wp, &wp->modular_curves_instance); + bool anim_has_curve = wip->weapon_curves.has_curve(weapon_info::WeaponCurveOutputs::LASER_ANIM_STATE) || wip->weapon_curves.has_curve(weapon_info::WeaponCurveOutputs::LASER_ANIM_STATE_ADD); + // We'll be using both anim_state and anim_state_add if either one has a curve defined, even if the other doesn't, + // so we need to make sure they've got sensible defaults, which in this case means 0. + float anim_state = 0.f; + if (wip->weapon_curves.has_curve(weapon_info::WeaponCurveOutputs::LASER_ANIM_STATE)) { + anim_state = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_ANIM_STATE, *wp, &wp->modular_curves_instance); + } float anim_state_add = 0.f; if (wip->weapon_curves.has_curve(weapon_info::WeaponCurveOutputs::LASER_ANIM_STATE_ADD)) { anim_state_add = wip->weapon_curves.get_output(weapon_info::WeaponCurveOutputs::LASER_ANIM_STATE_ADD, *wp, &wp->modular_curves_instance); From 72ed863700847da8a3629ba7b79d49f9fa75a82d Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Fri, 25 Jul 2025 22:06:00 +0200 Subject: [PATCH 273/466] Prevent lua from garbage collecting possibly-stale subframes (#6866) --- code/scripting/api/objs/texture.cpp | 20 ++++++++++++++------ code/scripting/api/objs/texture.h | 3 ++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/code/scripting/api/objs/texture.cpp b/code/scripting/api/objs/texture.cpp index 17b0b6f6711..18bba798a89 100644 --- a/code/scripting/api/objs/texture.cpp +++ b/code/scripting/api/objs/texture.cpp @@ -6,14 +6,13 @@ #define BMPMAN_INTERNAL #include "bmpman/bm_internal.h" - namespace scripting { namespace api { texture_h::texture_h() = default; -texture_h::texture_h(int bm, bool refcount) : handle(bm) { +texture_h::texture_h(int bm, bool refcount, int parent_bm) : handle(bm), parent_handle(parent_bm) { if (refcount && isValid()) - bm_get_entry(bm)->load_count++; + bm_get_entry(parent_bm != -1 ? parent_bm : bm)->load_count++; } texture_h::~texture_h() { @@ -28,15 +27,22 @@ texture_h::~texture_h() //the textures using load_count. Anything that creates a texture object must also increase load count, unless it is //created in a way that already increases load_count (like bm_load). That way, a texture going out of scope needs to be //released and is safed against memleaks. -Lafiel - bm_release(handle); + //Note 2: Some textures, notably subframes of animations, aren't first-class bmpman citizens and mustn't be released directly. + //Otherwise it is possible (and has been observed in practice) that the parent texture get's deleted before all dependent objects, + //causing this release of the dependent object to clear unrelated textures that were assigned the previously freed spots. + //So instead, both lock and later unlock the parent texture rather than this child texture. -Lafiel + bm_release(parent_handle != -1 ? parent_handle : handle); } bool texture_h::isValid() const { return bm_is_valid(handle) != 0; } + texture_h::texture_h(texture_h&& other) noexcept { *this = std::move(other); } texture_h& texture_h::operator=(texture_h&& other) noexcept { - if (this != &other) + if (this != &other) { std::swap(handle, other.handle); + std::swap(parent_handle, other.parent_handle); + } return *this; } @@ -92,7 +98,7 @@ ADE_INDEXER(l_Texture, "number", //Get actual texture handle frame = first + frame; - return ade_set_args(L, "o", l_Texture.Set(texture_h(frame))); + return ade_set_args(L, "o", l_Texture.Set(texture_h(frame, true, first))); } ADE_FUNC(isValid, l_Texture, NULL, "Detects whether handle is valid", "boolean", "true if valid, false if handle is invalid, nil if a syntax/type error occurs") @@ -139,6 +145,8 @@ ADE_FUNC(destroyRenderTarget, l_Texture, nullptr, "Destroys a texture's render t bm_release_rendertarget(th->handle); + th->handle = -1; + return ADE_RETURN_NIL; } diff --git a/code/scripting/api/objs/texture.h b/code/scripting/api/objs/texture.h index ec7a4343ad6..620fd8f5673 100644 --- a/code/scripting/api/objs/texture.h +++ b/code/scripting/api/objs/texture.h @@ -9,9 +9,10 @@ namespace api { struct texture_h { int handle = -1; + int parent_handle = -1; texture_h(); - explicit texture_h(int bm, bool refcount = true); + explicit texture_h(int bm, bool refcount = true, int parent_handle = -1); ~texture_h(); From fa5ee727708b414e64aff30d81f890b090b52308 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Sat, 26 Jul 2025 02:53:44 +0200 Subject: [PATCH 274/466] Consider current RTT state for screenToBlob (#6867) --- code/graphics/opengl/gropengl.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/code/graphics/opengl/gropengl.cpp b/code/graphics/opengl/gropengl.cpp index d13a48bb4ce..4db80fb0cd8 100644 --- a/code/graphics/opengl/gropengl.cpp +++ b/code/graphics/opengl/gropengl.cpp @@ -337,10 +337,21 @@ SCP_string gr_opengl_blob_screen() GLuint pbo = 0; GL_state.PushFramebufferState(); - GL_state.BindFrameBuffer(Cmdline_window_res ? Back_framebuffer : 0, GL_FRAMEBUFFER); + + GLuint source_fbo = 0; + + GLuint render_target = opengl_get_rtt_framebuffer(); + if (render_target != 0) { + source_fbo = render_target; + } + else if (Cmdline_window_res) { + source_fbo = Back_framebuffer; + } + + GL_state.BindFrameBuffer(source_fbo, GL_FRAMEBUFFER); //Reading from the front buffer here seems to no longer work correctly; that just reads back all zeros - glReadBuffer(Cmdline_window_res ? GL_COLOR_ATTACHMENT0 : GL_FRONT); + glReadBuffer(source_fbo != 0 ? GL_COLOR_ATTACHMENT0 : GL_FRONT); // now for the data if (Use_PBOs) { From fcb257db5929ec952fe1c67d2d4093f03abaa34a Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Sat, 26 Jul 2025 03:04:44 +0200 Subject: [PATCH 275/466] Fix calculation of apparent size (#6872) --- code/model/modelrender.cpp | 16 ++++++++-------- code/model/modelrender.h | 2 +- code/particle/ParticleEffect.cpp | 4 ++-- code/utils/RandomRange.h | 3 +++ code/weapon/weapons.cpp | 6 +++--- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/code/model/modelrender.cpp b/code/model/modelrender.cpp index 545db0023ce..87f130a1092 100644 --- a/code/model/modelrender.cpp +++ b/code/model/modelrender.cpp @@ -2049,17 +2049,17 @@ void model_render_glow_points(const polymodel *pm, const polymodel_instance *pmi // These scaling functions were adapted from Elecman's code. // https://forum.unity.com/threads/this-script-gives-you-objects-screen-size-in-pixels.48966/#post-2107126 -float convert_pixel_size_and_distance_to_diameter(float pixelsize, float distance, float field_of_view_deg, int screen_height) +float convert_pixel_size_and_distance_to_diameter(float pixelsize, float distance, float field_of_view, int screen_width) { - float diameter = (pixelsize * distance * field_of_view_deg) / (fl_degrees(screen_height)); + float diameter = (pixelsize * distance * tanf(field_of_view)) / (screen_width); return diameter; } // These scaling functions were adapted from Elecman's code. // https://forum.unity.com/threads/this-script-gives-you-objects-screen-size-in-pixels.48966/#post-2107126 -float convert_distance_and_diameter_to_pixel_size(float distance, float diameter, float field_of_view_deg, int screen_height) +float convert_distance_and_diameter_to_pixel_size(float distance, float diameter, float field_of_view, int screen_width) { - float pixel_size = (diameter * fl_degrees(screen_height)) / (distance * field_of_view_deg); + float pixel_size = (diameter * screen_width) / (distance * tanf(field_of_view)); return pixel_size; } @@ -2073,16 +2073,16 @@ float model_render_get_diameter_clamped_to_min_pixel_size(const vec3d* pos, floa float current_pixel_size = convert_distance_and_diameter_to_pixel_size( distance_to_eye, diameter, - fl_degrees(g3_get_hfov(Eye_fov)), - gr_screen.max_h); + g3_get_hfov(Eye_fov), + gr_screen.max_w); float scaled_diameter = diameter; if (current_pixel_size < min_pixel_size) { scaled_diameter = convert_pixel_size_and_distance_to_diameter( min_pixel_size, distance_to_eye, - fl_degrees(g3_get_hfov(Eye_fov)), - gr_screen.max_h); + g3_get_hfov(Eye_fov), + gr_screen.max_w); } return scaled_diameter; diff --git a/code/model/modelrender.h b/code/model/modelrender.h index 1b120fe1d19..66edfc28d00 100644 --- a/code/model/modelrender.h +++ b/code/model/modelrender.h @@ -311,7 +311,7 @@ void model_render_arc(const vec3d* v1, const vec3d* v2, const SCP_vector void model_render_set_wireframe_color(const color* clr); bool render_tech_model(tech_render_type model_type, int x1, int y1, int x2, int y2, float zoom, bool lighting, int class_idx, const matrix* orient, const SCP_string& pof_filename = "", float closeup_zoom = 0, const vec3d* closeup_pos = &vmd_zero_vector, const SCP_string& tcolor = ""); -float convert_distance_and_diameter_to_pixel_size(float distance, float diameter, float field_of_view_deg, int screen_height); +float convert_distance_and_diameter_to_pixel_size(float distance, float diameter, float field_of_view, int screen_width); float model_render_get_diameter_clamped_to_min_pixel_size(const vec3d* pos, float diameter, float min_pixel_size); diff --git a/code/particle/ParticleEffect.cpp b/code/particle/ParticleEffect.cpp index eaaa5a1b0d7..5ec86e96f53 100644 --- a/code/particle/ParticleEffect.cpp +++ b/code/particle/ParticleEffect.cpp @@ -130,8 +130,8 @@ float ParticleEffect::getApproximateVisualSize(const vec3d& pos) const { return convert_distance_and_diameter_to_pixel_size( distance_to_eye, m_radius.avg() * 2.f, - fl_degrees(g3_get_hfov(Eye_fov)), - gr_screen.max_h); + g3_get_hfov(Eye_fov), + gr_screen.max_w); } float ParticleEffect::getCurrentFrequencyMult(decltype(modular_curves_definition)::input_type_t source) const { diff --git a/code/utils/RandomRange.h b/code/utils/RandomRange.h index ae7a24e75e1..c574b5ce1ac 100644 --- a/code/utils/RandomRange.h +++ b/code/utils/RandomRange.h @@ -144,6 +144,9 @@ class RandomRange { */ ValueType avg() const { + if (m_constant) + return m_minValue; + if constexpr (has_member(DistributionType, avg())) { return m_distribution.avg(); } diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 8a4571a9793..d782da034ed 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -10200,7 +10200,7 @@ float weapon_get_apparent_size(const weapon& wp) { return convert_distance_and_diameter_to_pixel_size( dist, - wep_objp->radius, - fl_degrees(g3_get_hfov(Eye_fov)), - gr_screen.max_h) / i2fl(gr_screen.max_h); + wep_objp->radius * 2.0f, + g3_get_hfov(Eye_fov), + gr_screen.max_w) / i2fl(gr_screen.max_w); } \ No newline at end of file From 9dc6eb72487e99912e5d21d40a8ffc1f8034969d Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 25 Jul 2025 21:14:09 -0400 Subject: [PATCH 276/466] fix particle related standalone error (#6869) Standalone doesn't create particles so these checks will always fail. --- code/asteroid/asteroid.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index 114e6a97f0a..74c2c2f6f4a 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -2679,7 +2679,10 @@ void asteroid_init() verify_asteroid_splits(); if (!Asteroid_impact_explosion_ani.isValid()) { - Error(LOCATION, "Missing valid asteroid impact explosion definition in asteroid.tbl!"); + // this will always be missing on a standalone + if ( !Is_standalone ) { + Error(LOCATION, "Missing valid asteroid impact explosion definition in asteroid.tbl!"); + } } if (Asteroid_icon_closeup_model[0] == '\0') From 21e35d0ded17a6be3659b796f81bdc54cf49e524 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 25 Jul 2025 21:14:39 -0400 Subject: [PATCH 277/466] fix issue with turret/flak multi packets (#6870) Normalized vectors sent over multi are never guaranteed to still be normalized on the other side. --- code/network/multimsgs.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/network/multimsgs.cpp b/code/network/multimsgs.cpp index 3fb12ebecba..6c2e4e60953 100644 --- a/code/network/multimsgs.cpp +++ b/code/network/multimsgs.cpp @@ -3433,7 +3433,8 @@ void process_turret_fired_packet( ubyte *data, header *hinfo ) } // make an orientation matrix from the o_fvec - vm_vector_2_matrix_norm(&orient, &o_fvec, nullptr, nullptr); + // NOTE: o_fvec is NOT normalized due to pack/unpack altering values! + vm_vector_2_matrix(&orient, &o_fvec, nullptr, nullptr); // find this turret, and set the position of the turret that just fired to be where it fired. Quite a // hack, but should be suitable. @@ -8740,7 +8741,8 @@ void process_flak_fired_packet(ubyte *data, header *hinfo) } // make an orientation matrix from the o_fvec - vm_vector_2_matrix_norm(&orient, &o_fvec, nullptr, nullptr); + // NOTE: o_fvec is NOT normalized due to pack/unpack altering values! + vm_vector_2_matrix(&orient, &o_fvec, nullptr, nullptr); // find this turret, and set the position of the turret that just fired to be where it fired. Quite a // hack, but should be suitable. From 5fd06b40fe72ecc132dd46267e93a39567320f63 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 25 Jul 2025 21:15:31 -0400 Subject: [PATCH 278/466] fix multi crash loading campaigns (#6871) A minor bug in #6077 resulted in a crash loading campaigns on the multi ui. The bug itself was not fatal, however it triggered a cascade of poor error handling which led to a crash. A lot of these issues date back to retail. This fixes the minor bug, and hopefully adds enough error handling to preemptively squash a repeat of the crash problem. --- code/mission/missioncampaign.cpp | 44 +++++++++++++++++++---------- code/mission/missioncampaign.h | 2 +- code/mission/missionparse.cpp | 6 +++- code/network/multi_options.cpp | 3 +- code/network/multiui.cpp | 26 ++++++++--------- code/playerman/managepilot.cpp | 15 ++-------- code/scripting/api/libs/mission.cpp | 4 ++- 7 files changed, 54 insertions(+), 46 deletions(-) diff --git a/code/mission/missioncampaign.cpp b/code/mission/missioncampaign.cpp index 27942cab0c7..f52c494ed98 100644 --- a/code/mission/missioncampaign.cpp +++ b/code/mission/missioncampaign.cpp @@ -100,14 +100,30 @@ campaign Campaign; * In the type field, we return if the campaign is a single player or multiplayer campaign. * The type field will only be valid if the name returned is non-NULL */ -int mission_campaign_get_info(const char *filename, char *name, int *type, int *max_players, char **desc, char **first_mission) +bool mission_campaign_get_info(const char *filename, SCP_string &name, int *type, int *max_players, char **desc, char **first_mission) { - int i, success = 0; - char campaign_type[NAME_LENGTH], fname[MAX_FILENAME_LEN]; + int i, success = false; + SCP_string campaign_type; + char fname[MAX_FILENAME_LEN]; - Assert( name != NULL ); Assert( type != NULL ); + // make sure outputs always have sane values + name.clear(); + *type = -1; + + if (max_players) { + *max_players = 0; + } + + if (desc) { + *desc = nullptr; + } + + if (first_mission) { + *first_mission = nullptr; + } + strncpy(fname, filename, MAX_FILENAME_LEN - 1); auto fname_len = strlen(fname); if ((fname_len < 4) || stricmp(fname + fname_len - 4, FS_CAMPAIGN_FILE_EXT) != 0){ @@ -116,7 +132,6 @@ int mission_campaign_get_info(const char *filename, char *name, int *type, int * } Assert(fname_len < MAX_FILENAME_LEN); - *type = -1; do { try { @@ -124,23 +139,23 @@ int mission_campaign_get_info(const char *filename, char *name, int *type, int * reset_parse(); required_string("$Name:"); - stuff_string(name, F_NAME, NAME_LENGTH); - if (name == NULL) { + stuff_string(name, F_NAME); + if (name.empty()) { nprintf(("Warning", "No name found for campaign file %s\n", filename)); break; } required_string("$Type:"); - stuff_string(campaign_type, F_NAME, NAME_LENGTH); + stuff_string(campaign_type, F_NAME); for (i = 0; i < MAX_CAMPAIGN_TYPES; i++) { - if (!stricmp(campaign_type, campaign_types[i])) { + if (!stricmp(campaign_type.c_str(), campaign_types[i])) { *type = i; } } - if (name == NULL) { - Warning(LOCATION, "Invalid campaign type \"%s\"\n", campaign_type); + if (*type < 0) { + Warning(LOCATION, "Invalid campaign type \"%s\"\n", campaign_type.c_str()); break; } @@ -166,7 +181,7 @@ int mission_campaign_get_info(const char *filename, char *name, int *type, int * // if we found a valid campaign type if ((*type) >= 0) { - success = 1; + success = true; } } catch (const parse::ParseException& e) @@ -176,7 +191,6 @@ int mission_campaign_get_info(const char *filename, char *name, int *type, int * } } while (0); - Assert(success); return success; } @@ -250,7 +264,7 @@ void mission_campaign_free_list() int mission_campaign_maybe_add(const char *filename) { - char name[NAME_LENGTH]; + SCP_string name; char *desc = NULL; int type, max_players; @@ -261,7 +275,7 @@ int mission_campaign_maybe_add(const char *filename) if ( mission_campaign_get_info( filename, name, &type, &max_players, &desc) ) { if ( !MC_multiplayer && (type == CAMPAIGN_TYPE_SINGLE) ) { - Campaign_names[Num_campaigns] = vm_strdup(name); + Campaign_names[Num_campaigns] = vm_strdup(name.c_str()); if (MC_desc) Campaign_descs[Num_campaigns] = desc; diff --git a/code/mission/missioncampaign.h b/code/mission/missioncampaign.h index f8af4fbde96..a4b779ab70e 100644 --- a/code/mission/missioncampaign.h +++ b/code/mission/missioncampaign.h @@ -213,7 +213,7 @@ extern void mission_campaign_save_persistent( int type, int index ); // execute the corresponding mission_campaign_savefile functions. // get name and type of specified campaign file -int mission_campaign_get_info(const char *filename, char *name, int *type, int *max_players, char **desc = nullptr, char **first_mission = nullptr); +bool mission_campaign_get_info(const char *filename, SCP_string &name, int *type, int *max_players, char **desc = nullptr, char **first_mission = nullptr); // get a listing of missions in a campaign int mission_campaign_get_mission_list(const char *filename, char **list, int max); diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index d48678ed6d9..5496313d613 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -6675,7 +6675,11 @@ int get_mission_info(const char *filename, mission *mission_p, bool basic, bool { static SCP_string real_fname_buf; const char *real_fname = nullptr; - + + if ( !filename || !strlen(filename) ) { + return -1; + } + if (filename_is_full_path) { real_fname = filename; } else { diff --git a/code/network/multi_options.cpp b/code/network/multi_options.cpp index 678130ce643..2d28e5b3dec 100644 --- a/code/network/multi_options.cpp +++ b/code/network/multi_options.cpp @@ -750,7 +750,7 @@ void multi_options_process_packet(unsigned char *data, header *hinfo) // get mission choice options case MULTI_OPTION_MISSION: { netgame_info ng; - char title[NAME_LENGTH+1]; + SCP_string title; int campaign_type,max_players; Assert(Game_mode & GM_STANDALONE_SERVER); @@ -779,7 +779,6 @@ void multi_options_process_packet(unsigned char *data, header *hinfo) // set the netgame max players here if the filename has changed if(strcmp(Netgame.campaign_name,ng.campaign_name) != 0){ - memset(title,0,NAME_LENGTH+1); if(!mission_campaign_get_info(ng.campaign_name,title,&campaign_type,&max_players)){ Netgame.max_players = 0; } else { diff --git a/code/network/multiui.cpp b/code/network/multiui.cpp index b64039381eb..dc7280e8b64 100644 --- a/code/network/multiui.cpp +++ b/code/network/multiui.cpp @@ -4535,7 +4535,7 @@ void multi_create_list_load_campaigns() { int idx, file_count; int campaign_type,max_players; - char title[255]; + SCP_string title; char wild_card[10]; char **file_list = NULL; @@ -4717,13 +4717,13 @@ void multi_create_list_do() // so we can set the data without bothering to check the UI anymore void multi_create_list_set_item(int abs_index, int mode) { - int campaign_type, max_players; - char title[NAME_LENGTH + 1]; + int campaign_type = -1, max_players = 0; + SCP_string title; netgame_info ng_temp; netgame_info* ng; multi_create_info* mcip = NULL; - char* campaign_desc; + char* campaign_desc = nullptr; // if not on the standalone server if (Net_player->flags & NETINFO_FLAG_AM_MASTER) { @@ -4738,7 +4738,7 @@ void multi_create_list_set_item(int abs_index, int mode) { if (mode == MULTI_CREATE_SHOW_MISSIONS) { strcpy(ng->mission_name, Multi_create_mission_list[abs_index].filename); } else { - strcpy(ng->mission_name, Multi_create_campaign_list[abs_index].filename); + strcpy(ng->campaign_name, Multi_create_campaign_list[abs_index].filename); } // make sure the netgame type is properly set @@ -4819,24 +4819,22 @@ void multi_create_list_set_item(int abs_index, int mode) { // if not on the standalone server if (Net_player->flags & NETINFO_FLAG_AM_MASTER) { - memset(title, 0, sizeof(title)); // get the campaign info if (!mission_campaign_get_info(ng->campaign_name, title, &campaign_type, &max_players, &campaign_desc, - &first_mission)) { + &first_mission)) + { + nprintf(("Network", "MC: Failed to get campaign info for '%s'!\n", ng->campaign_name)); memset(ng->campaign_name, 0, sizeof(ng->campaign_name)); - ng->max_players = 0; - } - // if we successfully got the info - else { - memset(ng->title, 0, NAME_LENGTH + 1); - strcpy_s(ng->title, title); - ng->max_players = max_players; } + memset(ng->title, 0, sizeof(ng->title)); + strcpy_s(ng->title, title.c_str()); + ng->max_players = max_players; + nprintf(("Network", "MC MAX PLAYERS : %d\n", ng->max_players)); // set the information area text diff --git a/code/playerman/managepilot.cpp b/code/playerman/managepilot.cpp index 0775ca63673..656a409ca4f 100644 --- a/code/playerman/managepilot.cpp +++ b/code/playerman/managepilot.cpp @@ -170,26 +170,17 @@ int local_num_campaigns = 0; int campaign_file_list_filter(const char *filename) { - char name[NAME_LENGTH]; - char *desc = NULL; + SCP_string name; int type, max_players; - if ( mission_campaign_get_info( filename, name, &type, &max_players, &desc) ) { + if ( mission_campaign_get_info( filename, name, &type, &max_players) ) { if ( type == CAMPAIGN_TYPE_SINGLE) { - Campaign_names[local_num_campaigns] = vm_strdup(name); + Campaign_names[local_num_campaigns] = vm_strdup(name.c_str()); local_num_campaigns++; - - // here we *do* free the campaign description because we are not saving the pointer. - if (desc != NULL) - vm_free(desc); - return 1; } } - if (desc != NULL) - vm_free(desc); - return 0; } diff --git a/code/scripting/api/libs/mission.cpp b/code/scripting/api/libs/mission.cpp index b9b3b1ad264..0e1812973fe 100644 --- a/code/scripting/api/libs/mission.cpp +++ b/code/scripting/api/libs/mission.cpp @@ -1847,7 +1847,9 @@ ADE_FUNC(loadMission, l_Mission, "string missionName", "Loads a mission", "boole gr_post_process_set_defaults(); //NOW do the loading stuff - get_mission_info(s, &The_mission, false); + if (get_mission_info(s, &The_mission, false)) + return ADE_RETURN_FALSE; + game_level_init(); if(!mission_load(s)) From f96c73faac37703ef0a952fc7104b8ff6f04f512 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Sat, 26 Jul 2025 03:22:44 +0200 Subject: [PATCH 279/466] Fix unlit shader flag (#6873) --- code/def_files/data/effects/main-f.sdr | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/code/def_files/data/effects/main-f.sdr b/code/def_files/data/effects/main-f.sdr index e60324df170..6932430d65d 100644 --- a/code/def_files/data/effects/main-f.sdr +++ b/code/def_files/data/effects/main-f.sdr @@ -413,6 +413,18 @@ void main() baseColor.rgb += emissiveColor.rgb; #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_DEFERRED + #prereplace IF_FLAG MODEL_SDR_FLAG_DEFERRED + #prereplace IF_NOT_FLAG MODEL_SDR_FLAG_LIGHT + #prereplace IF_FLAG MODEL_SDR_FLAG_SPEC + baseColor.rgb += pow(1.0 - clamp(dot(eyeDir, normal), 0.0, 1.0), 5.0 * clamp(glossData, 0.01, 1.0)) * specColor.rgb; + glossData = 0; + #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_SPEC + // If there is no lighting then we copy the color data so far into the emissive. + emissiveColor.rgb += baseColor.rgb; + aoFactors.x = 0; + #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_LIGHT + #prereplace ENDIF_FLAG //MODEL_SDR_FLAG_DEFERRED + fragOut0 = baseColor; #prereplace IF_FLAG MODEL_SDR_FLAG_DEFERRED From bfacc16a51368ed688224fc037e2ef5ba81505dd Mon Sep 17 00:00:00 2001 From: Asteroth Date: Sat, 26 Jul 2025 06:46:10 -0400 Subject: [PATCH 280/466] Fix for single segment trails (#6864) * fix single segmen trails when interrupted on both ends * cleaner case handling * remove wip stuff --- code/weapon/trails.cpp | 46 +++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/code/weapon/trails.cpp b/code/weapon/trails.cpp index d8c3f43b92e..ea6ff54a093 100644 --- a/code/weapon/trails.cpp +++ b/code/weapon/trails.cpp @@ -160,20 +160,15 @@ void trail_render( trail * trailp ) float speed = vm_vec_mag(&trailp->vel[front]); float total_len = speed * ti->max_life; - float t = vm_vec_dist(&trailp->pos[front], &trailp->pos[back]) / total_len; - CLAMP(t, 0.0f, 1.0f); float f_alpha, b_alpha, f_width, b_width; - if (trailp->object_died) { - f_alpha = t * (ti->a_start - ti->a_end) + ti->a_end; - b_alpha = ti->a_end; - f_width = t * (ti->w_start - ti->w_end) + ti->w_end; - b_width = ti->w_end; - } else { - f_alpha = ti->a_start; - b_alpha = t * (ti->a_end - ti->a_start) + ti->a_start; - f_width = ti->w_start; - b_width = t * (ti->w_end - ti->w_start) + ti->w_start; - } + + float t_front = trailp->val[front] - trailp->val[back]; + float t_back = MAX(0.0f, -trailp->val[back]); + + f_alpha = t_front * (ti->a_start - ti->a_end) + ti->a_end; + b_alpha = t_back * (ti->a_start - ti->a_end) + ti->a_end; + f_width = t_front * (ti->w_start - ti->w_end) + ti->w_end; + b_width = t_back * (ti->w_start - ti->w_end) + ti->w_end; vec3d trail_direction, ftop, fbot, btop, bbot; vm_vec_normalized_dir(&trail_direction, &trailp->pos[back], &trailp->pos[front]); @@ -195,8 +190,8 @@ void trail_render( trail * trailp ) verts[0].texture_position.u = trailp->val[front] * uv_scale; verts[1].texture_position.u = trailp->val[front] * uv_scale; - verts[2].texture_position.u = trailp->val[back] * uv_scale; - verts[3].texture_position.u = trailp->val[back] * uv_scale; + verts[2].texture_position.u = MAX(trailp->val[back], 0.0f) * uv_scale; + verts[3].texture_position.u = MAX(trailp->val[back], 0.0f) * uv_scale; verts[0].texture_position.v = verts[3].texture_position.v = 0.0f; verts[1].texture_position.v = verts[2].texture_position.v = 1.0f; @@ -372,6 +367,9 @@ void trail_add_segment( trail *trailp, vec3d *pos , const matrix* orient, vec3d* if (trailp->single_segment && velocity) { trailp->vel[next] = *velocity; + if (next == 0) { + trailp->val[next] = -1.0; + } } else if (orient != nullptr && trailp->info.spread > 0.0f) { vm_vec_random_in_circle(&trailp->vel[next], &vmd_zero_vector, orient, trailp->info.spread, false, true); } else @@ -404,10 +402,15 @@ void trail_move_all(float frametime) time_delta = frametime / trailp->info.max_life; if (trailp->single_segment) { - if (trailp->object_died) { - trailp->pos[0] += trailp->vel[0] * frametime; // only back keeps going... - trailp->val[0] += time_delta; + trailp->val[0] += time_delta; + if (trailp->val[0] > 0.0f) { + // finished 'unfurling' + // back end moves too now + trailp->pos[0] += trailp->vel[0] * frametime; + } + + if (trailp->object_died) { if (trailp->val[0] >= trailp->val[1]) num_alive_segments = 0; // back has caught up to front and were dead else @@ -416,13 +419,6 @@ void trail_move_all(float frametime) trailp->pos[1] += trailp->vel[1] * frametime; trailp->val[1] += time_delta; - if (trailp->val[1] > 1.0f) { - // finished 'unfurling' - // back end moves too now - trailp->pos[0] += trailp->vel[0] * frametime; - trailp->val[0] += time_delta; - } - num_alive_segments = 2; } } else if ( trailp->tail != trailp->head ) { From 2ed608b303c2684710c933c9028cf46081e96f5f Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 26 Jul 2025 11:46:10 -0400 Subject: [PATCH 281/466] add smoothing field to volumetrics dialog (#6865) FRED support for the smoothing field added in #6854. --- fred2/fred.rc | 77 +++++++++++++++++++++------------------- fred2/resource.h | 2 ++ fred2/volumetricsdlg.cpp | 11 ++++++ fred2/volumetricsdlg.h | 2 ++ 4 files changed, 55 insertions(+), 37 deletions(-) diff --git a/fred2/fred.rc b/fred2/fred.rc index 33d466173e2..3b49fc3c700 100644 --- a/fred2/fred.rc +++ b/fred2/fred.rc @@ -2432,7 +2432,7 @@ BEGIN EDITTEXT IDC_NEW_CONTAINER_NAME,15,14,102,14,ES_AUTOHSCROLL END -IDD_VOLUMETRICS DIALOGEX 0, 0, 541, 215 +IDD_VOLUMETRICS DIALOGEX 0, 0, 541, 232 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Volumetric Nebula" FONT 8, "MS Shell Dlg", 400, 0, 0x1 @@ -2475,15 +2475,18 @@ BEGIN LTEXT "Resolution Oversampling",IDC_STATIC,7,145,80,8 EDITTEXT IDC_OVERSAMPLING,100,143,144,14,ES_AUTOHSCROLL | ES_NUMBER CONTROL "",IDC_SPIN_OVERSAMPLING,"msctls_updown32",UDS_ARROWKEYS,246,143,11,14 - LTEXT "Henyey Greenstein Coeff.",IDC_STATIC,7,162,85,8 - EDITTEXT IDC_HGCOEFF,100,160,144,14,ES_AUTOHSCROLL - CONTROL "",IDC_SPIN_HGCOEFF,"msctls_updown32",UDS_ARROWKEYS,246,160,11,14 - LTEXT "Sun Falloff Factor",IDC_STATIC,7,179,58,8 - EDITTEXT IDC_SUN_FALLOFF,100,177,144,14,ES_AUTOHSCROLL - CONTROL "",IDC_SPIN_SUN_FALLOFF,"msctls_updown32",UDS_ARROWKEYS,246,177,11,14 - LTEXT "Sun Quality Steps",IDC_STATIC,7,196,58,8 - EDITTEXT IDC_STEPS_SUN,100,194,144,14,ES_AUTOHSCROLL | ES_NUMBER - CONTROL "",IDC_SPIN_STEPS_SUN,"msctls_updown32",UDS_ARROWKEYS,246,194,11,14 + LTEXT "Smoothing",IDC_STATIC,7,162,80,8 + EDITTEXT IDC_SMOOTHING,100,160,144,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_SMOOTHING,"msctls_updown32",UDS_ARROWKEYS,246,160,11,14 + LTEXT "Henyey Greenstein Coeff.",IDC_STATIC,7,179,85,8 + EDITTEXT IDC_HGCOEFF,100,177,144,14,ES_AUTOHSCROLL + CONTROL "",IDC_SPIN_HGCOEFF,"msctls_updown32",UDS_ARROWKEYS,246,177,11,14 + LTEXT "Sun Falloff Factor",IDC_STATIC,7,196,58,8 + EDITTEXT IDC_SUN_FALLOFF,100,194,144,14,ES_AUTOHSCROLL + CONTROL "",IDC_SPIN_SUN_FALLOFF,"msctls_updown32",UDS_ARROWKEYS,246,194,11,14 + LTEXT "Sun Quality Steps",IDC_STATIC,7,213,58,8 + EDITTEXT IDC_STEPS_SUN,100,211,144,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_STEPS_SUN,"msctls_updown32",UDS_ARROWKEYS,246,211,11,14 LTEXT "Emissive Light Spread",IDC_STATIC,283,25,70,8 EDITTEXT IDC_EM_SPREAD,376,23,144,14,ES_AUTOHSCROLL CONTROL "",IDC_SPIN_EM_SPREAD,"msctls_updown32",UDS_ARROWKEYS,522,23,11,14 @@ -2493,33 +2496,33 @@ BEGIN LTEXT "Emissive Light Falloff",IDC_STATIC,283,59,67,8 EDITTEXT IDC_EM_FALLOFF,376,57,144,14,ES_AUTOHSCROLL CONTROL "",IDC_SPIN_EM_FALLOFF,"msctls_updown32",UDS_ARROWKEYS,522,57,11,14 - CONTROL "Enable Noise",IDC_NOISE_ENABLE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,283,98,57,10 - GROUPBOX "Noise Settings",IDC_STATIC,283,112,249,97 - CONTROL "",IDC_SPIN_NOISE_COLOR_R,"msctls_updown32",UDS_ARROWKEYS,409,122,11,14 - LTEXT "Color",IDC_STATIC,289,124,18,8 - EDITTEXT IDC_NOISE_COLOR_R,381,122,26,14,ES_AUTOHSCROLL | ES_NUMBER - LTEXT "R",IDC_STATIC,373,124,8,8 - CONTROL "",IDC_SPIN_NOISE_COLOR_G,"msctls_updown32",UDS_ARROWKEYS,463,122,11,14 - EDITTEXT IDC_NOISE_COLOR_G,435,122,26,14,ES_AUTOHSCROLL | ES_NUMBER - LTEXT "G",IDC_STATIC,427,124,8,8 - CONTROL "",IDC_SPIN_NOISE_COLOR_B,"msctls_updown32",UDS_ARROWKEYS,517,122,11,14 - EDITTEXT IDC_NOISE_COLOR_B,489,122,26,14,ES_AUTOHSCROLL | ES_NUMBER - LTEXT "B",IDC_STATIC,481,124,8,8,NOT WS_GROUP - CONTROL "",IDC_SPIN_NOISE_SCALE_B,"msctls_updown32",UDS_ARROWKEYS,437,139,11,14 - LTEXT "Scale",IDC_STATIC,289,141,18,8 - EDITTEXT IDC_NOISE_SCALE_B,392,139,43,14,ES_AUTOHSCROLL - LTEXT "Base",IDC_STATIC,373,141,18,8 - CONTROL "",IDC_SPIN_NOISE_SCALE_S,"msctls_updown32",UDS_ARROWKEYS,517,139,11,14 - EDITTEXT IDC_NOISE_SCALE_S,472,139,43,14,ES_AUTOHSCROLL - LTEXT "Sub",IDC_STATIC,457,141,13,8 - LTEXT "Intensity",IDC_STATIC,289,158,30,8 - EDITTEXT IDC_NOISE_INTENSITY,371,156,144,14,ES_AUTOHSCROLL - CONTROL "",IDC_SPIN_NOISE_INTENSITY,"msctls_updown32",UDS_ARROWKEYS,517,156,11,14 - LTEXT "Resolution",IDC_STATIC,289,175,34,8 - EDITTEXT IDC_NOISE_RESOLUTION,371,173,144,14,ES_AUTOHSCROLL | ES_NUMBER - CONTROL "",IDC_SPIN_NOISE_RESOLUTION,"msctls_updown32",UDS_ARROWKEYS,517,173,11,14 - PUSHBUTTON "Set Base Noise Function",IDC_NOISE_BASE,289,190,118,14,WS_DISABLED - PUSHBUTTON "Set Sub Noise Function",IDC_NOISE_SUB,410,190,118,14,WS_DISABLED + CONTROL "Enable Noise",IDC_NOISE_ENABLE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,283,115,57,10 + GROUPBOX "Noise Settings",IDC_STATIC,283,129,249,97 + CONTROL "",IDC_SPIN_NOISE_COLOR_R,"msctls_updown32",UDS_ARROWKEYS,409,139,11,14 + LTEXT "Color",IDC_STATIC,289,141,18,8 + EDITTEXT IDC_NOISE_COLOR_R,381,139,26,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "R",IDC_STATIC,373,141,8,8 + CONTROL "",IDC_SPIN_NOISE_COLOR_G,"msctls_updown32",UDS_ARROWKEYS,463,139,11,14 + EDITTEXT IDC_NOISE_COLOR_G,435,139,26,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "G",IDC_STATIC,427,141,8,8 + CONTROL "",IDC_SPIN_NOISE_COLOR_B,"msctls_updown32",UDS_ARROWKEYS,517,139,11,14 + EDITTEXT IDC_NOISE_COLOR_B,489,139,26,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "B",IDC_STATIC,481,141,8,8,NOT WS_GROUP + CONTROL "",IDC_SPIN_NOISE_SCALE_B,"msctls_updown32",UDS_ARROWKEYS,437,156,11,14 + LTEXT "Scale",IDC_STATIC,289,158,18,8 + EDITTEXT IDC_NOISE_SCALE_B,392,156,43,14,ES_AUTOHSCROLL + LTEXT "Base",IDC_STATIC,373,158,18,8 + CONTROL "",IDC_SPIN_NOISE_SCALE_S,"msctls_updown32",UDS_ARROWKEYS,517,156,11,14 + EDITTEXT IDC_NOISE_SCALE_S,472,156,43,14,ES_AUTOHSCROLL + LTEXT "Sub",IDC_STATIC,457,158,13,8 + LTEXT "Intensity",IDC_STATIC,289,175,30,8 + EDITTEXT IDC_NOISE_INTENSITY,371,173,144,14,ES_AUTOHSCROLL + CONTROL "",IDC_SPIN_NOISE_INTENSITY,"msctls_updown32",UDS_ARROWKEYS,517,173,11,14 + LTEXT "Resolution",IDC_STATIC,289,192,34,8 + EDITTEXT IDC_NOISE_RESOLUTION,371,190,144,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_NOISE_RESOLUTION,"msctls_updown32",UDS_ARROWKEYS,517,190,11,14 + PUSHBUTTON "Set Base Noise Function",IDC_NOISE_BASE,289,207,118,14,WS_DISABLED + PUSHBUTTON "Set Sub Noise Function",IDC_NOISE_SUB,410,207,118,14,WS_DISABLED END IDD_EDIT_CUSTOM_DATA DIALOGEX 0, 0, 441, 238 diff --git a/fred2/resource.h b/fred2/resource.h index 837c1a88ddb..36ebdff5399 100644 --- a/fred2/resource.h +++ b/fred2/resource.h @@ -1263,6 +1263,8 @@ #define IDC_REQUIRED_WEAPONS 1704 #define IDC_SELECT_DEBRIS 1705 #define IDC_SELECT_ASTEROID 1706 +#define IDC_SMOOTHING 1707 +#define IDC_SPIN_SMOOTHING 1708 #define IDC_SEXP_POPUP_LIST 32770 #define ID_FILE_MISSIONNOTES 32771 #define ID_DUPLICATE 32774 diff --git a/fred2/volumetricsdlg.cpp b/fred2/volumetricsdlg.cpp index c65c0a9ed68..f347f435095 100644 --- a/fred2/volumetricsdlg.cpp +++ b/fred2/volumetricsdlg.cpp @@ -20,6 +20,7 @@ static constexpr std::initializer_list Interactible_fields = { ID_AND_SPIN(STEPS), ID_AND_SPIN(RESOLUTION), ID_AND_SPIN(OVERSAMPLING), + ID_AND_SPIN(SMOOTHING), ID_AND_SPIN(HGCOEFF), ID_AND_SPIN(SUN_FALLOFF), ID_AND_SPIN(STEPS_SUN), @@ -49,6 +50,7 @@ volumetrics_dlg::volumetrics_dlg(CWnd* pParent /*=nullptr*/) : CDialog(volumetri m_steps(15), m_resolution(6), m_oversampling(2), + m_smoothing(0.0f), m_henyeyGreenstein(0.2f), m_sunFalloffFactor(1.0f), m_sunSteps(6), @@ -75,6 +77,7 @@ BOOL volumetrics_dlg::OnInitDialog() static constexpr char* Tooltip_distance = _T("This is how far something has to be in the nebula to be obscured to the maximum opacity."); static constexpr char* Tooltip_steps = _T("If you see banding on ships in the volumetrics, increase this."); static constexpr char* Tooltip_oversampling = _T("Increasing this improves the nebula's edge's smoothness especially for large nebula at low resolutions."); + static constexpr char* Tooltip_smoothing = _T("Smoothing controls how soft edges of the hull POF will be in the nebula, defined as a fraction of the nebula size."); static constexpr char* Tooltip_henyey = _T("Values greater than 0 cause a cloud-like light shine-through, values smaller than 0 cause a highly reflective nebula."); static constexpr char* Tooltip_sun_falloff = _T("Values greater than 1 means the nebula's depths are brighter than they ought to be, values smaller than 0 means they're darker."); static constexpr char* Tooltip_steps_sun = _T("If you see banding in the volumetrics' light and shadow, increase this."); @@ -86,6 +89,8 @@ BOOL volumetrics_dlg::OnInitDialog() m_toolTip.AddTool(GetDlgItem(IDC_SPIN_STEPS), Tooltip_steps); m_toolTip.AddTool(GetDlgItem(IDC_OVERSAMPLING), Tooltip_oversampling); m_toolTip.AddTool(GetDlgItem(IDC_SPIN_OVERSAMPLING), Tooltip_oversampling); + m_toolTip.AddTool(GetDlgItem(IDC_SMOOTHING), Tooltip_smoothing); + m_toolTip.AddTool(GetDlgItem(IDC_SPIN_SMOOTHING), Tooltip_smoothing); m_toolTip.AddTool(GetDlgItem(IDC_HGCOEFF), Tooltip_henyey); m_toolTip.AddTool(GetDlgItem(IDC_SPIN_HGCOEFF), Tooltip_henyey); m_toolTip.AddTool(GetDlgItem(IDC_SUN_FALLOFF), Tooltip_sun_falloff); @@ -111,6 +116,7 @@ BOOL volumetrics_dlg::OnInitDialog() m_steps = volumetrics.steps; m_resolution = volumetrics.resolution; m_oversampling = volumetrics.oversampling; + m_smoothing = volumetrics.smoothing; m_henyeyGreenstein = volumetrics.henyeyGreensteinCoeff; m_sunFalloffFactor = volumetrics.globalLightDistanceFactor; m_sunSteps = volumetrics.globalLightSteps; @@ -154,6 +160,7 @@ void volumetrics_dlg::OnClose() volumetrics.steps = m_steps; volumetrics.resolution = m_resolution; volumetrics.oversampling = m_oversampling; + volumetrics.smoothing = m_smoothing; volumetrics.henyeyGreensteinCoeff = m_henyeyGreenstein; volumetrics.globalLightDistanceFactor = m_sunFalloffFactor; volumetrics.globalLightSteps= m_sunSteps; @@ -199,6 +206,8 @@ void volumetrics_dlg::DoDataExchange(CDataExchange* pDX) DDV_MinMaxInt(pDX, m_resolution, 6, 8); DDX_Text(pDX, IDC_OVERSAMPLING, m_oversampling); DDV_MinMaxInt(pDX, m_oversampling, 1, 3); + DDX_Text(pDX, IDC_SMOOTHING, m_smoothing); + DDV_MinMaxFloat(pDX, m_smoothing, 0.0f, 0.5f); DDX_Text(pDX, IDC_HGCOEFF, m_henyeyGreenstein); DDV_MinMaxFloat(pDX, m_henyeyGreenstein, -1.0f, 1.0f); DDX_Text(pDX, IDC_SUN_FALLOFF, m_sunFalloffFactor); @@ -250,6 +259,7 @@ BEGIN_MESSAGE_MAP(volumetrics_dlg, CDialog) ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN_STEPS, &volumetrics_dlg::OnDeltaposSpinSteps) ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN_RESOLUTION, &volumetrics_dlg::OnDeltaposSpinResolution) ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN_OVERSAMPLING, &volumetrics_dlg::OnDeltaposSpinResolutionOversampling) + ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN_SMOOTHING, &volumetrics_dlg::OnDeltaposSpinSmoothing) ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN_HGCOEFF, &volumetrics_dlg::OnDeltaposSpinHGCoeff) ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN_SUN_FALLOFF, &volumetrics_dlg::OnDeltaposSpinSunFalloff) ON_NOTIFY(UDN_DELTAPOS, IDC_SPIN_STEPS_SUN, &volumetrics_dlg::OnDeltaposSpinStepsSun) @@ -373,6 +383,7 @@ SPINNER_IMPL(SPIN_LINEAR, OpacityDistance, m_opacityDistance, 0.1f, FLT_MAX) SPINNER_IMPL(SPIN_LINEAR, Steps, m_steps, 1, 100) SPINNER_IMPL(SPIN_LINEAR, Resolution, m_resolution, 5, 8) SPINNER_IMPL(SPIN_LINEAR, ResolutionOversampling, m_oversampling, 1, 3) +SPINNER_IMPL(SPIN_LINEAR, Smoothing, m_smoothing, 0.0f, 0.5f, 0.01f) SPINNER_IMPL(SPIN_LINEAR, HGCoeff, m_henyeyGreenstein, -1.0f, 1.0f, 0.1f) SPINNER_IMPL(SPIN_FACTOR, SunFalloff, m_sunFalloffFactor, 0.001f, 100.0f) diff --git a/fred2/volumetricsdlg.h b/fred2/volumetricsdlg.h index 755b905a78e..6055233f5ef 100644 --- a/fred2/volumetricsdlg.h +++ b/fred2/volumetricsdlg.h @@ -24,6 +24,7 @@ class volumetrics_dlg : public CDialog int m_steps; int m_resolution; int m_oversampling; + float m_smoothing; float m_henyeyGreenstein; float m_sunFalloffFactor; int m_sunSteps; @@ -65,6 +66,7 @@ class volumetrics_dlg : public CDialog afx_msg void OnDeltaposSpinSteps(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnDeltaposSpinResolution(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnDeltaposSpinResolutionOversampling(NMHDR* pNMHDR, LRESULT* pResult); + afx_msg void OnDeltaposSpinSmoothing(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnDeltaposSpinHGCoeff(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnDeltaposSpinSunFalloff(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnDeltaposSpinStepsSun(NMHDR* pNMHDR, LRESULT* pResult); From 7a655e44dae4586745e5d3557425c7e467298768 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Mon, 28 Jul 2025 18:20:40 -0400 Subject: [PATCH 282/466] Fix p-spew with homing weapons (#6875) #6808 converted all Pspews to use the new particle effects, though that in turn lead to a fun little visual bug. Overall, particles on weapons will only be rendered if they are valid, and the is valid check included a `weapon_state == m_weaponstate` check (IE states had to match between what the weapon object held and what the particle held. When a weapon switches to homing that particle state was never updated, b/c before 6808 it never needed to be. This lead to the particle effects from pspews not rendering when a missile was in homing mode. This PR fixes that issue by also updating the particle host status whenever the weapon state changes (uses the same logic as when the particle spawns the first time). Tested and works as expected. --- code/weapon/weapons.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index d782da034ed..b32a8a855c5 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -6076,6 +6076,19 @@ static void weapon_set_state(weapon_info* wip, weapon* wp, WeaponState state) source->setHost(make_unique(&Objects[wp->objnum], vmd_zero_vector)); source->finishCreation(); } + + if (wip->wi_flags[Weapon::Info_Flags::Particle_spew]) { + for (const auto& effect : wip->particle_spewers) { + if (!effect.isValid()) + continue; + + auto source = particle::ParticleManager::get()->createSource(effect); + auto host = std::make_unique(&Objects[wp->objnum], vmd_zero_vector); + source->setHost(std::move(host)); + source->finishCreation(); + } + } + } static void weapon_update_state(weapon* wp) From bbb838a332c24471af5a73af90e432bb7e3c17ff Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 28 Jul 2025 18:21:14 -0500 Subject: [PATCH 283/466] custom data for personas (#6861) --- code/mission/missionmessage.cpp | 4 ++ code/mission/missionmessage.h | 1 + code/scripting/api/objs/message.cpp | 58 ++++++++++++++++++++++------- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/code/mission/missionmessage.cpp b/code/mission/missionmessage.cpp index ae3729553fa..0c10595b375 100644 --- a/code/mission/missionmessage.cpp +++ b/code/mission/missionmessage.cpp @@ -322,6 +322,10 @@ void persona_parse() this_persona.flags |= PERSONA_FLAG_NO_AUTOMATIC_ASSIGNMENT; } + if (optional_string("$Custom data:")) { + parse_string_map(this_persona.custom_data, "$end_custom_data", "+Val:"); + } + if (!dup) { int persona_index = (int) Personas.size(); Personas.push_back(this_persona); diff --git a/code/mission/missionmessage.h b/code/mission/missionmessage.h index 85650edd78b..f8f84e851aa 100644 --- a/code/mission/missionmessage.h +++ b/code/mission/missionmessage.h @@ -239,6 +239,7 @@ struct Persona { char name[NAME_LENGTH]; int flags; int species_bitfield; + SCP_map custom_data; }; extern SCP_vector Personas; diff --git a/code/scripting/api/objs/message.cpp b/code/scripting/api/objs/message.cpp index 10057380f34..5e8c99f37b5 100644 --- a/code/scripting/api/objs/message.cpp +++ b/code/scripting/api/objs/message.cpp @@ -23,10 +23,7 @@ ADE_VIRTVAR(Name, l_Persona, "string", "The name of the persona", "string", "The if (!ade_get_args(L, "o", l_Persona.Get(&idx))) return ade_set_error(L, "s", ""); - if (Personas.empty()) - return ade_set_error(L, "s", ""); - - if (idx < 0 || idx >= (int)Personas.size()) + if (!SCP_vector_inbounds(Personas, idx)) return ade_set_error(L, "s", ""); return ade_set_args(L, "s", Personas[idx].name); @@ -39,9 +36,6 @@ ADE_VIRTVAR(Index, l_Persona, nullptr, nullptr, "number", "The index of the pers if (!ade_get_args(L, "o", l_Persona.Get(&idx))) return ade_set_args(L, "i", -1); - if (Personas.empty()) - return ade_set_args(L, "i", -1); - if (!SCP_vector_inbounds(Personas, idx)) return ade_set_args(L, "i", -1); @@ -51,14 +45,52 @@ ADE_VIRTVAR(Index, l_Persona, nullptr, nullptr, "number", "The index of the pers return ade_set_args(L, "i", idx + 1); } -ADE_FUNC(isValid, l_Persona, NULL, "Detect if the handle is valid", "boolean", "true if valid, false otherwise") +ADE_VIRTVAR(CustomData, l_Persona, nullptr, "Gets the custom data table for this persona", "table", "The persona's custom data table or nil if an error occurs.") +{ + int idx = -1; + + if (!ade_get_args(L, "o", l_Persona.Get(&idx))) + return ADE_RETURN_NIL; + + if (!SCP_vector_inbounds(Personas, idx)) + return ADE_RETURN_NIL; + + if (ADE_SETTING_VAR) { + LuaError(L, "This property is read only."); + } + + auto table = luacpp::LuaTable::create(L); + + for (const auto& pair : Personas[idx].custom_data) + { + table.addValue(pair.first, pair.second); + } + + return ade_set_args(L, "t", &table); +} + +ADE_FUNC(hasCustomData, l_Persona, nullptr, "Detects whether the persona has any custom data", "boolean", "true if the persona's custom_data is not empty, false otherwise. Nil if an error occurs.") +{ + int idx = -1; + + if (!ade_get_args(L, "o", l_Persona.Get(&idx))) + return ADE_RETURN_NIL; + + if (!SCP_vector_inbounds(Personas, idx)) + return ADE_RETURN_NIL; + + bool result = !Personas[idx].custom_data.empty(); + return ade_set_args(L, "b", result); +} + +ADE_FUNC(isValid, l_Persona, nullptr, "Detect if the handle is valid", "boolean", "true if valid, false otherwise") { int idx = -1; if (!ade_get_args(L, "o", l_Persona.Get(&idx))) return ADE_RETURN_FALSE; - return ade_set_args(L, "b", idx >= 0 && idx < (int)Personas.size()); + return ade_set_args(L, "b", SCP_vector_inbounds(Personas, idx)); } //**********HANDLE: Message @@ -88,7 +120,7 @@ ADE_VIRTVAR(Message, l_Message, "string", "The unaltered text of the message, se if (!SCP_vector_inbounds(Messages, idx)) return ade_set_error(L, "s", ""); - if (ADE_SETTING_VAR && newText != NULL) + if (ADE_SETTING_VAR && newText != nullptr) { if (strlen(newText) > MESSAGE_LENGTH) LuaError(L, "New message text is too long, maximum is %d!", MESSAGE_LENGTH); @@ -151,7 +183,7 @@ ADE_VIRTVAR(Persona, l_Message, "persona", "The persona of the message", "person if (!SCP_vector_inbounds(Messages, idx)) return ade_set_error(L, "o", l_Soundfile.Set(soundfile_h())); - if (ADE_SETTING_VAR && newPersona >= 0 && newPersona < (int)Personas.size()) + if (ADE_SETTING_VAR && SCP_vector_inbounds(Personas, newPersona)) { Messages[idx].persona_index = newPersona; } @@ -183,13 +215,13 @@ ADE_FUNC(getMessage, l_Message, "[boolean replaceVars = true]", "Gets the text o } } -ADE_FUNC(isValid, l_Message, NULL, "Checks if the message handle is valid", "boolean", "true if valid, false otherwise") +ADE_FUNC(isValid, l_Message, nullptr, "Checks if the message handle is valid", "boolean", "true if valid, false otherwise") { int idx = -1; if (!ade_get_args(L, "o", l_Message.Get(&idx))) return ADE_RETURN_FALSE; - return ade_set_args(L, "b", idx >= 0 && idx < (int) Messages.size()); + return ade_set_args(L, "b", SCP_vector_inbounds(Messages, idx)); } From 2ce4bdd55c3052a9c91ee45ac40995206ae77f67 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 29 Jul 2025 06:41:45 -0500 Subject: [PATCH 284/466] get font scale options for lua (#6876) --- code/scripting/api/objs/font.cpp | 38 +++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/code/scripting/api/objs/font.cpp b/code/scripting/api/objs/font.cpp index 323338490fe..46503d7f8d3 100644 --- a/code/scripting/api/objs/font.cpp +++ b/code/scripting/api/objs/font.cpp @@ -36,9 +36,9 @@ bool font_h::isValid() const { ADE_OBJ(l_Font, font_h, "font", "font handle"); -ADE_FUNC(__tostring, l_Font, NULL, "Name of font", "string", "Font filename, or an empty string if the handle is invalid") +ADE_FUNC(__tostring, l_Font, nullptr, "Name of font", "string", "Font filename, or an empty string if the handle is invalid") { - font_h *fh = NULL; + font_h* fh = nullptr; if (!ade_get_args(L, "o", l_Font.GetPtr(&fh))) return ade_set_error(L, "s", ""); @@ -60,9 +60,9 @@ ADE_FUNC(__eq, l_Font, "font, font", "Checks if the two fonts are equal", "boole return ade_set_args(L, "b", fh1->Get()->getName() == fh2->Get()->getName()); } -ADE_VIRTVAR(Filename, l_Font, "string", "Name of font (including extension)
Important:This variable is deprecated. Use Name instead.", "string", NULL) +ADE_VIRTVAR(Filename, l_Font, "string", "Name of font (including extension)
Important:This variable is deprecated. Use Name instead.", "string", nullptr) { - font_h *fh = NULL; + font_h* fh = nullptr; const char* newname = nullptr; if (!ade_get_args(L, "o|s", l_Font.GetPtr(&fh), &newname)) return ade_set_error(L, "s", ""); @@ -78,9 +78,9 @@ ADE_VIRTVAR(Filename, l_Font, "string", "Name of font (including extension)
< return ade_set_args(L, "s", fh->Get()->getName().c_str()); } -ADE_VIRTVAR(Name, l_Font, "string", "Name of font (including extension)", "string", NULL) +ADE_VIRTVAR(Name, l_Font, "string", "Name of font (including extension)", "string", nullptr) { - font_h *fh = NULL; + font_h* fh = nullptr; const char* newname = nullptr; if (!ade_get_args(L, "o|s", l_Font.GetPtr(&fh), &newname)) return ade_set_error(L, "s", ""); @@ -116,7 +116,7 @@ ADE_VIRTVAR(FamilyName, l_Font, "string", "Family Name of font. Bitmap fonts alw ADE_VIRTVAR(Height, l_Font, "number", "Height of font (in pixels)", "number", "Font height, or 0 if the handle is invalid") { - font_h *fh = NULL; + font_h* fh = nullptr; int newheight = -1; if (!ade_get_args(L, "o|i", l_Font.GetPtr(&fh), &newheight)) return ade_set_error(L, "i", 0); @@ -134,7 +134,7 @@ ADE_VIRTVAR(Height, l_Font, "number", "Height of font (in pixels)", "number", "F ADE_VIRTVAR(TopOffset, l_Font, "number", "The offset this font has from the baseline of textdrawing downwards. (in pixels)", "number", "Font top offset, or 0 if the handle is invalid") { - font_h *fh = NULL; + font_h* fh = nullptr; float newOffset = -1; if (!ade_get_args(L, "o|f", l_Font.GetPtr(&fh), &newOffset)) return ade_set_error(L, "f", 0.0f); @@ -152,7 +152,7 @@ ADE_VIRTVAR(TopOffset, l_Font, "number", "The offset this font has from the base ADE_VIRTVAR(BottomOffset, l_Font, "number", "The space (in pixels) this font skips downwards after drawing a line of text", "number", "Font bottom offset, or 0 if the handle is invalid") { - font_h *fh = NULL; + font_h* fh = nullptr; float newOffset = -1; if (!ade_get_args(L, "o|f", l_Font.GetPtr(&fh), &newOffset)) return ade_set_error(L, "f", 0.0f); @@ -168,7 +168,25 @@ ADE_VIRTVAR(BottomOffset, l_Font, "number", "The space (in pixels) this font ski return ade_set_args(L, "f", fh->Get()->getBottomOffset()); } -ADE_FUNC(isValid, l_Font, NULL, "True if valid, false or nil if not", "boolean", "Detects whether handle is valid") +ADE_FUNC(hasAutoSize, l_Font, nullptr, "True if FSO will auto size this font, false or nil if not", "boolean", "Detects whether the font has Auto Size activated") +{ + font_h* fh = nullptr; + if (!ade_get_args(L, "o", l_Font.GetPtr(&fh))) + return ADE_RETURN_NIL; + + return ade_set_args(L, "b", fh != nullptr && fh->Get()->getAutoScaleBehavior()); +} + +ADE_FUNC(hasCanScale, l_Font, nullptr, "True if this font is allowed to scale based on user settings, false or nil if not", "boolean", "Detects whether the font can scale") +{ + font_h* fh = nullptr; + if (!ade_get_args(L, "o", l_Font.GetPtr(&fh))) + return ADE_RETURN_NIL; + + return ade_set_args(L, "b", fh != nullptr && fh->Get()->getScaleBehavior()); +} + +ADE_FUNC(isValid, l_Font, nullptr, "True if valid, false or nil if not", "boolean", "Detects whether handle is valid") { font_h *fh = nullptr; if (!ade_get_args(L, "o", l_Font.GetPtr(&fh))) From 3e77e1370d3b8b52c61d77c3ec2a986bffd7e529 Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:24:32 -0700 Subject: [PATCH 285/466] fix copy-paste error (#6878) --- code/object/collideshipship.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/object/collideshipship.cpp b/code/object/collideshipship.cpp index 1cca7872e32..8e350d6f47e 100644 --- a/code/object/collideshipship.cpp +++ b/code/object/collideshipship.cpp @@ -1371,7 +1371,7 @@ void collide_ship_ship_process(obj_pair * pair, const std::any& collision_data) hud_shield_quadrant_hit(ship_ship_hit_info.heavy, quadrant_num); // don't draw sparks (using sphere hitpos) - float damage_light = (100.0f * damage / heavy_obj->phys_info.mass); + float damage_light = (100.0f * damage / light_obj->phys_info.mass); ship_apply_local_damage(ship_ship_hit_info.light, ship_ship_hit_info.heavy, &world_hit_pos, damage_light, heavy_shipp->collision_damage_type_idx, MISS_SHIELDS, NO_SPARKS, -1, &ship_ship_hit_info.collision_normal); From f374eadb4576cd949163792d2461a3a7f2fbc10b Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Wed, 30 Jul 2025 00:24:51 +0200 Subject: [PATCH 286/466] Fix load order guarantees (#6883) --- code/object/objcollide.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/code/object/objcollide.cpp b/code/object/objcollide.cpp index 1d2a9365a2d..5d65219fa04 100644 --- a/code/object/objcollide.cpp +++ b/code/object/objcollide.cpp @@ -795,7 +795,10 @@ void post_process_threaded_collisions() { for(auto& [i, processed] : workerThreads) { auto& thread = collision_thread_data_buffer[i]; - if (thread.result_length.load(std::memory_order_acquire) > processed) { + size_t queue_length = thread.queue_length.load(std::memory_order_acquire); + size_t result_length = thread.result_length.load(std::memory_order_acquire); + + if (result_length > processed) { { std::scoped_lock lock(thread.result_mutex); thread.queue_results.swap(thread.queue_send); @@ -816,7 +819,7 @@ void post_process_threaded_collisions() { processed += thread.queue_send->size(); thread.queue_send->clear(); } - else if (thread.queue_length.load(std::memory_order_acquire) == 0) { + else if (queue_length == 0) { thread.queue_results->clear(); workerThreads.erase(i); break; From abc82bd323fee65e47dce768b8daee0bfd164c1c Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Wed, 30 Jul 2025 04:12:35 -0700 Subject: [PATCH 287/466] Handle impacts for obscure code paths (#6848) * handle healing weapons * incorporate shield threshold features * fix piercing sign error * use function --- code/ship/shiphit.cpp | 67 ++++++++++++++++++++++++++++++++++++++--- code/weapon/weapons.cpp | 2 +- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 505de505073..b1fb711579e 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -418,7 +418,7 @@ typedef struct { // fundamentally similar to do_subobj_hit_stuff, but without many checks inherent to damaging instead of healing // most notably this does NOT return "remaining healing" (healing always carries), this is will NOT subtract from hull healing -void do_subobj_heal_stuff(const object* ship_objp, const object* other_obj, const vec3d* hitpos, int submodel_num, float healing) +std::pair, float> do_subobj_heal_stuff(const object* ship_objp, const object* other_obj, const vec3d* hitpos, int submodel_num, float healing) { vec3d g_subobj_pos; float healing_left; @@ -434,6 +434,8 @@ void do_subobj_heal_stuff(const object* ship_objp, const object* other_obj, cons ship_p = &Ships[ship_objp->instance]; + std::optional subsys_impact = std::nullopt; + if (other_obj->type == OBJ_SHOCKWAVE) { healing_left = shockwave_get_damage(other_obj->instance) / 2.0f; @@ -602,6 +604,16 @@ void do_subobj_heal_stuff(const object* ship_objp, const object* other_obj, cons // Nuke: this will finally factor it in to heal_to_apply and i wont need to factor it in anywhere after this heal_to_apply = Armor_types[subsystem->armor_type_idx].GetDamage(heal_to_apply, dmg_type_idx, 1.0f, other_obj->type == OBJ_BEAM); + if (j == 0) { + subsys_impact = ConditionData { + ImpactCondition(subsystem->armor_type_idx), + HitType::SUBSYS, + heal_to_apply, + subsystem->current_hits, + subsystem->max_hits, + }; + } + subsystem->current_hits += heal_to_apply; float* agg_hits = &ship_p->subsys_info[subsystem->system_info->type].aggregate_current_hits; @@ -626,6 +638,7 @@ void do_subobj_heal_stuff(const object* ship_objp, const object* other_obj, cons break; } } + return std::make_pair(subsys_impact, healing); } // do_subobj_hit_stuff() is called when a collision is detected between a ship and something @@ -2361,9 +2374,14 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi MONITOR_INC( ShipHits, 1 ); + std::array, NumHitTypes> impact_data = {}; + // Don't damage player ship in the process of warping out. if ( Player->control_mode >= PCM_WARPOUT_STAGE2 ) { if ( ship_objp == Player_obj ){ + if (!global_damage) { + maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); + } return; } } @@ -2410,10 +2428,12 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi } } - std::array, NumHitTypes> impact_data = {}; // If the ship is invulnerable, do nothing if (ship_objp->flags[Object::Object_Flags::Invulnerable]) { + if (!global_damage) { + maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); + } return; } @@ -2713,7 +2733,7 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi } } -static void ship_do_healing(object* ship_objp, const object* other_obj, const vec3d* hitpos, float healing, int submodel_num, int damage_type_idx = -1) +static void ship_do_healing(object* ship_objp, const object* other_obj, const vec3d* hitpos, float healing, int quadrant, int submodel_num, int damage_type_idx = -1, const vec3d* hit_normal = nullptr, const vec3d* local_hitpos = nullptr) { // multiplayer clients dont do healing if (MULTIPLAYER_CLIENT) @@ -2740,8 +2760,13 @@ static void ship_do_healing(object* ship_objp, const object* other_obj, const ve MONITOR_INC(ShipHits, 1); + std::array, NumHitTypes> impact_data = {}; + // Don't heal player ship in the process of warping out. if ((Player->control_mode >= PCM_WARPOUT_STAGE2) && (ship_objp == Player_obj)) { + if (!global_damage) { + maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); + } return; } @@ -2757,22 +2782,44 @@ static void ship_do_healing(object* ship_objp, const object* other_obj, const ve return; weapon_info* wip = &Weapon_info[wip_index]; + float shield_health; + if (quadrant >= 0) { + shield_health = MAX(0.0f, ship_objp->shield_quadrant[quadrant] - ship_shield_hitpoint_threshold(ship_objp, false)); + } else { + //if we haven't hit a shield, assume the shield is fully depleted, because we have no way of knowing what the relevant quadrant would be + shield_health = 0.f; + } + // handle shield healing if (!(ship_objp->flags[Object::Object_Flags::No_shields])) { + auto shield_impact = ConditionData { + ImpactCondition(shipp->shield_armor_type_idx), + HitType::SHIELD, + 0.0f, + shield_health, + shield_get_max_quad(ship_objp) - ship_shield_hitpoint_threshold(ship_objp, false), + }; + float shield_healing = healing * wip->shield_factor; if (shield_healing > 0.0f) { if (shipp->shield_armor_type_idx != -1) shield_healing = Armor_types[shipp->shield_armor_type_idx].GetDamage(shield_healing, damage_type_idx, 1.0f, other_obj_is_beam); + shield_impact.damage = shield_healing; shield_apply_healing(ship_objp, shield_healing); } + impact_data[static_cast>(HitType::SHIELD)] = shield_impact; } // now for subsystems and hull if ((healing > 0.0f)) { - do_subobj_heal_stuff(ship_objp, other_obj, hitpos, submodel_num, healing); + auto healing_pair = do_subobj_heal_stuff(ship_objp, other_obj, hitpos, submodel_num, healing); + + healing = healing_pair.second; + + impact_data[static_cast>(HitType::SUBSYS)] = healing_pair.first; //Do armor stuff if (shipp->armor_type_idx != -1) @@ -2783,11 +2830,21 @@ static void ship_do_healing(object* ship_objp, const object* other_obj, const ve healing *= wip->armor_factor; + impact_data[static_cast>(HitType::HULL)] = ConditionData { + ImpactCondition(shipp->armor_type_idx), + HitType::HULL, + healing, + ship_objp->hull_strength, + shipp->ship_max_hull_strength, + }; + ship_objp->hull_strength += healing; if (ship_objp->hull_strength > shipp->ship_max_hull_strength) ship_objp->hull_strength = shipp->ship_max_hull_strength; } + maybe_play_conditional_impacts(impact_data, other_obj, ship_objp, true, submodel_num, hitpos, local_hitpos, hit_normal); + // fix up the ship's sparks :) // turn off a random spark, if its a beam, do this on average twice a second if(!other_obj_is_beam || frand() > flFrametime * 2.0f ) @@ -2950,7 +3007,7 @@ void ship_apply_local_damage(object *ship_objp, object *other_obj, const vec3d * global_damage = false; if (wip_index >= 0 && Weapon_info[wip_index].wi_flags[Weapon::Info_Flags::Heals]) { - ship_do_healing(ship_objp, other_obj, hitpos, damage, submodel_num); + ship_do_healing(ship_objp, other_obj, hitpos, damage, quadrant, submodel_num, -1, hit_normal, local_hitpos); create_sparks = false; } else { ship_do_damage(ship_objp, other_obj, hitpos, damage, quadrant, submodel_num, damage_type_idx, false, hit_dot, hit_normal, local_hitpos); diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index b32a8a855c5..07cf9ce4633 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -7987,7 +7987,7 @@ void maybe_play_conditional_impacts(const std::arrayfinishCreation(); } - if (impacted_objp != nullptr && (impact_data[static_cast>(HitType::HULL)].has_value() || impact_data[static_cast>(HitType::SUBSYS)].has_value()) && (!valid_conditional_impact && wip->piercing_impact_effect.isValid() && armed_weapon)) { + if (impacted_objp != nullptr && !impact_data[static_cast>(HitType::SHIELD)].has_value() && (!valid_conditional_impact && wip->piercing_impact_effect.isValid() && armed_weapon)) { if ((impacted_objp->type == OBJ_SHIP) || (impacted_objp->type == OBJ_DEBRIS)) { int ok_to_draw = 1; From 85ebd316e1335dc58940d60dd686621522d9ce43 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 30 Jul 2025 06:17:09 -0500 Subject: [PATCH 288/466] Only show one warning for skipped fonts (#6880) --- code/graphics/software/font.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/code/graphics/software/font.cpp b/code/graphics/software/font.cpp index ca8b0d9b6c7..3ce1380c4e3 100644 --- a/code/graphics/software/font.cpp +++ b/code/graphics/software/font.cpp @@ -460,13 +460,15 @@ namespace FontType type; SCP_string fontName; + SCP_vector skipped_font_names; + while (parse_type(type, fontName)) { switch (type) { case VFNT_FONT: if (Unicode_text_mode) { - Warning(LOCATION, "Bitmap fonts are not supported in Unicode text mode! Font %s will be ignored.", fontName.c_str()); + skipped_font_names.push_back(fontName); skip_to_start_of_string_one_of({"$TrueType:", "$Font:", "#End"}); } else { parse_vfnt_font(fontName); @@ -481,6 +483,14 @@ namespace } } + // check if we skipped any fonts + if (!skipped_font_names.empty()) { + Warning(LOCATION, "One or more bitmap fonts were skipped because they are not supported in Unicode text mode. The list of fonts is in the debug log."); + for (const auto& skippedFont : skipped_font_names) { + mprintf(("Warning: Skipped bitmap font: %s\n", skippedFont.c_str())); + } + } + // done parsing required_string("#End"); } From c26b0caeb847e42a933aaff46a2301f089565a68 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Wed, 30 Jul 2025 07:17:54 -0400 Subject: [PATCH 289/466] Ensure `Aspect_invulnerability_fix` always works (#6879) #6248 added an extra subtraction line to calculating when a bomb can be shot down `extra_buggy_time`, but that value should only be calculated if `Aspect_invulnerability_fix` is off. Specifically, the `else if` checks on lines 79 and 97 of `collideweaponweapon.cpp` were getting hit even when `Aspect_invulnerability_fix` was enabled, so ensure that if that flag is on that it correctly keeps the `extra_buggy_time` at 0. Fix works as expected, happy to edit or tune however. --- code/object/collideweaponweapon.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/object/collideweaponweapon.cpp b/code/object/collideweaponweapon.cpp index a337f3084c6..04e49bec114 100644 --- a/code/object/collideweaponweapon.cpp +++ b/code/object/collideweaponweapon.cpp @@ -69,7 +69,7 @@ int collide_weapon_weapon( obj_pair * pair ) // the erroneous extra time a bomb stays invulnerable without the fix float extra_buggy_time = 0.0f; - if (wipA->is_locked_homing()) + if (!(The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && wipA->is_locked_homing()) extra_buggy_time = (wipA->lifetime * LOCKED_HOMING_EXTENDED_LIFE_FACTOR) - wipA->lifetime; if ((The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && (wipA->is_locked_homing()) && (wpA->homing_object != &obj_used_list)) { @@ -87,7 +87,7 @@ int collide_weapon_weapon( obj_pair * pair ) // the erroneous extra time a bomb stays invulnerable without the fix float extra_buggy_time = 0.0f; - if (wipB->is_locked_homing()) + if (!(The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && wipB->is_locked_homing()) extra_buggy_time = (wipB->lifetime * LOCKED_HOMING_EXTENDED_LIFE_FACTOR) - wipB->lifetime; if ((The_mission.ai_profile->flags[AI::Profile_Flags::Aspect_invulnerability_fix]) && (wipB->is_locked_homing()) && (wpB->homing_object != &obj_used_list)) { From 8f835fe34b9d9aec16b45ca8267ebfb2c68efd71 Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Wed, 30 Jul 2025 04:52:46 -0700 Subject: [PATCH 290/466] check for global (#6882) --- code/ship/shiphit.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index b1fb711579e..a53510edf17 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -799,7 +799,7 @@ std::pair, float> do_subobj_hit_stuff(object *ship_ } } subsys->current_hits = 0.0f; - do_subobj_destroyed_stuff( ship_p, subsys, hitpos ); + do_subobj_destroyed_stuff( ship_p, subsys, global_damage ? nullptr : hitpos ); continue; } else { continue; @@ -1045,7 +1045,7 @@ std::pair, float> do_subobj_hit_stuff(object *ship_ // multiplayer clients never blow up subobj stuff on their own if ( (subsystem->current_hits <= 0.0f) && !MULTIPLAYER_CLIENT) { - do_subobj_destroyed_stuff( ship_p, subsystem, hitpos ); + do_subobj_destroyed_stuff( ship_p, subsystem, global_damage ? nullptr : hitpos ); } if (damage_left <= 0) { // no more damage to distribute, so stop checking From 95d21a408550ba47c27d7e7c18f3c725990544c0 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Wed, 30 Jul 2025 13:53:26 +0200 Subject: [PATCH 291/466] Minor Particle Fixes (#6884) * Fix scripted particle velocity * Add error if no bitmaps were defined --- code/particle/ParticleParse.cpp | 7 ++++++- code/scripting/api/libs/graphics.cpp | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/code/particle/ParticleParse.cpp b/code/particle/ParticleParse.cpp index f5ba89cad7f..387cb8c96c9 100644 --- a/code/particle/ParticleParse.cpp +++ b/code/particle/ParticleParse.cpp @@ -17,7 +17,12 @@ namespace particle { static void parseBitmaps(ParticleEffect &effect) { if (internal::required_string_if_new("+Filename:", false)) { effect.m_bitmap_list = internal::parseAnimationList(true); - effect.m_bitmap_range = ::util::UniformRange(0, effect.m_bitmap_list.size() - 1); + + if (effect.m_bitmap_list.empty()) { + error_display(1, "No bitmap defined for particle effect!"); + } else { + effect.m_bitmap_range = ::util::UniformRange(0, effect.m_bitmap_list.size() - 1); + } } } diff --git a/code/scripting/api/libs/graphics.cpp b/code/scripting/api/libs/graphics.cpp index 723cccd62a8..249785b6b9b 100644 --- a/code/scripting/api/libs/graphics.cpp +++ b/code/scripting/api/libs/graphics.cpp @@ -2212,10 +2212,10 @@ particle::ParticleEffectHandle getLegacyScriptingParticleEffect(int bitmap, bool ::util::UniformFloatRange(), //No duration ::util::UniformFloatRange (-1.f), //Single particle only particle::ParticleEffect::ShapeDirection::ALIGNED, //Particle direction - ::util::UniformFloatRange(), //Velocity Inherit + ::util::UniformFloatRange(1.f), //Velocity Inherit false, //Velocity Inherit absolute? - std::make_unique(::util::ParsedRandomFloatRange(), 1.f), //Velocity volume - ::util::UniformFloatRange(1.f), //Velocity volume multiplier + nullptr, //Velocity volume + ::util::UniformFloatRange(), //Velocity volume multiplier particle::ParticleEffect::VelocityScaling::NONE, //Velocity directional scaling std::nullopt, //Orientation-based velocity std::nullopt, //Position-based velocity From 44952eb2d65af61ba8cccd2ad823bdb81bfa6f9c Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 30 Jul 2025 13:03:34 -0500 Subject: [PATCH 292/466] Fix some minor hud config color errors (#6881) --- code/hud/hudconfig.cpp | 6 +++--- code/scripting/api/objs/hudconfig.cpp | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/code/hud/hudconfig.cpp b/code/hud/hudconfig.cpp index 32fd548cf51..7d008b7f5ab 100644 --- a/code/hud/hudconfig.cpp +++ b/code/hud/hudconfig.cpp @@ -320,9 +320,9 @@ SCP_vector> HC_gauge_mouse_coords; // Names and XSTR IDs for these come from HC_text above hc_col HC_colors[NUM_HUD_COLOR_PRESETS] = { - {0, 255, 0, "", -1}, // Green - {67, 123, 203, "", -1}, // Blue - {255, 197, 0, "", -1}, // Amber + {0, 255, 0, "Green", 1457}, // Green + {67, 123, 203, "Blue", 1456}, // Blue + {255, 197, 0, "Amber", 1455}, // Ambers }; int HC_default_color = HUD_COLOR_PRESET_1; diff --git a/code/scripting/api/objs/hudconfig.cpp b/code/scripting/api/objs/hudconfig.cpp index 12733a8fd39..6bb42fb08f5 100644 --- a/code/scripting/api/objs/hudconfig.cpp +++ b/code/scripting/api/objs/hudconfig.cpp @@ -71,7 +71,7 @@ SCP_string hud_color_preset_h::getName() const bool hud_color_preset_h::isValid() const { - return preset >= 0 && preset < static_cast(HC_preset_filenames.size()); + return preset >= 0 && preset < NUM_HUD_COLOR_PRESETS; } //**********HANDLE: hud color preset From e52fa1edeb368212ccbd05830abf6e1ef648b5a9 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 30 Jul 2025 13:04:04 -0500 Subject: [PATCH 293/466] No need to display debris label on objects in the Debris list (#6877) * No need to display debris label on objects in the Debris list * remove the unnecessary loop --- code/lab/dialogs/lab_ui.cpp | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/code/lab/dialogs/lab_ui.cpp b/code/lab/dialogs/lab_ui.cpp index aa0eca46a41..75b51bf4193 100644 --- a/code/lab/dialogs/lab_ui.cpp +++ b/code/lab/dialogs/lab_ui.cpp @@ -138,21 +138,15 @@ void LabUi::build_debris_list() continue; } - int subtype_idx = 0; - for (const auto& subtype : info.subtypes) { - SCP_string node_label; - sprintf(node_label, "##DebrisClassIndex%i_%i", debris_idx, subtype_idx); - TreeNodeEx(node_label.c_str(), - ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen, - "%s (%s)", - info.name, - subtype.type_name.c_str()); - - if (IsItemClicked() && !IsItemToggledOpen()) { - getLabManager()->changeDisplayedObject(LabMode::Asteroid, debris_idx, subtype_idx); - } - - subtype_idx++; + SCP_string node_label; + sprintf(node_label, "##DebrisClassIndex%i", debris_idx); + TreeNodeEx(node_label.c_str(), + ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen, + "%s", + info.name); + + if (IsItemClicked() && !IsItemToggledOpen()) { + getLabManager()->changeDisplayedObject(LabMode::Asteroid, debris_idx, 0); // Debris subtype is always 0 } debris_idx++; From ffcf078f03a39cdcdc65255bf32c1816ae3c4129 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 30 Jul 2025 15:45:38 -0500 Subject: [PATCH 294/466] Preload asteroids and debris when config-field sexps are used (#6874) --- code/asteroid/asteroid.cpp | 28 ++++++++++----------- code/asteroid/asteroid.h | 3 ++- code/parse/sexp.cpp | 50 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index 74c2c2f6f4a..8a0320537d6 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -2542,24 +2542,21 @@ int get_asteroid_index(const char* asteroid_name) return -1; } -// For FRED. Gets a list of unique asteroid subtype names -SCP_vector get_list_valid_asteroid_subtypes() +// Returns the list of unique asteroid subtype names. +// List is cached after the first call since Asteroid_info cannot change during an engine instance. +const SCP_vector& get_list_valid_asteroid_subtypes() { - SCP_vector list; - - for (const auto& this_asteroid : Asteroid_info) { - if (this_asteroid.type != ASTEROID_TYPE_DEBRIS) { - for (const auto& subtype : this_asteroid.subtypes) { - bool exists = false; - for (const auto& entry : list) { - if (subtype.type_name == entry) { - exists = true; + static SCP_vector list; + + if (list.empty()) { + for (const auto& this_asteroid : Asteroid_info) { + if (this_asteroid.type != ASTEROID_TYPE_DEBRIS) { + for (const auto& subtype : this_asteroid.subtypes) { + // Only add unique names + if (std::find(list.begin(), list.end(), subtype.type_name) == list.end()) { + list.push_back(subtype.type_name); } } - - if (!exists) { - list.push_back(subtype.type_name); - } } } } @@ -2567,6 +2564,7 @@ SCP_vector get_list_valid_asteroid_subtypes() return list; } + static void verify_asteroid_splits() { diff --git a/code/asteroid/asteroid.h b/code/asteroid/asteroid.h index 7b7faf40e59..36d5bd0961d 100644 --- a/code/asteroid/asteroid.h +++ b/code/asteroid/asteroid.h @@ -179,7 +179,8 @@ void asteroid_show_brackets(); void asteroid_target_closest_danger(); void asteroid_add_target(object* objp); int get_asteroid_index(const char* asteroid_name); -SCP_vector get_list_valid_asteroid_subtypes(); +const SCP_vector& get_list_valid_asteroid_subtypes(); +int get_asteroid_subtype_index_by_name(const SCP_string& name, int asteroid_idx); // extern for the lab void asteroid_load(int asteroid_info_index, int asteroid_subtype); diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 91565086b61..d260ef0f22c 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -4426,6 +4426,32 @@ void preload_change_ship_class(const char *text) model_page_in_textures(sip->model_num, idx); } +// MjnMixael +void preload_asteroid_class(const char* text) +{ + const auto& list = get_list_valid_asteroid_subtypes(); + + bool valid = std::any_of(list.begin(), list.end(), [&](const SCP_string& item) { return !stricmp(text, item.c_str()); }); + + if (!valid) + return; + + asteroid_load(ASTEROID_TYPE_SMALL, get_asteroid_subtype_index_by_name(text, ASTEROID_TYPE_SMALL)); + asteroid_load(ASTEROID_TYPE_MEDIUM, get_asteroid_subtype_index_by_name(text, ASTEROID_TYPE_MEDIUM)); + asteroid_load(ASTEROID_TYPE_LARGE, get_asteroid_subtype_index_by_name(text, ASTEROID_TYPE_LARGE)); + +} + +// MjnMixael +void preload_debris_class(const char* text) +{ + auto idx = get_asteroid_index(text); + if (idx < 0) + return; + + asteroid_load(idx, 0); +} + // Goober5000 void preload_turret_change_weapon(const char *text) { @@ -4846,6 +4872,30 @@ int get_sexp() do_preload_for_arguments(sexp_set_skybox_model_preload, n, arg_handler); break; + case OP_CONFIG_ASTEROID_FIELD: + // asteroid types start at argument #17 + n = CDDDDDR(start); + n = CDDDDDR(n); + n = CDDDDDR(n); + n = CDDR(n); + + // loop through all remaining arguments + for (int arg = n; arg >= 0; arg = CDR(arg)) { + do_preload_for_arguments(preload_asteroid_class, arg, arg_handler); + } + break; + + case OP_CONFIG_DEBRIS_FIELD: + // debris types start at argument #10 + n = CDDDDDR(start); + n = CDDDDDR(n); + + // loop through all remaining arguments + for (int arg = n; arg >= 0; arg = CDR(arg)) { + do_preload_for_arguments(preload_debris_class, arg, arg_handler); + } + break; + case OP_TURRET_CHANGE_WEAPON: // weapon to change to is arg #3 n = CDDDR(start); From b19c3e565b85c43a6bd30fafb82fa8418e6bae8d Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Thu, 31 Jul 2025 15:06:11 -0400 Subject: [PATCH 295/466] fix more particle issues with standalone (#6886) --- code/asteroid/asteroid.cpp | 2 +- code/scripting/api/libs/graphics.cpp | 4 ++++ code/ship/ship.cpp | 5 +++++ code/ship/shipfx.cpp | 8 +++++++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index 8a0320537d6..abb91378d4e 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -1750,7 +1750,7 @@ void asteroid_hit( object * pasteroid_obj, object * other_obj, vec3d * hitpos, f weapon_info *wip; wip = &Weapon_info[Weapons[other_obj->instance].weapon_info_index]; // If the weapon didn't play any impact animation, play custom asteroid impact animation - if (!wip->impact_weapon_expl_effect.isValid()) { + if (!wip->impact_weapon_expl_effect.isValid() && Asteroid_impact_explosion_ani.isValid()) { auto source = particle::ParticleManager::get()->createSource(Asteroid_impact_explosion_ani); source->setHost(std::make_unique(*hitpos, vmd_identity_matrix, vmd_zero_vector)); source->finishCreation(); diff --git a/code/scripting/api/libs/graphics.cpp b/code/scripting/api/libs/graphics.cpp index 249785b6b9b..bcb1af8dbb7 100644 --- a/code/scripting/api/libs/graphics.cpp +++ b/code/scripting/api/libs/graphics.cpp @@ -2255,6 +2255,10 @@ static int spawnParticles(lua_State *L, bool persistent) { // Need to consume tracer_length parameter but it isn't used anymore float temp; + if (Is_standalone) { + return persistent ? ADE_RETURN_NIL : ADE_RETURN_FALSE; + } + enum_h* type = nullptr; bool rev = false; object_h* objh = nullptr; diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 85e956799df..3049ed1053c 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -2449,6 +2449,11 @@ static ::util::UniformRange parse_ship_particle_random_range(const char particle::ParticleEffectHandle create_ship_legacy_particle_effect(LegacyShipParticleType type, float range, int bitmap, ::util::UniformFloatRange particle_num, ::util::UniformFloatRange radius, ::util::UniformFloatRange lifetime, ::util::UniformFloatRange velocity, float normal_variance, bool useNormal, float velocityInherit) { + // this is always invalid on standalone so just bail early + if (Is_standalone) { + return particle::ParticleEffectHandle::invalid(); + } + //Unfortunately legacy ship effects did a lot of ad-hoc computation of effect parameters. //To mimic this in the modern system, these ad-hoc parameters are represented as hard-coded modular curves applied to various parts of the effect std::optional part_number_curve, lifetime_curve, radius_curve, velocity_curve; diff --git a/code/ship/shipfx.cpp b/code/ship/shipfx.cpp index 0eb934b792c..3d030ed0779 100644 --- a/code/ship/shipfx.cpp +++ b/code/ship/shipfx.cpp @@ -1844,7 +1844,13 @@ static void maybe_fireball_wipe(clip_ship* half_ship, sound_handle* handle_array if ( timestamp_elapsed(half_ship->next_fireball) ) { if ( half_ship->length_left > 0.2f*fl_abs(half_ship->explosion_vel) ) { ship_info *sip = &Ship_info[Ships[half_ship->parent_obj->instance].ship_info_index]; - + + if ( !sip->split_particles.isValid() ) { + // time out forever + half_ship->next_fireball = timestamp(-1); + return; + } + polymodel* pm = model_get(sip->model_num); vec3d model_clip_plane_pt, orig_ship_world_center, temp; From bf84dbf34f8c4c620b4a45c13f7736ed04aa2bb0 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Thu, 31 Jul 2025 15:08:56 -0400 Subject: [PATCH 296/466] fix invalid array index crash in debug message (#6887) --- code/object/collideshipship.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/code/object/collideshipship.cpp b/code/object/collideshipship.cpp index 8e350d6f47e..857a70fb356 100644 --- a/code/object/collideshipship.cpp +++ b/code/object/collideshipship.cpp @@ -389,15 +389,19 @@ int ship_ship_check_collision(collision_info_struct *ship_ship_hit_info) } if ((collide_obj != NULL) && (Ship_info[Ships[collide_obj->instance].ship_info_index].is_fighter_bomber())) { const char *submode_string = ""; + const char *mode_string = ""; ai_info *aip; extern const char *Mode_text[]; aip = &Ai_info[Ships[collide_obj->instance].ai_index]; + if (aip->mode >= 0) + mode_string = Mode_text[aip->mode]; + if (aip->mode == AIM_CHASE) submode_string = Submode_text[aip->submode]; - nprintf(("AI", "Player collided with ship %s, AI mode = %s, submode = %s\n", Ships[collide_obj->instance].ship_name, Mode_text[aip->mode], submode_string)); + nprintf(("AI", "Player collided with ship %s, AI mode = %s, submode = %s\n", Ships[collide_obj->instance].ship_name, mode_string, submode_string)); } #endif } From 4c7e4feaf357409d9d5b1680ff6317aa338f3eb2 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Thu, 31 Jul 2025 15:09:30 -0400 Subject: [PATCH 297/466] fix double dumbfire missiles for multi clients (#6888) Dumbfire missiles are client fired for rollback purposes. However players would see their missiles fire twice due to the server also sending a fire packet back to the player. So we need to prevent the player that fired the missile in question from getting that extra packet. --- code/network/multimsgs.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/code/network/multimsgs.cpp b/code/network/multimsgs.cpp index 6c2e4e60953..3b4835f1272 100644 --- a/code/network/multimsgs.cpp +++ b/code/network/multimsgs.cpp @@ -3140,6 +3140,11 @@ void send_secondary_fired_packet( ship *shipp, ushort starting_sig, int /*start return; } + // if this is a dumbfire weapon then skip it since it's client fired + if ( !Weapon_info[shipp->weapons.secondary_bank_weapons[current_bank]].is_homing() ) { + return; + } + // now build up the packet to send to the player who actually fired. BUILD_HEADER( SECONDARY_FIRED_PLR ); ADD_USHORT(starting_sig); From 0a7fac447d6b9bc11cf2d993fe31ec86e23a2ced Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Fri, 1 Aug 2025 02:05:08 +0200 Subject: [PATCH 298/466] Add particle curve input for number of existing particles (#6889) * Add code to process global starts for modular curves * Add particle curve input for number of existing particles --- code/particle/ParticleEffect.h | 6 +++++ code/particle/particle.cpp | 4 ++++ code/particle/particle.h | 2 ++ code/utils/modular_curves.h | 40 +++++++++++++++++++++++++++++++++- 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/code/particle/ParticleEffect.h b/code/particle/ParticleEffect.h index 9264988642b..abc60fdced2 100644 --- a/code/particle/ParticleEffect.h +++ b/code/particle/ParticleEffect.h @@ -3,6 +3,7 @@ #pragma once #include "globalincs/pstypes.h" +#include "globalincs/systemvars.h" #include "particle/ParticleVolume.h" #include "particle/ParticleSource.h" #include "utils/RandomRange.h" @@ -228,6 +229,11 @@ class ParticleEffect { std::pair {"Effects Running", modular_curves_math_input< modular_curves_submember_input<&ParticleSource::m_effect_is_running, &decltype(ParticleSource::m_effect_is_running)::count>, modular_curves_submember_input<&ParticleSource::getEffect, &SCP_vector::size>, + ModularCurvesMathOperators::division>{}}, + std::pair {"Total Particle Count", modular_curves_global_submember_input{}}, + std::pair {"Particle Usage Score", modular_curves_math_input< + modular_curves_global_submember_input, + modular_curves_global_submember_input, ModularCurvesMathOperators::division>{}}) .derive_modular_curves_input_only_subset( //Effect Number std::pair {"Spawntime Left", modular_curves_functional_full_input<&ParticleSource::getEffectRemainingTime>{}}, diff --git a/code/particle/particle.cpp b/code/particle/particle.cpp index 3163e95fd19..65cd9bac02e 100644 --- a/code/particle/particle.cpp +++ b/code/particle/particle.cpp @@ -113,6 +113,10 @@ namespace particle Particles.clear(); } + size_t get_particle_count() { + return Particles.size() + Persistent_particles.size(); + } + void page_in() { if (!Particles_enabled) diff --git a/code/particle/particle.h b/code/particle/particle.h index 871d85370a5..5a0e8c9dc1d 100644 --- a/code/particle/particle.h +++ b/code/particle/particle.h @@ -40,6 +40,8 @@ namespace particle // kill all active particles void kill_all(); + size_t get_particle_count(); + //============================================================================ //=============== LOW-LEVEL SINGLE PARTICLE CREATION CODE ==================== diff --git a/code/utils/modular_curves.h b/code/utils/modular_curves.h index 136550aafa0..a3fec77b1a2 100644 --- a/code/utils/modular_curves.h +++ b/code/utils/modular_curves.h @@ -178,6 +178,44 @@ struct modular_curves_submember_input_full : public modular_curves_submember_inp } }; +template +struct modular_curves_global_submember_input { +protected: + template + static inline float number_to_float(const result_type& number) { + // if constexpr(std::is_same_v, fix>) // TODO: Make sure we can differentiate fixes from ints. + // return f2fl(number); + // else + if constexpr(std::is_integral_v>) + return static_cast(number); + else if constexpr(std::is_floating_point_v>) + return static_cast(number); + else { + static_assert(!std::is_same_v, "Tried to return non-numeric value"); + return 0.f; + } + } + +public: + template + static inline float grab(const input_type& /*input*/) { + if constexpr (sizeof...(grabbers) == 0) { + if constexpr (std::is_invocable_v>) { + return number_to_float(global()); + } else { + return number_to_float(global); + } + } + else { + if constexpr (std::is_invocable_v>) { + return modular_curves_submember_input::template grab<-1, decltype(global())>(global()); + } else { + return modular_curves_submember_input::template grab<-1, std::decay_t>(global); + } + } + } +}; + template struct modular_curves_functional_input { private: @@ -227,7 +265,7 @@ enum class ModularCurvesMathOperators { addition, subtraction, multiplication, - division, + division }; template From e28b3bb592b4dad1708ac4a829b62591a4c71df3 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Fri, 1 Aug 2025 03:33:29 +0200 Subject: [PATCH 299/466] Remove unused enum in particle code (#6891) * Remove unused enum * remove comment as well --- code/particle/ParticleSource.h | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/code/particle/ParticleSource.h b/code/particle/ParticleSource.h index acfade0982a..3c2af2a4693 100644 --- a/code/particle/ParticleSource.h +++ b/code/particle/ParticleSource.h @@ -18,19 +18,7 @@ struct weapon_info; enum class WeaponState: uint32_t; namespace particle { -/** - * The origin type - */ -enum class SourceOriginType { - NONE, //!< Invalid origin - VECTOR, //!< World-space offset - BEAM, //!< A beam - OBJECT, //!< An object - SUBOBJECT, //!< A subobject - TURRET, //!< A turret - PARTICLE //!< A particle -}; - + class ParticleEffect; struct particle_effect_tag { }; From b5f3edc0195d0a27cfff96f031952b1b5f16fcb9 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Fri, 1 Aug 2025 22:31:09 -0400 Subject: [PATCH 300/466] Fix engine not updating with change-ship (#6893) * Fix engine not updating with change-ship Yet another caveat of the change-ship function was discovered by Darius, who found that that both the 3D assigned engine sounds and the 2D throttle/cockpit engine sounds do not properly updated when a ship type is changed via sexp/script. This PR fixes this by properly calling the respective clearing functions and it also resets the throttle sound check timestamp to ensure the 2D sounds are indeed updated on the next frame. Tested and works as expected. Fixes #6892. * cleanup for safety --- code/hud/hud.cpp | 1 + code/ship/ship.cpp | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/code/hud/hud.cpp b/code/hud/hud.cpp index 99b4a26ab05..b6b66877d0b 100644 --- a/code/hud/hud.cpp +++ b/code/hud/hud.cpp @@ -2175,6 +2175,7 @@ void hud_stop_looped_engine_sounds() snd_stop(Player_engine_snd_loop); Player_engine_snd_loop = sound_handle::invalid(); } + throttle_sound_check_id = timestamp(THROTTLE_SOUND_CHECK_INTERVAL); } #define ZERO_PERCENT 0.01f diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 3049ed1053c..1567658fb19 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -12203,8 +12203,12 @@ void change_ship_type(int n, int ship_type, int by_sexp) animation::anim_set_initial_states(sp); //Reassign sound stuff - if (!Fred_running) + if (!Fred_running) { + if (objp == Player_obj) { + hud_stop_looped_engine_sounds(); + } ship_assign_sound(sp); + } // Valathil - Reinitialize collision checks obj_remove_collider(OBJ_INDEX(objp)); @@ -16317,6 +16321,9 @@ void ship_assign_sound(ship *sp) objp = &Objects[sp->objnum]; sip = &Ship_info[sp->ship_info_index]; + // clear out any existing assigned sounds --wookieejedi + obj_snd_delete_type(OBJ_INDEX(objp)); + // Do subsystem sounds moveup = GET_FIRST(&sp->subsys_list); while(moveup != END_OF_LIST(&sp->subsys_list)) { From dce9e1c031c382add7737ad896113268bdad6808 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Fri, 1 Aug 2025 22:31:40 -0400 Subject: [PATCH 301/466] Allow mods to cap rotational velocity from collisions (#6890) The `+Rotation Factor:` value in the ship collide section of the ships table has been around for many years, but raising that value beyond the default of 0.2 results in ship collisions that can result in the colliding ship spinning at ludicrous rotational velocities that last for over 10 seconds (this is especially evident with long ships with high speeds). This PR adds a field after `+Rotation Factor:` called `+Rotation Magnitude Maximum:` which serves as a maximum for the resulting rotational velocity from a ship collision in degrees per second. By default this is -1 and thus disabled. Tested and works as expected, and now properly allows mods to have ships spin out if they have collision while not spinning out at wild speeds that look immersion breaking. --- code/object/collideshipship.cpp | 4 ++-- code/physics/physics.cpp | 9 ++++++++- code/physics/physics.h | 2 +- code/ship/ship.cpp | 5 +++++ code/ship/ship.h | 1 + 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/code/object/collideshipship.cpp b/code/object/collideshipship.cpp index 857a70fb356..ab8eb3ff445 100644 --- a/code/object/collideshipship.cpp +++ b/code/object/collideshipship.cpp @@ -753,11 +753,11 @@ void calculate_ship_ship_collision_physics(collision_info_struct *ship_ship_hit_ if (should_collide){ vm_vec_scale(&impulse, impulse_mag); vm_vec_scale(&delta_rotvel_light, impulse_mag); - physics_collide_whack(&impulse, &delta_rotvel_light, &lighter->phys_info, &lighter->orient, ship_ship_hit_info->is_landing); + physics_collide_whack(&impulse, &delta_rotvel_light, &lighter->phys_info, &lighter->orient, ship_ship_hit_info->is_landing, light_sip->collision_physics.rotation_mag_max); vm_vec_negate(&impulse); vm_vec_scale(&delta_rotvel_heavy, -impulse_mag); - physics_collide_whack(&impulse, &delta_rotvel_heavy, &heavy->phys_info, &heavy->orient, true); + physics_collide_whack(&impulse, &delta_rotvel_heavy, &heavy->phys_info, &heavy->orient, true, heavy_sip->collision_physics.rotation_mag_max); } // If within certain bounds, we want to add some more rotation towards the "resting orientation" of the ship diff --git a/code/physics/physics.cpp b/code/physics/physics.cpp index ce2bf662463..6ef9e8db76a 100644 --- a/code/physics/physics.cpp +++ b/code/physics/physics.cpp @@ -1084,7 +1084,7 @@ void physics_apply_shock(vec3d *direction_vec, float pressure, physics_info *pi, // Warning: Do not change ROTVEL_COLLIDE_WHACK_CONST. This will mess up collision physics. // If you need to change the rotation, change COLLISION_ROTATION_FACTOR in collide_ship_ship. #define ROTVEL_COLLIDE_WHACK_CONST 1.0 -void physics_collide_whack( vec3d *impulse, vec3d *world_delta_rotvel, physics_info *pi, matrix *orient, bool is_landing ) +void physics_collide_whack( vec3d *impulse, vec3d *world_delta_rotvel, physics_info *pi, matrix *orient, bool is_landing, float max_rotvel ) { vec3d body_delta_rotvel; @@ -1096,6 +1096,13 @@ void physics_collide_whack( vec3d *impulse, vec3d *world_delta_rotvel, physics_i // vm_vec_scale( &body_delta_rotvel, (float) ROTVEL_COLLIDE_WHACK_CONST ); vm_vec_add2( &pi->rotvel, &body_delta_rotvel ); + if (max_rotvel > 0.0f) { + float rotvel_mag = vm_vec_mag(&pi->rotvel); + if (rotvel_mag > max_rotvel) { + vm_vec_scale(&pi->rotvel, max_rotvel / rotvel_mag); + } + } + pi->flags |= PF_REDUCED_DAMP; update_reduced_damp_timestamp( pi, vm_vec_mag(impulse) ); diff --git a/code/physics/physics.h b/code/physics/physics.h index b1240edf278..a67b087e953 100644 --- a/code/physics/physics.h +++ b/code/physics/physics.h @@ -164,7 +164,7 @@ extern bool whack_below_limit(float impulse); extern void physics_calculate_and_apply_whack(const vec3d *force, const vec3d *pos, physics_info *pi, const matrix *orient, const matrix *inv_moi); extern void physics_apply_whack(float orig_impulse, physics_info* pi, const vec3d *delta_rotvel, const vec3d* delta_vel, const matrix* orient); extern void physics_apply_shock(vec3d *direction_vec, float pressure, physics_info *pi, matrix *orient, vec3d *min, vec3d *max, float radius); -extern void physics_collide_whack(vec3d *impulse, vec3d *delta_rotvel, physics_info *pi, matrix *orient, bool is_landing); +extern void physics_collide_whack(vec3d *impulse, vec3d *delta_rotvel, physics_info *pi, matrix *orient, bool is_landing, float max_rotvel = -1.0f); int check_rotvel_limit( physics_info *pi ); extern void physics_add_point_mass_moi(matrix *moi, float mass, vec3d *pos); extern bool physics_lead_ballistic_trajectory(const vec3d* start, const vec3d* end_pos, const vec3d* target_vel, float weapon_speed, const vec3d* gravity, vec3d* out_direction); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 1567658fb19..13834c48316 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -1757,6 +1757,7 @@ ship_info::ship_info() collision_physics.bounce = 5.0; collision_physics.friction = COLLISION_FRICTION_FACTOR; collision_physics.rotation_factor = COLLISION_ROTATION_FACTOR; + collision_physics.rotation_mag_max = -1.0f; collision_physics.reorient_mult = 1.0f; collision_physics.landing_sound_idx = gamesnd_id(); collision_physics.collision_sound_light_idx = gamesnd_id(); @@ -3337,6 +3338,10 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool if(optional_string("+Rotation Factor:")) { stuff_float(&sip->collision_physics.rotation_factor); } + if (optional_string("+Rotation Magnitude Maximum:")) { + stuff_float(&sip->collision_physics.rotation_mag_max); + sip->collision_physics.rotation_mag_max *= PI / 180.0f; + } if(optional_string("+Landing Max Forward Vel:")) { stuff_float(&sip->collision_physics.landing_max_z); } diff --git a/code/ship/ship.h b/code/ship/ship.h index ee101922973..5cfb53c0dc9 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1082,6 +1082,7 @@ typedef struct ship_collision_physics { float bounce{}; // Bounce factor for all other cases float friction{}; // Controls lateral velocity lost when colliding with a large ship float rotation_factor{}; // Affects the rotational energy of collisions... TBH not sure how. + float rotation_mag_max{}; // Maximum value of the final rotational velocity resulting from a collision --wookieejedi // Speed & angle constraints for a smooth landing // Note that all angles are stored as a dotproduct between normalized vectors instead. This saves us from having From ab438116d3eb0194a4dd2cb1603467d29ececb43 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 1 Aug 2025 22:35:43 -0400 Subject: [PATCH 302/466] remove old os_sleep() hack for Mac (#6894) SDL 1.2 had an issue using SDL_Delay() on Mac where it caused bad stuttering and freezes. A hack was added to avoid this, but it causes 100% CPU usage when it should be a sleep state, so it wasn't ideal. With SDL 2 this issue was resolved with the underlying code being unified for all *NIX variants and that problematic Mac-specific code was removed. So now we can remove the hack to offer a little better performance on Macs. --- code/osapi/osapi.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/code/osapi/osapi.cpp b/code/osapi/osapi.cpp index b6dd9478e78..c80016a3385 100644 --- a/code/osapi/osapi.cpp +++ b/code/osapi/osapi.cpp @@ -451,16 +451,7 @@ bool os_foreground() // Sleeps for n milliseconds or until app becomes active. void os_sleep(uint ms) { -#ifdef __APPLE__ - // ewwww, I hate this!! SDL_Delay() is causing issues for us though and this - // basically matches Apple examples of the same thing. Same as SDL_Delay() but - // we aren't hitting up the system for anything during the process - uint then = SDL_GetTicks() + ms; - - while (then > SDL_GetTicks()); -#else SDL_Delay(ms); -#endif } static bool file_exists(const SCP_string& path) { From 9718e7008c0d42c7676b92e4e64e5b58e26c821d Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 1 Aug 2025 22:36:54 -0400 Subject: [PATCH 303/466] fix mipmap resizing bug (#6895) The if statement wouldn't trigger as defined which caused the incorrect data offsets to be used. This resulted in corrupted textures and invalid memory reads of uncompressed textures if the hardware texture detail slider was set to anything other than max. --- code/graphics/opengl/gropengltexture.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/graphics/opengl/gropengltexture.cpp b/code/graphics/opengl/gropengltexture.cpp index 6407df36a47..76f540f78c8 100644 --- a/code/graphics/opengl/gropengltexture.cpp +++ b/code/graphics/opengl/gropengltexture.cpp @@ -673,8 +673,10 @@ static int opengl_texture_set_level(int bitmap_handle, int bitmap_type, int bmap auto mipmap_w = tex_w; auto mipmap_h = tex_h; - // should never have mipmap levels if we also have to manually resize - if ((mipmap_levels > 1) && resize) { + // if we are doing mipmap resizing we need to account for adjusted tex size + // (we can end up with only one mipmap level if base_level is high enough so don't check it) + if (base_level > 0) { + Assertion(resize == false, "Cannot use manual and mipmap resizing at the same time!"); Assert(texmem == nullptr); // If we have mipmaps then tex_w/h are already adjusted for the base level but that will cause problems with From 671a7586e784ed3a29cf61302dc47e462e6d4c67 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Sat, 2 Aug 2025 10:10:31 -0400 Subject: [PATCH 304/466] fix nullptr crash when debris collides with ship (#6896) --- code/object/collideshipship.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/code/object/collideshipship.cpp b/code/object/collideshipship.cpp index ab8eb3ff445..5fcaf880d5f 100644 --- a/code/object/collideshipship.cpp +++ b/code/object/collideshipship.cpp @@ -751,13 +751,15 @@ void calculate_ship_ship_collision_physics(collision_info_struct *ship_ship_hit_ // physics should not have to recalculate this, just change into body coords (done in collide_whack) // Cyborg - to complicate this, multiplayer clients should never ever whack non-player ships. if (should_collide){ + auto light_rot_mag = light_sip ? light_sip->collision_physics.rotation_mag_max : -1.0f; vm_vec_scale(&impulse, impulse_mag); vm_vec_scale(&delta_rotvel_light, impulse_mag); - physics_collide_whack(&impulse, &delta_rotvel_light, &lighter->phys_info, &lighter->orient, ship_ship_hit_info->is_landing, light_sip->collision_physics.rotation_mag_max); + physics_collide_whack(&impulse, &delta_rotvel_light, &lighter->phys_info, &lighter->orient, ship_ship_hit_info->is_landing, light_rot_mag); + auto heavy_rot_mag = heavy_sip ? heavy_sip->collision_physics.rotation_mag_max : -1.0f; vm_vec_negate(&impulse); vm_vec_scale(&delta_rotvel_heavy, -impulse_mag); - physics_collide_whack(&impulse, &delta_rotvel_heavy, &heavy->phys_info, &heavy->orient, true, heavy_sip->collision_physics.rotation_mag_max); + physics_collide_whack(&impulse, &delta_rotvel_heavy, &heavy->phys_info, &heavy->orient, true, heavy_rot_mag); } // If within certain bounds, we want to add some more rotation towards the "resting orientation" of the ship From 32905ac841cbac459a059da0a908718ed3c52363 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 2 Aug 2025 16:36:27 -0500 Subject: [PATCH 305/466] still allow subsystem animations in the lab --- code/ai/aigoals.h | 2 +- code/lab/manager/lab_manager.cpp | 1 + code/ship/ship.cpp | 8 +++++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/code/ai/aigoals.h b/code/ai/aigoals.h index 12470cd6f11..dfcc82606c3 100644 --- a/code/ai/aigoals.h +++ b/code/ai/aigoals.h @@ -80,7 +80,7 @@ enum ai_goal_mode : uint8_t AI_GOAL_FLY_TO_SHIP, AI_GOAL_IGNORE_NEW, AI_GOAL_CHASE_SHIP_CLASS, - AI_GOAL_PLAY_DEAD_PERSISTENT, + AI_GOAL_PLAY_DEAD_PERSISTENT, // Disables subsystem rotation/translation among other things but there is a carveout for that in the lab only in ship_move_subsystems() AI_GOAL_LUA, AI_GOAL_DISARM_SHIP_TACTICAL, AI_GOAL_DISABLE_SHIP_TACTICAL, diff --git a/code/lab/manager/lab_manager.cpp b/code/lab/manager/lab_manager.cpp index 0b6239d6e11..300a4d97f9b 100644 --- a/code/lab/manager/lab_manager.cpp +++ b/code/lab/manager/lab_manager.cpp @@ -614,6 +614,7 @@ void LabManager::changeDisplayedObject(LabMode mode, int info_index, int subtype Player_ship = &Ships[Objects[CurrentObject].instance]; ai_paused = 0; + // Set the ship to play dead so it doesn't move. There is a special carveout to still allow subsystem rotations/translations in the lab, though ai_add_ship_goal_scripting(AI_GOAL_PLAY_DEAD_PERSISTENT, -1, 100, nullptr, &Ai_info[Player_ship->ai_index], 0, 0); } break; diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 13834c48316..32c246a7117 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -20001,9 +20001,11 @@ 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); auto shipp = &Ships[objp->instance]; - // non-player ships that are playing dead do not process subsystems or turrets - if ((!(objp->flags[Object::Object_Flags::Player_ship]) || Player_use_ai) && Ai_info[shipp->ai_index].mode == AIM_PLAY_DEAD) - return; + // non-player ships that are playing dead do not process subsystems or turrets unless we're in the lab + if (gameseq_get_state() != GS_STATE_LAB) { + if ((!(objp->flags[Object::Object_Flags::Player_ship]) || Player_use_ai) && Ai_info[shipp->ai_index].mode == AIM_PLAY_DEAD) + return; + } for (auto pss = GET_FIRST(&shipp->subsys_list); pss != END_OF_LIST(&shipp->subsys_list); pss = GET_NEXT(pss)) { From 6abfddfea5e805010bc5923098d0ee77ac2c0919 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 2 Aug 2025 18:56:06 -0500 Subject: [PATCH 306/466] show current detail level in the lab --- code/lab/manager/lab_manager.cpp | 3 +++ code/lab/renderer/lab_renderer.cpp | 8 +++++--- code/model/modelrender.cpp | 7 +++++++ code/model/modelrender.h | 2 ++ 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/code/lab/manager/lab_manager.cpp b/code/lab/manager/lab_manager.cpp index 0b6239d6e11..89f6b70f57b 100644 --- a/code/lab/manager/lab_manager.cpp +++ b/code/lab/manager/lab_manager.cpp @@ -4,6 +4,7 @@ #include "io/key.h" #include "math/staticrand.h" #include "missionui/missionscreencommon.h" +#include "model/modelrender.h" #include "object/object.h" #include "object/objectdock.h" #include "debris/debris.h" @@ -419,6 +420,8 @@ void LabManager::cleanup() { CurrentOrientation = vmd_identity_matrix; ModelFilename = ""; Player_ship = nullptr; + + Lab_object_detail_level = -1; } Cmdline_dis_collisions = Saved_cmdline_collisions_value; diff --git a/code/lab/renderer/lab_renderer.cpp b/code/lab/renderer/lab_renderer.cpp index 33f5cd70e68..c2c9d36dbad 100644 --- a/code/lab/renderer/lab_renderer.cpp +++ b/code/lab/renderer/lab_renderer.cpp @@ -7,6 +7,7 @@ #include "lab/renderer/lab_renderer.h" #include "lighting/lighting_profiles.h" #include "math/bitarray.h" +#include "model/modelrender.h" #include "nebula/neb.h" #include "parse/parselo.h" #include "particle/particle.h" @@ -32,10 +33,11 @@ void LabRenderer::onFrame(float frametime) { // print out the current pof filename, to help with... something if (strlen(getLabManager()->ModelFilename.c_str())) { - gr_get_string_size(&w, &h, getLabManager()->ModelFilename.c_str()); + SCP_string lab_text = "POF File: " + getLabManager()->ModelFilename + " Detail Level: " + std::to_string(Lab_object_detail_level); + gr_get_string_size(&w, &h, lab_text.c_str()); gr_set_color_fast(&Color_white); - gr_string(gr_screen.center_offset_x + gr_screen.center_w - w, - gr_screen.center_offset_y + gr_screen.center_h - h, getLabManager()->ModelFilename.c_str(), GR_RESIZE_NONE); + gr_string(gr_screen.center_offset_x + gr_screen.center_w - w - 20, // add a little padding to the right + gr_screen.center_offset_y + gr_screen.center_h - h, lab_text.c_str(), GR_RESIZE_NONE); } } diff --git a/code/model/modelrender.cpp b/code/model/modelrender.cpp index 87f130a1092..5cac795db25 100644 --- a/code/model/modelrender.cpp +++ b/code/model/modelrender.cpp @@ -43,6 +43,8 @@ extern float model_radius; extern bool Scene_framebuffer_in_frame; color Wireframe_color; +int Lab_object_detail_level = -1; // Used to display the detail level in the lab + extern void interp_generate_arc_segment(SCP_vector &arc_segment_points, const vec3d *v1, const vec3d *v2, ubyte depth_limit, ubyte depth); int model_render_determine_elapsed_time(int objnum, uint64_t flags); @@ -2667,6 +2669,11 @@ void model_render_queue(const model_render_params* interp, model_draw_list* scen float depth = model_render_determine_depth(objnum, model_num, orient, pos, interp->get_detail_level_lock()); int detail_level = model_render_determine_detail(depth, model_num, interp->get_detail_level_lock()); + // Send the detail level to the lab for displaying + if (gameseq_get_state() == GS_STATE_LAB) { + Lab_object_detail_level = detail_level; + } + // If we're rendering attached weapon models, check against the ships' tabled Weapon Model Draw Distance (which defaults to 200) if ( model_flags & MR_ATTACHED_MODEL && shipp != NULL ) { if (depth > Ship_info[shipp->ship_info_index].weapon_model_draw_distance) { diff --git a/code/model/modelrender.h b/code/model/modelrender.h index 66edfc28d00..c516eb7ba0a 100644 --- a/code/model/modelrender.h +++ b/code/model/modelrender.h @@ -27,6 +27,8 @@ extern vec3d Object_position; extern color Wireframe_color; +extern int Lab_object_detail_level; + typedef enum { TECH_SHIP, TECH_WEAPON, From 8748109eaff347fcbf6fdec9364a71fd021498e2 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Sun, 3 Aug 2025 20:08:25 -0400 Subject: [PATCH 307/466] cleanup and fix issues with multi client data Fix use of incorrect data macro Fix packet size safety check Fix sequence order issue with client data packet (Cyborg) Cleanup code to address readability and maintence issues --- code/network/multi_interpolate.h | 5 ++ code/network/multi_obj.cpp | 106 +++++++++++++++++++------------ 2 files changed, 70 insertions(+), 41 deletions(-) diff --git a/code/network/multi_interpolate.h b/code/network/multi_interpolate.h index e600f02fb9a..1c25614db28 100644 --- a/code/network/multi_interpolate.h +++ b/code/network/multi_interpolate.h @@ -49,6 +49,7 @@ class interpolation_manager { // we already received a newer packet than this one for that type of info. int _hull_comparison_frame; // what frame was the last hull information received? int _shields_comparison_frame; // what frame was the last shield information received? + int _client_info_comparison_frame; // what frame was the last cleint info received? SCP_vector> _subsystems_comparison_frame; // what frame was the last subsystem information received? (for each subsystem) First is health, second is animation int _ai_comparison_frame; // what frame was the last ai information received? @@ -61,6 +62,7 @@ class interpolation_manager { int get_hull_comparison_frame() { return _hull_comparison_frame; } int get_shields_comparison_frame() { return _shields_comparison_frame; } + int get_client_info_comparison_frame() { return _client_info_comparison_frame; } int get_subsystem_health_frame(int i) { @@ -86,6 +88,7 @@ class interpolation_manager { void set_hull_comparison_frame(int frame) { _hull_comparison_frame = frame; } void set_shields_comparison_frame(int frame) { _shields_comparison_frame = frame; } + void set_client_info_comparison_frame(int frame) { _client_info_comparison_frame = frame; } void set_subsystem_health_frame(int i, int frame) { @@ -120,6 +123,7 @@ class interpolation_manager { _local_skip_forward = 0; _hull_comparison_frame = -1; _shields_comparison_frame = -1; + _client_info_comparison_frame = -1; _source_player_index = -1; @@ -149,6 +153,7 @@ class interpolation_manager { _local_skip_forward = 0; _hull_comparison_frame = -1; _shields_comparison_frame = -1; + _client_info_comparison_frame = -1; _ai_comparison_frame = -1; _source_player_index = -1; } diff --git a/code/network/multi_obj.cpp b/code/network/multi_obj.cpp index 7abaeb61441..3be50296a63 100644 --- a/code/network/multi_obj.cpp +++ b/code/network/multi_obj.cpp @@ -1191,26 +1191,26 @@ int multi_oo_pack_client_data(ubyte *data, ship* shipp) // look for locked slots for (auto & lock : shipp->missile_locks) { if (lock.locked) { + // Check to see if this lock will force us over the max. + if ((packet_size + sizeof(count) + (OO_LOCK_SIZE * (count+1))) >= OO_MAX_CLIENT_DATA_SIZE) { + break; + } + lock_list.push_back(lock.obj->net_signature); // if the subsystem is a nullptr within the lock, send nullptr to the server. if (lock.subsys == nullptr) { subsystems.push_back(OOC_INDEX_NULLPTR_SUBSYSEM); } // otherwise, just send the subsystem index. else { - subsystems.push_back( (ubyte)std::distance( GET_FIRST(&Ships[lock.obj->instance].subsys_list), lock.subsys) ); + subsystems.push_back( (ushort)std::distance( GET_FIRST(&Ships[lock.obj->instance].subsys_list), lock.subsys) ); } count++; - - // Check to see if the *next* lock will force us over the max. - if (((count + 1) * OO_LOCK_SIZE + 1) >= OO_MAX_CLIENT_DATA_SIZE) { - break; - } } } // add the data we just found, in the correct order. (so the simulation will be as exact as possible) - ADD_DATA(count); + ADD_USHORT(count); for (int i = 0; i < (int)lock_list.size(); i++) { ADD_USHORT(lock_list[i]); @@ -1588,27 +1588,37 @@ int multi_oo_pack_data(net_player *pl, object *objp, ushort oo_flags, ubyte *dat } // unpack information for a client, return bytes processed -int multi_oo_unpack_client_data(net_player* pl, ubyte* data) +int multi_oo_unpack_client_data(net_player* pl, ubyte* data, bool keep_data) { - ushort in_flags; - ship* shipp = nullptr; - object* objp = nullptr; - int offset = 0; - if (pl == nullptr) Error(LOCATION, "Invalid net_player pointer passed to multi_oo_unpack_client\n"); + int offset = 0; + + // read flag info + ushort in_flags; memcpy(&in_flags, data, sizeof(ubyte)); offset++; - // get the player ship and object - if ((pl->m_player->objnum >= 0) && (Objects[pl->m_player->objnum].type == OBJ_SHIP) && (Objects[pl->m_player->objnum].instance >= 0)) { + ship* shipp = nullptr; + object* objp = nullptr; + ai_info* aip = nullptr; + + // get the player object and ship + if (pl->m_player->objnum >= 0) { objp = &Objects[pl->m_player->objnum]; + } + + if ((objp != nullptr) && (objp->type == OBJ_SHIP) && (objp->instance >= 0)) { shipp = &Ships[objp->instance]; + + if (shipp->ai_index != -1) { + aip = &Ai_info[shipp->ai_index]; + } } // if we have a valid netplayer pointer - if ((pl != nullptr) && !(pl->flags & NETINFO_FLAG_RESPAWNING) && !(pl->flags & NETINFO_FLAG_LIMBO)) { + if (keep_data && !(pl->flags & NETINFO_FLAG_RESPAWNING) && !(pl->flags & NETINFO_FLAG_LIMBO)) { // primary fired pl->m_player->ci.fire_primary_count = 0; @@ -1645,8 +1655,8 @@ int multi_oo_unpack_client_data(net_player* pl, ubyte* data) } // other locking information - if ((shipp != nullptr) && (shipp->ai_index != -1)) { - Ai_info[shipp->ai_index].ai_flags.set(AI::AI_Flags::Seek_lock, (in_flags & OOC_TARGET_SEEK_LOCK) != 0); + if (aip != nullptr) { + aip->ai_flags.set(AI::AI_Flags::Seek_lock, (in_flags & OOC_TARGET_SEEK_LOCK) != 0); } // afterburner status @@ -1658,38 +1668,43 @@ int multi_oo_unpack_client_data(net_player* pl, ubyte* data) // client targeting information ushort tnet_sig; ushort t_subsys, l_subsys; - object* tobj; // get the data GET_USHORT(tnet_sig); GET_USHORT(t_subsys); GET_USHORT(l_subsys); - // try and find the targeted object - tobj = nullptr; - if (tnet_sig != 0) { - tobj = multi_get_network_object(tnet_sig); - } - // maybe fill in targeted object values - if ((tobj != nullptr) && (pl != nullptr) && (pl->m_player->objnum != -1)) { - // assign the target object - if (Objects[pl->m_player->objnum].type == OBJ_SHIP) { - Ai_info[Ships[Objects[pl->m_player->objnum].instance].ai_index].target_objnum = OBJ_INDEX(tobj); + if (keep_data){ + // try and find the targeted object + object* tobj = nullptr; + + if (tnet_sig != 0) { + tobj = multi_get_network_object(tnet_sig); } - pl->s_info.target_objnum = OBJ_INDEX(tobj); - // assign subsystems if possible - if (Objects[pl->m_player->objnum].type == OBJ_SHIP) { - Ai_info[Ships[Objects[pl->m_player->objnum].instance].ai_index].targeted_subsys = nullptr; - if ((t_subsys != OOC_INDEX_NULLPTR_SUBSYSEM) && (tobj->type == OBJ_SHIP)) { - Ai_info[Ships[Objects[pl->m_player->objnum].instance].ai_index].targeted_subsys = ship_get_indexed_subsys(&Ships[tobj->instance], t_subsys); + // maybe fill in targeted object values + if ((tobj != nullptr) && (objp != nullptr)) { + // assign the target object + if (aip != nullptr) { + aip->target_objnum = OBJ_INDEX(tobj); } - } + pl->s_info.target_objnum = OBJ_INDEX(tobj); + + // assign subsystems if possible + if (aip != nullptr) { + aip->targeted_subsys = nullptr; + + if ((t_subsys != OOC_INDEX_NULLPTR_SUBSYSEM) && (tobj->type == OBJ_SHIP)) { + aip->targeted_subsys = ship_get_indexed_subsys(&Ships[tobj->instance], t_subsys); + } + } + + pl->m_player->locking_subsys = nullptr; - pl->m_player->locking_subsys = nullptr; - if (Objects[pl->m_player->objnum].type == OBJ_SHIP) { - if ((l_subsys != OOC_INDEX_NULLPTR_SUBSYSEM) && (tobj->type == OBJ_SHIP)) { - pl->m_player->locking_subsys = ship_get_indexed_subsys(&Ships[tobj->instance], l_subsys); + if (shipp != nullptr) { + if ((l_subsys != OOC_INDEX_NULLPTR_SUBSYSEM) && (tobj->type == OBJ_SHIP)) { + pl->m_player->locking_subsys = ship_get_indexed_subsys(&Ships[tobj->instance], l_subsys); + } } } } @@ -1700,6 +1715,11 @@ int multi_oo_unpack_client_data(net_player* pl, ubyte* data) // Get how many locks were in the packet. GET_USHORT(count); + // we can finally bail if not keeping the data here, now that we know how long this section is supposed to be. + if (!keep_data) { + offset += (count * OO_LOCK_SIZE); + return offset; + } lock_info temp_lock_info; ship_clear_lock(&temp_lock_info); @@ -1717,6 +1737,7 @@ int multi_oo_unpack_client_data(net_player* pl, ubyte* data) for (int i = 0; i < count; i++) { GET_USHORT(multilock_target_net_signature); GET_USHORT(subsystem_index); + temp_lock_info.obj = multi_get_network_object(multilock_target_net_signature); if (temp_lock_info.obj != nullptr && shipp != nullptr) { @@ -1726,9 +1747,11 @@ int multi_oo_unpack_client_data(net_player* pl, ubyte* data) } // otherwise look it up to store the lock onto the subsystem else { ship_subsys* ml_target_subsysp = GET_FIRST(&Ships[temp_lock_info.obj->instance].subsys_list); + for (int j = 0; j < subsystem_index; j++) { ml_target_subsysp = GET_NEXT(ml_target_subsysp); } + temp_lock_info.subsys = ml_target_subsysp; } // store the lock. @@ -1738,6 +1761,7 @@ int multi_oo_unpack_client_data(net_player* pl, ubyte* data) shipp->missile_locks.push_back(temp_lock_info); } } + return offset; } @@ -1816,7 +1840,7 @@ int multi_oo_unpack_data(net_player* pl, ubyte* data, int seq_num, int time_delt // if this is from a player, read his button info if(MULTIPLAYER_MASTER){ - int r0 = multi_oo_unpack_client_data(pl, data + offset); + int r0 = multi_oo_unpack_client_data(pl, data + offset, seq_num > Interp_info[objnum].get_client_info_comparison_frame()); offset += r0; } From 255f3773c10ce468f1e1f46e688704d67389c10d Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Sun, 3 Aug 2025 22:57:03 -0400 Subject: [PATCH 308/466] address clang-tidy issues --- code/network/multi_interpolate.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/network/multi_interpolate.h b/code/network/multi_interpolate.h index 1c25614db28..c24b1068441 100644 --- a/code/network/multi_interpolate.h +++ b/code/network/multi_interpolate.h @@ -60,9 +60,9 @@ class interpolation_manager { void interpolate_main(vec3d* pos, matrix* ori, physics_info* pip, vec3d* last_pos, matrix* last_orient, vec3d* gravity, bool player_ship); void reinterpolate_previous(TIMESTAMP stamp, int prev_packet_index, int next_packet_index, vec3d& position, matrix& orientation, vec3d& velocity, vec3d& rotational_velocity); - int get_hull_comparison_frame() { return _hull_comparison_frame; } - int get_shields_comparison_frame() { return _shields_comparison_frame; } - int get_client_info_comparison_frame() { return _client_info_comparison_frame; } + const int get_hull_comparison_frame() { return _hull_comparison_frame; } + const int get_shields_comparison_frame() { return _shields_comparison_frame; } + const int get_client_info_comparison_frame() { return _client_info_comparison_frame; } int get_subsystem_health_frame(int i) { @@ -84,7 +84,7 @@ class interpolation_manager { } - int get_ai_comparison_frame() { return _ai_comparison_frame; } + const int get_ai_comparison_frame() { return _ai_comparison_frame; } void set_hull_comparison_frame(int frame) { _hull_comparison_frame = frame; } void set_shields_comparison_frame(int frame) { _shields_comparison_frame = frame; } From d7a531d449891489892c9069e59c4257f756e4e2 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 4 Aug 2025 00:18:09 -0400 Subject: [PATCH 309/466] properly this time --- code/network/multi_interpolate.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/network/multi_interpolate.h b/code/network/multi_interpolate.h index c24b1068441..8870b28fc42 100644 --- a/code/network/multi_interpolate.h +++ b/code/network/multi_interpolate.h @@ -60,9 +60,9 @@ class interpolation_manager { void interpolate_main(vec3d* pos, matrix* ori, physics_info* pip, vec3d* last_pos, matrix* last_orient, vec3d* gravity, bool player_ship); void reinterpolate_previous(TIMESTAMP stamp, int prev_packet_index, int next_packet_index, vec3d& position, matrix& orientation, vec3d& velocity, vec3d& rotational_velocity); - const int get_hull_comparison_frame() { return _hull_comparison_frame; } - const int get_shields_comparison_frame() { return _shields_comparison_frame; } - const int get_client_info_comparison_frame() { return _client_info_comparison_frame; } + int get_hull_comparison_frame() const { return _hull_comparison_frame; } + int get_shields_comparison_frame() const { return _shields_comparison_frame; } + int get_client_info_comparison_frame() const { return _client_info_comparison_frame; } int get_subsystem_health_frame(int i) { @@ -84,7 +84,7 @@ class interpolation_manager { } - const int get_ai_comparison_frame() { return _ai_comparison_frame; } + int get_ai_comparison_frame() const { return _ai_comparison_frame; } void set_hull_comparison_frame(int frame) { _hull_comparison_frame = frame; } void set_shields_comparison_frame(int frame) { _shields_comparison_frame = frame; } From 4ddcaec027cf60d3ca4b057d51e5720a67afc3ef Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Mon, 4 Aug 2025 12:05:54 -0400 Subject: [PATCH 310/466] Debris Cleanup In current master the debris creation logic was really showing some rust, which resulted in inconsistent behavior when using `Lifetime Min` and `Lifetime Max` for debris in the `ships.tbl`. Specifically, by default debris can randomly be set as `DoNotExpire` if the check `(Random::next() < (Random::MAX_VALUE / 6))` is false. This was set *before* the code that set the lifetimes from `Min` and `Max` from the table though, which could randomly result in debris with infinite lifetimes even if a min and max were specified (and the ship was small, since large ships trigger infinite debris by default always). Also, by default, ships with a radius larger than 50 meters always had infinite debris lifetimes because of `MIN_RADIUS_FOR_PERSISTENT_DEBRIS`. Thus, this PR also allows modders to set that value in the game settings table. Tested and works as expected. --- code/debris/debris.cpp | 34 ++++++++++++++++++---------------- code/mod_table/mod_table.cpp | 6 ++++++ code/mod_table/mod_table.h | 1 + 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/code/debris/debris.cpp b/code/debris/debris.cpp index 11b024220c0..b0ce4df9120 100644 --- a/code/debris/debris.cpp +++ b/code/debris/debris.cpp @@ -37,7 +37,6 @@ #include "utils/Random.h" #include "weapon/weapon.h" -constexpr int MIN_RADIUS_FOR_PERSISTENT_DEBRIS = 50; // ship radius at which debris from it becomes persistant constexpr int DEBRIS_SOUND_DELAY = 2000; // time to start debris sound after created int Num_hull_pieces; // number of hull pieces in existence @@ -567,22 +566,9 @@ object *debris_create_only(int parent_objnum, int parent_ship_class, int alt_typ } // Create Debris piece n! - if ( hull_flag ) { - if (Random::next() < (Random::MAX_VALUE/6)) // Make some pieces blow up shortly after explosion. - db->lifeleft = 2.0f * (frand()) + 0.5f; - else { - db->flags.set(Debris_Flags::DoNotExpire); - db->lifeleft = -1.0f; // large hull pieces stay around forever - } - } else { - // small non-hull pieces should stick around longer the larger they are - // sqrtf should make sure its never too crazy long - db->lifeleft = (frand() * 2.0f + 0.1f) * sqrtf(radius); - } - - //WMC - Oh noes, we may need to change lifeleft if(hull_flag) { + // WMC - set lifeleft based on tabeled entries if they are not negative if(sip->debris_min_lifetime >= 0.0f && sip->debris_max_lifetime >= 0.0f) { db->lifeleft = (( sip->debris_max_lifetime - sip->debris_min_lifetime ) * frand()) + sip->debris_min_lifetime; @@ -597,6 +583,22 @@ object *debris_create_only(int parent_objnum, int parent_ship_class, int alt_typ if(db->lifeleft > sip->debris_max_lifetime) db->lifeleft = sip->debris_max_lifetime; } + // By default, make some pieces blow up shortly after explosion. + else if (Random::next() < (Random::MAX_VALUE / 6)) + { + db->lifeleft = 2.0f * (frand()) + 0.5f; + } + else + { + db->flags.set(Debris_Flags::DoNotExpire); + db->lifeleft = -1.0f; // large hull pieces stay around forever + } + } + else + { + // small non-hull pieces should stick around longer the larger they are + // sqrtf should make sure its never too crazy long + db->lifeleft = (frand() * 2.0f + 0.1f) * sqrtf(radius); } // increase lifetime for vaporized debris @@ -698,7 +700,7 @@ object *debris_create_only(int parent_objnum, int parent_ship_class, int alt_typ db->arc_timeout = TIMESTAMP::immediate(); } - if (parent_objnum >= 0 && Objects[parent_objnum].radius >= MIN_RADIUS_FOR_PERSISTENT_DEBRIS) { + if (parent_objnum >= 0 && Objects[parent_objnum].radius >= Min_radius_for_persistent_debris) { db->flags.set(Debris_Flags::DoNotExpire); } else { debris_add_to_hull_list(db); diff --git a/code/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index 20cf1a65a95..7ac76af3427 100644 --- a/code/mod_table/mod_table.cpp +++ b/code/mod_table/mod_table.cpp @@ -173,6 +173,7 @@ bool Disable_intro_movie; bool Show_locked_status_scramble_missions; bool Disable_expensive_turret_target_check; float Shield_percent_skips_damage; +float Min_radius_for_persistent_debris; #ifdef WITH_DISCORD @@ -1551,6 +1552,10 @@ void parse_mod_table(const char *filename) } } + if (optional_string("$Minimum ship radius for persistent debris:")) { + stuff_float(&Min_radius_for_persistent_debris); + } + // end of options ---------------------------------------- // if we've been through once already and are at the same place, force a move @@ -1788,6 +1793,7 @@ void mod_table_reset() Show_locked_status_scramble_missions = false; Disable_expensive_turret_target_check = false; Shield_percent_skips_damage = 0.1f; + Min_radius_for_persistent_debris = 50.0f; } void mod_table_set_version_flags() diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index 541cf51d392..463fbf9b5cd 100644 --- a/code/mod_table/mod_table.h +++ b/code/mod_table/mod_table.h @@ -188,6 +188,7 @@ extern bool Disable_intro_movie; extern bool Show_locked_status_scramble_missions; extern bool Disable_expensive_turret_target_check; extern float Shield_percent_skips_damage; +extern float Min_radius_for_persistent_debris; void mod_table_init(); void mod_table_post_process(); From e5a5c30fbf2a918a3cb193a0ff5ae19200532e17 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 4 Aug 2025 14:24:03 -0500 Subject: [PATCH 311/466] misc lab fixes --- code/ai/aicode.cpp | 8 +++-- code/ai/aigoals.h | 2 +- code/lab/dialogs/lab_ui.cpp | 56 +++++++++++++++----------------- code/lab/manager/lab_manager.cpp | 1 - code/lab/manager/lab_manager.h | 2 ++ 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index df09fee6c14..50a882e4228 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -12125,9 +12125,11 @@ void ai_process_subobjects(int objnum) bool in_lab = gameseq_get_state() == GS_STATE_LAB; - // non-player ships that are playing dead do not process subsystems or turrets - if ((!(objp->flags[Object::Object_Flags::Player_ship]) || Player_use_ai) && aip->mode == AIM_PLAY_DEAD) - return; + // non-player ships that are playing dead do not process subsystems or turrets unless we're in the lab + if (gameseq_get_state() != GS_STATE_LAB) { + if ((!(objp->flags[Object::Object_Flags::Player_ship]) || Player_use_ai) && aip->mode == AIM_PLAY_DEAD) + return; + } polymodel_instance *pmi = model_get_instance(shipp->model_instance_num); polymodel *pm = model_get(pmi->model_num); diff --git a/code/ai/aigoals.h b/code/ai/aigoals.h index dfcc82606c3..6a498b85129 100644 --- a/code/ai/aigoals.h +++ b/code/ai/aigoals.h @@ -80,7 +80,7 @@ enum ai_goal_mode : uint8_t AI_GOAL_FLY_TO_SHIP, AI_GOAL_IGNORE_NEW, AI_GOAL_CHASE_SHIP_CLASS, - AI_GOAL_PLAY_DEAD_PERSISTENT, // Disables subsystem rotation/translation among other things but there is a carveout for that in the lab only in ship_move_subsystems() + AI_GOAL_PLAY_DEAD_PERSISTENT, // Disables subsystem rotation/translation among other things but there is a carveout for that in the lab in ship_move_subsystems() and ai_process_subobjects() AI_GOAL_LUA, AI_GOAL_DISARM_SHIP_TACTICAL, AI_GOAL_DISABLE_SHIP_TACTICAL, diff --git a/code/lab/dialogs/lab_ui.cpp b/code/lab/dialogs/lab_ui.cpp index 75b51bf4193..d032b31a7b0 100644 --- a/code/lab/dialogs/lab_ui.cpp +++ b/code/lab/dialogs/lab_ui.cpp @@ -1031,35 +1031,8 @@ void LabUi::build_secondary_weapon_combobox(SCP_string& text, weapon_info* wip, void LabUi::reset_animations(ship* shipp, ship_info* sip) const { - polymodel_instance* shipp_pmi = model_get_instance(shipp->model_instance_num); - - for (auto i = 0; i < MAX_SHIP_PRIMARY_BANKS; ++i) { - if (triggered_primary_banks[i]) { - sip->animations.getAll(shipp_pmi, animation::ModelAnimationTriggerType::PrimaryBank, i) - .start(animation::ModelAnimationDirection::RWD); - triggered_primary_banks[i] = false; - } - } - - for (auto i = 0; i < MAX_SHIP_SECONDARY_BANKS; ++i) { - if (triggered_secondary_banks[i]) { - sip->animations.getAll(shipp_pmi, animation::ModelAnimationTriggerType::SecondaryBank, i) - .start(animation::ModelAnimationDirection::RWD); - triggered_secondary_banks[i] = false; - } - } - - for (auto& entry : manual_animations) { - if (entry.second) { - sip->animations.getAll(shipp_pmi, entry.first).start(animation::ModelAnimationDirection::RWD); - entry.second = false; - } - } - - for (const auto& entry : manual_animation_triggers) { - auto animation_type = entry.first; - sip->animations.getAll(shipp_pmi, animation_type).start(animation::ModelAnimationDirection::RWD); - } + // With full animation support for docking stages and fighter bays it's honestly just easier to reload the current object + getLabManager()->changeDisplayedObject(getLabManager()->CurrentMode, getLabManager()->CurrentClass, getLabManager()->CurrentSubtype); } void LabUi::maybe_show_animation_category(const SCP_vector& anim_triggers, @@ -1070,10 +1043,30 @@ void LabUi::maybe_show_animation_category(const SCP_vector Date: Mon, 4 Aug 2025 14:55:11 -0500 Subject: [PATCH 312/466] default case --- code/lab/dialogs/lab_ui.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/code/lab/dialogs/lab_ui.cpp b/code/lab/dialogs/lab_ui.cpp index d032b31a7b0..fcccf45aa10 100644 --- a/code/lab/dialogs/lab_ui.cpp +++ b/code/lab/dialogs/lab_ui.cpp @@ -1064,6 +1064,11 @@ void LabUi::maybe_show_animation_category(const SCP_vector(trigger_type)); + button_label += "Trigger Animation " + std::to_string(count++); + break; } if (Button(button_label.c_str())) { From 3f0c33046c6f75fbb38138d418166ca1131a60c6 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 4 Aug 2025 15:17:28 -0500 Subject: [PATCH 313/466] unused params --- code/lab/dialogs/lab_ui.cpp | 4 ++-- code/lab/dialogs/lab_ui.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/lab/dialogs/lab_ui.cpp b/code/lab/dialogs/lab_ui.cpp index fcccf45aa10..2d5030b3dc7 100644 --- a/code/lab/dialogs/lab_ui.cpp +++ b/code/lab/dialogs/lab_ui.cpp @@ -1029,7 +1029,7 @@ void LabUi::build_secondary_weapon_combobox(SCP_string& text, weapon_info* wip, } } -void LabUi::reset_animations(ship* shipp, ship_info* sip) const +void LabUi::reset_animations() { // With full animation support for docking stages and fighter bays it's honestly just easier to reload the current object getLabManager()->changeDisplayedObject(getLabManager()->CurrentMode, getLabManager()->CurrentClass, getLabManager()->CurrentSubtype); @@ -1093,7 +1093,7 @@ void LabUi::build_animation_options(ship* shipp, ship_info* sip) const const auto& anim_triggers = sip->animations.getRegisteredTriggers(); if (Button("Reset animations")) { - reset_animations(shipp, sip); + reset_animations(); } if (shipp->weapons.num_primary_banks > 0) { diff --git a/code/lab/dialogs/lab_ui.h b/code/lab/dialogs/lab_ui.h index 79aaf2f2313..888baf7b232 100644 --- a/code/lab/dialogs/lab_ui.h +++ b/code/lab/dialogs/lab_ui.h @@ -60,7 +60,7 @@ class LabUi { ship_info* sip) const; void build_model_info_box_actual(ship_info* sip, polymodel* pm) const; void build_team_color_combobox() const; - void reset_animations(ship* shipp, ship_info* sip) const; + static void reset_animations(); void do_triggered_anim(animation::ModelAnimationTriggerType type, const SCP_string& name, bool direction, From 32183f5e71af61b3329530188cedfa8c906eb7b8 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Mon, 4 Aug 2025 18:43:06 -0400 Subject: [PATCH 314/466] update comments --- code/debris/debris.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/code/debris/debris.cpp b/code/debris/debris.cpp index b0ce4df9120..d3c883bb546 100644 --- a/code/debris/debris.cpp +++ b/code/debris/debris.cpp @@ -568,7 +568,8 @@ object *debris_create_only(int parent_objnum, int parent_ship_class, int alt_typ // Create Debris piece n! if(hull_flag) { - // WMC - set lifeleft based on tabeled entries if they are not negative + // set lifeleft based on tabled entries if they are not negative + // coded originally by WMC then clean-up added by wookieejedi if(sip->debris_min_lifetime >= 0.0f && sip->debris_max_lifetime >= 0.0f) { db->lifeleft = (( sip->debris_max_lifetime - sip->debris_min_lifetime ) * frand()) + sip->debris_min_lifetime; From 6132f8bfc13fdbc06af2eb8081b1e38357289b8a Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Tue, 5 Aug 2025 16:25:16 -0400 Subject: [PATCH 315/466] Another particle fix for standalones (#6905) * Another particle fix for standalones Fix more standalone crashes from particles by "allowing the code to create particles, and just silently quit if in standalone. I.e., accept an invalid effect handle if in standalone and that'll just quit out the source on its first frame of processing." Thanks to @Bmagnu for the fix! Putting here to allow taylor and chief to more easily test. * add needed include * and another fix --- code/particle/ParticleSource.cpp | 3 +++ code/particle/ParticleSource.h | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/code/particle/ParticleSource.cpp b/code/particle/ParticleSource.cpp index a5d92ff9ca1..ac98171317e 100644 --- a/code/particle/ParticleSource.cpp +++ b/code/particle/ParticleSource.cpp @@ -29,6 +29,9 @@ bool ParticleSource::isValid() const { } void ParticleSource::finishCreation() { + if (Is_standalone) + return; + m_host->setupProcessing(); for (const auto& effect : ParticleManager::get()->getEffect(m_effect)) { diff --git a/code/particle/ParticleSource.h b/code/particle/ParticleSource.h index 3c2af2a4693..cde17a1dcca 100644 --- a/code/particle/ParticleSource.h +++ b/code/particle/ParticleSource.h @@ -3,6 +3,7 @@ #pragma once #include "globalincs/pstypes.h" +#include "globalincs/systemvars.h" #include "object/object.h" #include "particle/particle.h" #include "io/timer.h" @@ -88,7 +89,7 @@ class ParticleSource { const SCP_vector& getEffect() const; inline void setEffect(ParticleEffectHandle eff) { - Assert(eff.isValid()); + Assert(eff.isValid() || Is_standalone); m_effect = eff; } From b8c3c560d280d308ee8848ef32cb15ed0be8169c Mon Sep 17 00:00:00 2001 From: LuytenKy Date: Thu, 7 Aug 2025 16:56:18 +0300 Subject: [PATCH 316/466] Implementation for an AI order to chase ship types -Second Attempt- (#6828) * Back in business hopefully * Refactor * Resolve CI issue in aigoals.cpp * Updating order.cpp * Whoops * Updated objecttypes.tbl to incorporate attack ship type * Fix clang-tidy issue * Forgot one other instance --------- Co-authored-by: wookieejedi --- code/ai/ai.h | 8 +-- code/ai/aicode.cpp | 54 ++++++++++++-------- code/ai/aigoals.cpp | 57 +++++++++++++++++++--- code/ai/aigoals.h | 3 +- code/ai/aiturret.cpp | 8 +-- code/def_files/data/tables/objecttypes.tbl | 8 +-- code/parse/sexp.cpp | 21 ++++++++ code/parse/sexp.h | 3 +- code/scripting/api/objs/enums.cpp | 1 + code/scripting/api/objs/enums.h | 1 + code/scripting/api/objs/order.cpp | 33 ++++++++++++- code/scripting/api/objs/ship.cpp | 16 +++++- code/scripting/api/objs/shipclass.h | 2 +- code/ship/ship.cpp | 7 ++- fred2/fredview.cpp | 1 + fred2/management.cpp | 1 + fred2/missionsave.cpp | 4 ++ fred2/shipgoalsdlg.cpp | 30 +++++++++++- qtfred/src/mission/missionsave.cpp | 4 ++ 19 files changed, 216 insertions(+), 46 deletions(-) diff --git a/code/ai/ai.h b/code/ai/ai.h index 944602beb3e..505ca30bfc4 100644 --- a/code/ai/ai.h +++ b/code/ai/ai.h @@ -527,7 +527,7 @@ const char *ai_get_goal_target_name(const char *name, int *index); void ai_clear_goal_target_names(); extern void init_ai_system(void); -extern void ai_attack_object(object *attacker, object *attacked, int ship_info_index = -1); +extern void ai_attack_object(object *attacker, object *attacked, int ship_info_index = -1, int class_type = -1); extern void ai_evade_object(object *evader, object *evaded); extern void ai_ignore_object(object *ignorer, object *ignored, int ignore_new); extern void ai_ignore_wing(object *ignorer, int wingnum); @@ -559,7 +559,7 @@ extern void ai_set_guard_wing(object *objp, int wingnum); extern void ai_warp_out(object *objp, vec3d *vp); extern void ai_attack_wing(object *attacker, int wingnum); extern void ai_deathroll_start(object *ship_obj); -extern int set_target_objnum(ai_info *aip, int objnum); +extern int set_target_objnum(ai_info* aip, int objnum); extern void ai_form_on_wing(object *objp, object *goal_objp); extern void ai_do_stay_near(object *objp, object *other_obj, float dist, int additional_data); extern ship_subsys *set_targeted_subsys(ai_info *aip, ship_subsys *new_subsys, int parent_objnum); @@ -596,7 +596,7 @@ extern int ai_maybe_fire_afterburner(object *objp, ai_info *aip); extern void set_predicted_enemy_pos(vec3d *predicted_enemy_pos, object *pobjp, vec3d *enemy_pos, vec3d *enemy_vel, ai_info *aip); extern int is_instructor(object *objp); -extern int find_enemy(int objnum, float range, int max_attackers, int ship_info_index = -1); +extern int find_enemy(int objnum, float range, int max_attackers, int ship_info_index = -1, int class_type = -1); float ai_get_weapon_speed(const ship_weapon *swp); void set_predicted_enemy_pos_turret(vec3d *predicted_enemy_pos, const vec3d *gun_pos, const object *pobjp, const vec3d *enemy_pos, const vec3d *enemy_vel, float weapon_speed, float time_enemy_in_range); @@ -617,7 +617,7 @@ extern float dock_orient_and_approach(object *docker_objp, int docker_index, obj void ai_set_mode_warp_out(object *objp, ai_info *aip); // prototyped by Goober5000 -int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index); +int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index, int class_type = -1); // moved to header file by Goober5000 void ai_announce_ship_dying(object *dying_objp); diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index 50a882e4228..1d8796114e9 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -2229,6 +2229,7 @@ typedef struct eval_nearest_objnum { object *trial_objp; int enemy_team_mask; int enemy_ship_info_index; + int enemy_class_type; int enemy_wing; float range; int max_attackers; @@ -2262,6 +2263,10 @@ void evaluate_object_as_nearest_objnum(eval_nearest_objnum *eno) if ((eno->enemy_ship_info_index >= 0) && (shipp->ship_info_index != eno->enemy_ship_info_index)) return; + // If only supposed to attack ships of a certain ship type, don't attack other ships. + if ((eno->enemy_class_type >= 0) && (Ship_info[shipp->ship_info_index].class_type != eno->enemy_class_type)) + return; + // Don't keep firing at a ship that is in its death throes. if (shipp->flags[Ship::Ship_Flags::Dying]) return; @@ -2350,8 +2355,9 @@ void evaluate_object_as_nearest_objnum(eval_nearest_objnum *eno) * @param range Ship must be within range "range". * @param max_attackers Don't attack a ship that already has at least max_attackers attacking it. * @param ship_info_index If >=0, the enemy object must be of the specified ship class + * @param class_type If >=0, the enemy object must be of the specified ship type */ -int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index) +int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float range, int max_attackers, int ship_info_index, int class_type) { object *danger_weapon_objp; ai_info *aip; @@ -2361,6 +2367,7 @@ int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float ra eval_nearest_objnum eno; eno.enemy_team_mask = enemy_team_mask; eno.enemy_ship_info_index = ship_info_index; + eno.enemy_class_type = class_type; eno.enemy_wing = enemy_wing; eno.max_attackers = max_attackers; eno.objnum = objnum; @@ -2406,7 +2413,7 @@ int get_nearest_objnum(int objnum, int enemy_team_mask, int enemy_wing, float ra // If only looking for target in certain wing and couldn't find anything in // that wing, look for any object. if ((eno.nearest_objnum == -1) && (enemy_wing != -1)) { - return get_nearest_objnum(objnum, enemy_team_mask, -1, range, max_attackers, ship_info_index); + return get_nearest_objnum(objnum, enemy_team_mask, -1, range, max_attackers, ship_info_index, class_type); } return eno.nearest_objnum; @@ -2510,13 +2517,15 @@ int get_enemy_timestamp() /** * Return objnum if enemy found, else return -1; * - * @param objnum Object number - * @param range Range within which to look - * @param max_attackers Don't attack a ship that already has at least max_attackers attacking it. + * @param objnum Object number + * @param range Range within which to look + * @param max_attackers Don't attack a ship that already has at least max_attackers attacking it. + * @param ship_info_index If specified, restrict the search to enemies with this ship class + * @param class_type If specified, restrict the search to enemies with this ship type */ -int find_enemy(int objnum, float range, int max_attackers, int ship_info_index) +int find_enemy(int objnum, float range, int max_attackers, int ship_info_index, int class_type) { - int enemy_team_mask; + int enemy_team_mask; if (objnum < 0) return -1; @@ -2524,19 +2533,25 @@ int find_enemy(int objnum, float range, int max_attackers, int ship_info_index) enemy_team_mask = iff_get_attackee_mask(obj_team(&Objects[objnum])); // if target_objnum != -1, use that as goal. - ai_info *aip = &Ai_info[Ships[Objects[objnum].instance].ai_index]; + ai_info* aip = &Ai_info[Ships[Objects[objnum].instance].ai_index]; if (timestamp_elapsed(aip->choose_enemy_timestamp)) { aip->choose_enemy_timestamp = timestamp(get_enemy_timestamp()); + if (aip->target_objnum != -1) { - int target_objnum = aip->target_objnum; + int target_objnum = aip->target_objnum; // DKA don't undo object as target in nebula missions. - // This could cause attack on ship on fringe on nebula to stop if attackee moves our of nebula range. (BAD) - if ( Objects[target_objnum].signature == aip->target_signature ) { - if (iff_matches_mask(Ships[Objects[target_objnum].instance].team, enemy_team_mask)) { - if (ship_info_index < 0 || ship_info_index == Ships[Objects[target_objnum].instance].ship_info_index) { - if (!(Objects[target_objnum].flags[Object::Object_Flags::Protected])) { - return target_objnum; + // This could cause attack on ship on fringe on nebula to stop if attackee moves out of nebula range. (BAD) + if (Objects[target_objnum].signature == aip->target_signature) { + ship* target_shipp = (Objects[target_objnum].type == OBJ_SHIP) ? &Ships[Objects[target_objnum].instance] : nullptr; + + if (target_shipp && iff_matches_mask(target_shipp->team, enemy_team_mask)) { + if (ship_info_index < 0 || ship_info_index == target_shipp->ship_info_index) { + if (class_type < 0 || (target_shipp->ship_info_index >= 0 && + class_type == Ship_info[target_shipp->ship_info_index].class_type)) { + if (!(Objects[target_objnum].flags[Object::Object_Flags::Protected])) { + return target_objnum; + } } } } @@ -2545,9 +2560,8 @@ int find_enemy(int objnum, float range, int max_attackers, int ship_info_index) aip->target_signature = -1; } } - - return get_nearest_objnum(objnum, enemy_team_mask, aip->enemy_wing, range, max_attackers, ship_info_index); - + + return get_nearest_objnum(objnum, enemy_team_mask, aip->enemy_wing, range, max_attackers, ship_info_index, class_type); } else { aip->target_objnum = -1; aip->target_signature = -1; @@ -2588,7 +2602,7 @@ void force_avoid_player_check(object *objp, ai_info *aip) * If attacked == NULL, then attack any enemy object. * Attack point *rel_pos on object. This is for supporting attacking subsystems. */ -void ai_attack_object(object* attacker, object* attacked, int ship_info_index) +void ai_attack_object(object* attacker, object* attacked, int ship_info_index, int class_type) { int temp; ai_info* aip; @@ -2621,7 +2635,7 @@ void ai_attack_object(object* attacker, object* attacked, int ship_info_index) if (attacked == nullptr) { aip->choose_enemy_timestamp = timestamp(0); // nebula safe - set_target_objnum(aip, find_enemy(OBJ_INDEX(attacker), 99999.9f, 4, ship_info_index)); + set_target_objnum(aip, find_enemy(OBJ_INDEX(attacker), 99999.9f, 4, ship_info_index, class_type)); } else { // check if we can see attacked in nebula if (aip->target_objnum != OBJ_INDEX(attacked)) { diff --git a/code/ai/aigoals.cpp b/code/ai/aigoals.cpp index a7119da408c..eb6eb3f0757 100644 --- a/code/ai/aigoals.cpp +++ b/code/ai/aigoals.cpp @@ -100,6 +100,7 @@ ai_goal_list Ai_goal_names[] = { "Attack weapon", AI_GOAL_CHASE_WEAPON, 0 }, { "Fly to ship", AI_GOAL_FLY_TO_SHIP, 0 }, { "Attack ship class", AI_GOAL_CHASE_SHIP_CLASS, 0 }, + { "Attack ship type", AI_GOAL_CHASE_SHIP_TYPE, 0 }, }; int Num_ai_goals = sizeof(Ai_goal_names) / sizeof(ai_goal_list); @@ -114,6 +115,7 @@ const char *Ai_goal_text(ai_goal_mode goal, int submode) case AI_GOAL_CHASE: case AI_GOAL_CHASE_WING: case AI_GOAL_CHASE_SHIP_CLASS: + case AI_GOAL_CHASE_SHIP_TYPE: return XSTR( "attack ", 474); case AI_GOAL_DOCK: return XSTR( "dock ", 475); @@ -513,13 +515,25 @@ void ai_goal_purge_invalid_goals( ai_goal *aigp, ai_goal *goal_list, ai_info *ai if ( purge_goal->target_name == NULL ) continue; - // goals operating on ship classes are handled slightly differently + // goals operating on ship classes and ship types are handled slightly differently if ( purge_ai_mode == AI_GOAL_CHASE_SHIP_CLASS ) { // if the target of the purge goal is the same class of ship we are concerned about, then we have a match; // if it is not, then we can continue (see standard ship check below) if ( stricmp(purge_goal->target_name, Ship_info[Ships[ship_index].ship_info_index].name) != 0 ) continue; } + else if (purge_ai_mode == AI_GOAL_CHASE_SHIP_TYPE) { + // Get the ship type of the ship we're concerned about + int ship_type = Ship_info[Ships[ship_index].ship_info_index].class_type; + + // If the ship type is invalid, we can't match it, so continue + if (ship_type < 0) + continue; + + // Check if the target name of the purge goal matches the ship type name + if (stricmp(purge_goal->target_name, Ship_types[ship_type].name) != 0) + continue; + } // standard goals operating on either wings or ships else { // determine if the purge goal is acting either on the ship or the ship's wing. @@ -1094,6 +1108,7 @@ void ai_add_goal_sub_sexp( int sexp, ai_goal_type type, ai_info *aip, ai_goal *a case OP_AI_CHASE: case OP_AI_CHASE_WING: case OP_AI_CHASE_SHIP_CLASS: + case OP_AI_CHASE_SHIP_TYPE: case OP_AI_GUARD: case OP_AI_GUARD_WING: case OP_AI_EVADE_SHIP: @@ -1125,6 +1140,8 @@ void ai_add_goal_sub_sexp( int sexp, ai_goal_type type, ai_info *aip, ai_goal *a aigp->ai_mode = AI_GOAL_CHASE_WING; } else if (op == OP_AI_CHASE_SHIP_CLASS) { aigp->ai_mode = AI_GOAL_CHASE_SHIP_CLASS; + } else if (op == OP_AI_CHASE_SHIP_TYPE) { + aigp->ai_mode = AI_GOAL_CHASE_SHIP_TYPE; } else if ( op == OP_AI_IGNORE ) { aigp->ai_mode = AI_GOAL_IGNORE; } else if ( op == OP_AI_IGNORE_NEW ) { @@ -1169,7 +1186,7 @@ void ai_add_goal_sub_sexp( int sexp, ai_goal_type type, ai_info *aip, ai_goal *a } // Goober5000 - we now have an extra optional chase argument to allow chasing our own team - if ( op == OP_AI_CHASE || op == OP_AI_CHASE_WING || op == OP_AI_CHASE_SHIP_CLASS + if (op == OP_AI_CHASE || op == OP_AI_CHASE_WING || op == OP_AI_CHASE_SHIP_CLASS || op == OP_AI_CHASE_SHIP_TYPE || op == OP_AI_DISABLE_SHIP || op == OP_AI_DISABLE_SHIP_TACTICAL || op == OP_AI_DISARM_SHIP || op == OP_AI_DISARM_SHIP_TACTICAL ) { if (is_sexp_true(CDDDR(node))) aigp->flags.set(AI::Goal_Flags::Target_own_team); @@ -1194,6 +1211,7 @@ void ai_add_goal_sub_sexp( int sexp, ai_goal_type type, ai_info *aip, ai_goal *a if (op == OP_AI_CHASE || op == OP_AI_CHASE_WING || op == OP_AI_CHASE_SHIP_CLASS || + op == OP_AI_CHASE_SHIP_TYPE || op == OP_AI_DISABLE_SHIP || op == OP_AI_DISABLE_SHIP_TACTICAL || op == OP_AI_DISARM_SHIP || @@ -1380,6 +1398,10 @@ int ai_remove_goal_sexp_sub( int sexp, ai_goal* aigp, bool &remove_more ) priority = eval_priority_et_seq(CDDR(node)); goalmode = AI_GOAL_CHASE_SHIP_CLASS; break; + case OP_AI_CHASE_SHIP_TYPE: + priority = eval_priority_et_seq(CDDR(node)); + goalmode = AI_GOAL_CHASE_SHIP_TYPE; + break; case OP_AI_EVADE_SHIP: priority = eval_priority_et_seq(CDDR(node)); goalmode = AI_GOAL_EVADE_SHIP; @@ -1689,7 +1711,20 @@ ai_achievability ai_mission_goal_achievable( int objnum, ai_goal *aigp ) } return ai_achievability::NOT_KNOWN; } - + // and similarly for chasing all ships of a certain ship type + if (aigp->ai_mode == AI_GOAL_CHASE_SHIP_TYPE) { + for (auto so : list_range(&Ship_obj_list)) { + auto type_objp = &Objects[so->objnum]; + if (type_objp->type != OBJ_SHIP || type_objp->flags[Object::Object_Flags::Should_be_dead]) + continue; + int ship_info_idx = Ships[type_objp->instance].ship_info_index; + int class_type = Ship_info[ship_info_idx].class_type; + if (class_type >= 0 && !strcmp(aigp->target_name, Ship_types[class_type].name)) { + return ai_achievability::ACHIEVABLE; + } + } + return ai_achievability::NOT_KNOWN; + } return_val = ai_achievability::SATISFIED; @@ -2523,10 +2558,20 @@ void ai_process_mission_orders( int objnum, ai_info *aip ) // chase-ship-class is chase-any but restricted to a subset of ships case AI_GOAL_CHASE_SHIP_CLASS: - shipnum = ship_info_lookup( current_goal->target_name ); - Assertion( shipnum >= 0, "The target of AI_GOAL_CHASE_SHIP_CLASS must refer to a valid ship class!" ); - ai_attack_object( objp, nullptr, shipnum ); + { + int ship_info_index = ship_info_lookup(current_goal->target_name); + Assertion(ship_info_index >= 0, "The target of AI_GOAL_CHASE_SHIP_CLASS must refer to a valid ship class!"); + ai_attack_object(objp, nullptr, ship_info_index); break; + } + // similarly for chase-ship-type + case AI_GOAL_CHASE_SHIP_TYPE: + { + int class_type = ship_type_name_lookup(current_goal->target_name); + Assertion(class_type >= 0, "The target of AI_GOAL_CHASE_SHIP_TYPE must refer to a valid ship type!"); + ai_attack_object(objp, nullptr, -1, class_type); + break; + } case AI_GOAL_WARP: { mission_do_departure( objp, true ); diff --git a/code/ai/aigoals.h b/code/ai/aigoals.h index 6a498b85129..d929e727d35 100644 --- a/code/ai/aigoals.h +++ b/code/ai/aigoals.h @@ -80,6 +80,7 @@ enum ai_goal_mode : uint8_t AI_GOAL_FLY_TO_SHIP, AI_GOAL_IGNORE_NEW, AI_GOAL_CHASE_SHIP_CLASS, + AI_GOAL_CHASE_SHIP_TYPE, AI_GOAL_PLAY_DEAD_PERSISTENT, // Disables subsystem rotation/translation among other things but there is a carveout for that in the lab in ship_move_subsystems() and ai_process_subobjects() AI_GOAL_LUA, AI_GOAL_DISARM_SHIP_TACTICAL, @@ -103,7 +104,7 @@ inline bool ai_goal_is_disable_or_disarm(ai_goal_mode ai_mode) } inline bool ai_goal_is_specific_chase(ai_goal_mode ai_mode) { - return ai_mode == AI_GOAL_CHASE || ai_mode == AI_GOAL_CHASE_WING || ai_mode == AI_GOAL_CHASE_SHIP_CLASS; + return ai_mode == AI_GOAL_CHASE || ai_mode == AI_GOAL_CHASE_WING || ai_mode == AI_GOAL_CHASE_SHIP_CLASS || ai_mode == AI_GOAL_CHASE_SHIP_TYPE; } enum class ai_achievability { ACHIEVABLE, NOT_ACHIEVABLE, NOT_KNOWN, SATISFIED }; diff --git a/code/ai/aiturret.cpp b/code/ai/aiturret.cpp index f949ec56e71..e34e102168a 100644 --- a/code/ai/aiturret.cpp +++ b/code/ai/aiturret.cpp @@ -1089,9 +1089,11 @@ int find_turret_enemy(const ship_subsys *turret_subsys, int objnum, const vec3d int target_objnum = aip->target_objnum; if (Objects[target_objnum].signature == aip->target_signature) { - if (iff_matches_mask(Ships[Objects[target_objnum].instance].team, enemy_team_mask)) { - if ( !(Objects[target_objnum].flags[Object::Object_Flags::Protected]) ) { // check this flag as well - // nprintf(("AI", "Frame %i: Object %i resuming goal of object %i\n", AI_FrameCount, objnum, target_objnum)); + ship* target_shipp = &Ships[Objects[target_objnum].instance]; + if (target_shipp && iff_matches_mask(target_shipp->team, enemy_team_mask)) { + if (!(Objects[target_objnum].flags[Object::Object_Flags::Protected])) { // check this flag as well + // nprintf(("AI", "Frame %i: Object %i resuming goal of object %i\n", AI_FrameCount, objnum, + // target_objnum)); return target_objnum; } } diff --git a/code/def_files/data/tables/objecttypes.tbl b/code/def_files/data/tables/objecttypes.tbl index ef73d9355b0..261ff0eb018 100644 --- a/code/def_files/data/tables/objecttypes.tbl +++ b/code/def_files/data/tables/objecttypes.tbl @@ -110,9 +110,9 @@ $Fog: +Start dist: 10.0 +Compl dist: 500.0 $AI: - +Valid goals: ( "fly to ship" "attack ship" "waypoints" "waypoints once" "depart" "attack subsys" "attack wing" "guard ship" "disable ship" "disable ship (tactical)" "disarm ship" "disarm ship (tactical)" "attack any" "attack ship class" "ignore ship" "ignore ship (new)" "guard wing" "evade ship" "stay still" "play dead" "play dead (persistent)" "stay near ship" "keep safe dist" "form on wing") + +Valid goals: ( "fly to ship" "attack ship" "waypoints" "waypoints once" "depart" "attack subsys" "attack wing" "guard ship" "disable ship" "disable ship (tactical)" "disarm ship" "disarm ship (tactical)" "attack any" "attack ship class" "attack ship type" "ignore ship" "ignore ship (new)" "guard wing" "evade ship" "stay still" "play dead" "play dead (persistent)" "stay near ship" "keep safe dist" "form on wing") +Accept Player Orders: YES - +Player Orders: ( "attack ship" "disable ship" "disable ship (tactical)" "disarm ship" "disarm ship (tactical)" "guard ship" "ignore ship" "ignore ship (new)" "form on wing" "cover me" "attack any" "attack ship class" "depart" "disable subsys" ) + +Player Orders: ( "attack ship" "disable ship" "disable ship (tactical)" "disarm ship" "disarm ship (tactical)" "guard ship" "ignore ship" "ignore ship (new)" "form on wing" "cover me" "attack any" "attack ship class" "attack ship type" "depart" "disable subsys" ) +Auto attacks: YES +Actively Pursues: ( "navbuoy" "sentry gun" "escape pod" "cargo" "support" "fighter" "bomber" "transport" "freighter" "awacs" "gas miner" "cruiser" "corvette" "capital" "super cap" "drydock" "knossos device" ) +Guards attack this: YES @@ -138,9 +138,9 @@ $Fog: +Start dist: 10.0 +Compl dist: 500.0 $AI: - +Valid goals: ( "fly to ship" "attack ship" "waypoints" "waypoints once" "depart" "attack subsys" "attack wing" "guard ship" "disable ship" "disable ship (tactical)" "disarm ship" "disarm ship (tactical)" "attack any" "attack ship class" "ignore ship" "ignore ship (new)" "guard wing" "evade ship" "stay still" "play dead" "play dead (persistent)" "stay near ship" "keep safe dist" "form on wing" ) + +Valid goals: ( "fly to ship" "attack ship" "waypoints" "waypoints once" "depart" "attack subsys" "attack wing" "guard ship" "disable ship" "disable ship (tactical)" "disarm ship" "disarm ship (tactical)" "attack any" "attack ship class" "attack ship type" "ignore ship" "ignore ship (new)" "guard wing" "evade ship" "stay still" "play dead" "play dead (persistent)" "stay near ship" "keep safe dist" "form on wing" ) +Accept Player Orders: YES - +Player Orders: ( "attack ship" "disable ship" "disable ship (tactical)" "disarm ship" "disarm ship (tactical)" "guard ship" "ignore ship" "ignore ship (new)" "form on wing" "cover me" "attack any" "attack ship class" "depart" "disable subsys" ) + +Player Orders: ( "attack ship" "disable ship" "disable ship (tactical)" "disarm ship" "disarm ship (tactical)" "guard ship" "ignore ship" "ignore ship (new)" "form on wing" "cover me" "attack any" "attack ship class" "attack ship type" "depart" "disable subsys" ) +Auto attacks: YES +Actively Pursues: ( "navbuoy" "sentry gun" "escape pod" "cargo" "support" "fighter" "bomber" "transport" "freighter" "awacs" "gas miner" "cruiser" "corvette" "capital" "super cap" "drydock" "knossos device" ) +Guards attack this: YES diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index d260ef0f22c..7725f591e2c 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -849,6 +849,7 @@ SCP_vector Operators = { { "ai-chase", OP_AI_CHASE, 2, 4, SEXP_GOAL_OPERATOR, }, { "ai-chase-wing", OP_AI_CHASE_WING, 2, 4, SEXP_GOAL_OPERATOR, }, { "ai-chase-ship-class", OP_AI_CHASE_SHIP_CLASS, 2, 4, SEXP_GOAL_OPERATOR, }, + { "ai-chase-ship-type", OP_AI_CHASE_SHIP_TYPE, 2, 4, SEXP_GOAL_OPERATOR, }, // LuytenKy { "ai-chase-any", OP_AI_CHASE_ANY, 1, 2, SEXP_GOAL_OPERATOR, }, { "ai-guard", OP_AI_GUARD, 2, 3, SEXP_GOAL_OPERATOR, }, { "ai-guard-wing", OP_AI_GUARD_WING, 2, 3, SEXP_GOAL_OPERATOR, }, @@ -909,6 +910,7 @@ sexp_ai_goal_link Sexp_ai_goal_links[] = { { AI_GOAL_CHASE, OP_AI_CHASE }, { AI_GOAL_CHASE_WING, OP_AI_CHASE_WING }, { AI_GOAL_CHASE_SHIP_CLASS, OP_AI_CHASE_SHIP_CLASS }, + { AI_GOAL_CHASE_SHIP_TYPE, OP_AI_CHASE_SHIP_TYPE}, { AI_GOAL_CHASE_ANY, OP_AI_CHASE_ANY }, { AI_GOAL_DOCK, OP_AI_DOCK }, { AI_GOAL_UNDOCK, OP_AI_UNDOCK }, @@ -31610,6 +31612,7 @@ int query_operator_return_type(int op) case OP_AI_CHASE: case OP_AI_CHASE_WING: case OP_AI_CHASE_SHIP_CLASS: + case OP_AI_CHASE_SHIP_TYPE: case OP_AI_CHASE_ANY: case OP_AI_DOCK: case OP_AI_UNDOCK: @@ -32627,6 +32630,14 @@ int query_operator_argument_type(int op, int argnum) else return OPF_BOOL; + case OP_AI_CHASE_SHIP_TYPE: + if (argnum == 0) + return OPF_SHIP_TYPE; + else if (argnum == 1) + return OPF_POSITIVE; + else + return OPF_BOOL; + case OP_AI_GUARD: if (argnum == 0) return OPF_SHIP_WING; @@ -36792,6 +36803,7 @@ int get_category(int op_id) case OP_AI_IGNORE_NEW: case OP_AI_FORM_ON_WING: case OP_AI_CHASE_SHIP_CLASS: + case OP_AI_CHASE_SHIP_TYPE: case OP_AI_PLAY_DEAD_PERSISTENT: case OP_AI_FLY_TO_SHIP: case OP_AI_REARM_REPAIR: @@ -39275,6 +39287,15 @@ SCP_vector Sexp_help = { "\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false." }, + { OP_AI_CHASE_SHIP_TYPE, "Ai-chase ship type (Ship goal)\r\n" + "\tCauses the specified ship to chase and attack a target ship type.\r\n\r\n" + "Takes 2 to 4 arguments...\r\n" + "\t1:\tName of ship type to chase.\r\n" + "\t2:\tGoal priority (number between 0 and 200. Player orders have a priority of 90-100).\r\n" + "\t3 (optional):\tWhether to attack the target even if it is on the same team; defaults to false.\r\n" + "\t4 (optional):\tWhether to afterburn as hard as possible to the target; defaults to false." + }, + { OP_AI_CHASE_ANY, "Ai-chase-any (Ship goal)\r\n" "\tCauses the specified ship to chase and attack any ship on the opposite team.\r\n\r\n" "Takes 1 or 2 arguments...\r\n" diff --git a/code/parse/sexp.h b/code/parse/sexp.h index e50ca926dcd..b82ed7a47bd 100644 --- a/code/parse/sexp.h +++ b/code/parse/sexp.h @@ -950,7 +950,8 @@ enum : int { OP_AI_PLAY_DEAD, OP_AI_IGNORE_NEW, // Goober5000 OP_AI_FORM_ON_WING, // The E - OP_AI_CHASE_SHIP_CLASS, // Goober5000 + OP_AI_CHASE_SHIP_CLASS, // Goober5000 + OP_AI_CHASE_SHIP_TYPE, // LuytenKy OP_AI_PLAY_DEAD_PERSISTENT, // Goober5000 OP_AI_FLY_TO_SHIP, // Goober5000 OP_AI_REARM_REPAIR, // Goober5000 diff --git a/code/scripting/api/objs/enums.cpp b/code/scripting/api/objs/enums.cpp index b000aa451e8..47ad9ce659c 100644 --- a/code/scripting/api/objs/enums.cpp +++ b/code/scripting/api/objs/enums.cpp @@ -28,6 +28,7 @@ const lua_enum_def_list Enumerations[] = { {"ORDER_ATTACK", LE_ORDER_ATTACK, true}, {"ORDER_ATTACK_WING", LE_ORDER_ATTACK_WING, true}, {"ORDER_ATTACK_SHIP_CLASS", LE_ORDER_ATTACK_SHIP_CLASS, true}, + {"ORDER_ATTACK_SHIP_TYPE", LE_ORDER_ATTACK_SHIP_TYPE, true}, {"ORDER_ATTACK_ANY", LE_ORDER_ATTACK_ANY, true}, {"ORDER_DEPART", LE_ORDER_DEPART, true}, {"ORDER_DISABLE", LE_ORDER_DISABLE, true}, diff --git a/code/scripting/api/objs/enums.h b/code/scripting/api/objs/enums.h index 39cc1487fc6..08895afd540 100644 --- a/code/scripting/api/objs/enums.h +++ b/code/scripting/api/objs/enums.h @@ -27,6 +27,7 @@ enum lua_enum : int32_t { LE_ORDER_ATTACK, LE_ORDER_ATTACK_WING, LE_ORDER_ATTACK_SHIP_CLASS, + LE_ORDER_ATTACK_SHIP_TYPE, LE_ORDER_ATTACK_ANY, LE_ORDER_DEPART, LE_ORDER_DISABLE, diff --git a/code/scripting/api/objs/order.cpp b/code/scripting/api/objs/order.cpp index 4a0d127a7dd..a2f96b2d035 100644 --- a/code/scripting/api/objs/order.cpp +++ b/code/scripting/api/objs/order.cpp @@ -174,6 +174,9 @@ ADE_FUNC(getType, l_Order, NULL, "Gets the type of the order.", "enumeration", " case AI_GOAL_CHASE_SHIP_CLASS: eh_idx = LE_ORDER_ATTACK_SHIP_CLASS; break; + case AI_GOAL_CHASE_SHIP_TYPE: + eh_idx = LE_ORDER_ATTACK_SHIP_TYPE; + break; case AI_GOAL_LUA: eh_idx = LE_ORDER_LUA; break; @@ -275,6 +278,33 @@ ADE_VIRTVAR(Target, l_Order, "object", "Target of the order. Value may also be a } break; + case AI_GOAL_CHASE_SHIP_TYPE: + { + if (newh->objp()->type == OBJ_SHIP) { + int info_idx = Ships[newh->objp()->instance].ship_info_index; + int type_index = Ship_info[info_idx].class_type; + + const char* type_name; + + if (type_index < 0) { + type_name = ""; + } else { + type_name = Ship_types[type_index].name; + } + + if (stricmp(type_name, ohp->aigp->target_name) != 0) { + ohp->aigp->target_name = ai_get_goal_target_name(type_name, &ohp->aigp->target_name_index); + ohp->aigp->time = (ohp->odx == 0) ? Missiontime : 0; + + if (ohp->odx == 0) { + aip->ok_to_target_timestamp = timestamp(0); + set_target_objnum(aip, newh->objnum); + } + } + } + } + break; + case AI_GOAL_WAYPOINTS: case AI_GOAL_WAYPOINTS_ONCE: if (newh->objp()->type == OBJ_WAYPOINT) { @@ -343,7 +373,8 @@ ADE_VIRTVAR(Target, l_Order, "object", "Target of the order. Value may also be a break; case AI_GOAL_CHASE_SHIP_CLASS: - // a ship class isn't an in-mission object + case AI_GOAL_CHASE_SHIP_TYPE: + // a ship class/type isn't an in-mission object return ade_set_args(L, "o", l_Object.Set(object_h())); case AI_GOAL_WAYPOINTS: diff --git a/code/scripting/api/objs/ship.cpp b/code/scripting/api/objs/ship.cpp index cf2ef4fdd76..dbea8871766 100644 --- a/code/scripting/api/objs/ship.cpp +++ b/code/scripting/api/objs/ship.cpp @@ -13,6 +13,7 @@ #include "ship.h" #include "ship_bank.h" #include "shipclass.h" +#include "shiptype.h" #include "subsystem.h" #include "team.h" #include "team_colors.h" @@ -1646,15 +1647,16 @@ ADE_FUNC(clearOrders, l_Ship, NULL, "Clears a ship's orders list", "boolean", "T return ADE_RETURN_TRUE; } -ADE_FUNC(giveOrder, l_Ship, "enumeration Order, [object Target=nil, subsystem TargetSubsystem=nil, number Priority=1.0, shipclass TargetShipclass=nil]", "Uses the goal code to execute orders", "boolean", "True if order was given, otherwise false or nil") +ADE_FUNC(giveOrder, l_Ship, "enumeration Order, [object Target=nil, subsystem TargetSubsystem=nil, number Priority=1.0, shipclass TargetShipclass=nil, shiptype TargetShiptype=nil]", "Uses the goal code to execute orders", "boolean", "True if order was given, otherwise false or nil") { object_h *objh = NULL; enum_h *eh = NULL; float priority = 1.0f; int sclass = -1; + int stype = -1; object_h *tgh = NULL; ship_subsys_h *tgsh = NULL; - if(!ade_get_args(L, "oo|oofo", l_Object.GetPtr(&objh), l_Enum.GetPtr(&eh), l_Object.GetPtr(&tgh), l_Subsystem.GetPtr(&tgsh), &priority, l_Shipclass.Get(&sclass))) + if(!ade_get_args(L, "oo|oofoo", l_Object.GetPtr(&objh), l_Enum.GetPtr(&eh), l_Object.GetPtr(&tgh), l_Subsystem.GetPtr(&tgsh), &priority, l_Shipclass.Get(&sclass), l_Shiptype.Get(&stype))) return ADE_RETURN_NIL; if(!objh->isValid() || !eh->isValid()) @@ -1902,6 +1904,16 @@ ADE_FUNC(giveOrder, l_Ship, "enumeration Order, [object Target=nil, subsystem Ta } break; } + case LE_ORDER_ATTACK_SHIP_TYPE: + { + if (stype >= 0) + { + ai_mode = AI_GOAL_CHASE_SHIP_TYPE; + ai_shipname = Ship_types[stype].name; + ai_submode = SM_ATTACK; + } + break; + } default: return ade_set_error(L, "b", false); } diff --git a/code/scripting/api/objs/shipclass.h b/code/scripting/api/objs/shipclass.h index 0db741b5768..7b9012c8a88 100644 --- a/code/scripting/api/objs/shipclass.h +++ b/code/scripting/api/objs/shipclass.h @@ -8,4 +8,4 @@ namespace api { DECLARE_ADE_OBJ(l_Shipclass, int); } -} +} \ No newline at end of file diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 32c246a7117..84cd0994aee 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -17432,6 +17432,7 @@ static const char* ship_get_ai_target_display_name(int goal, const char* name) // These goals need no special handling case AI_GOAL_CHASE_WING: case AI_GOAL_CHASE_SHIP_CLASS: + case AI_GOAL_CHASE_SHIP_TYPE: case AI_GOAL_GUARD_WING: case AI_GOAL_WAYPOINTS: case AI_GOAL_WAYPOINTS_ONCE: @@ -17465,7 +17466,7 @@ SCP_string ship_return_orders(ship* sp) auto order_text = Ai_goal_text(aigp->ai_mode, aigp->ai_submode); if (order_text == nullptr) - return SCP_string(); + return {}; SCP_string outbuf = order_text; @@ -17489,6 +17490,7 @@ SCP_string ship_return_orders(ship* sp) break; case AI_GOAL_CHASE_SHIP_CLASS: + case AI_GOAL_CHASE_SHIP_TYPE: if (aigp->target_name) { outbuf += XSTR("any ", -1); outbuf += target_name; @@ -17497,6 +17499,7 @@ SCP_string ship_return_orders(ship* sp) } break; + case AI_GOAL_CHASE: case AI_GOAL_DOCK: case AI_GOAL_UNDOCK: @@ -17534,7 +17537,7 @@ SCP_string ship_return_orders(ship* sp) break; default: - return SCP_string(); + return {}; } return outbuf; diff --git a/fred2/fredview.cpp b/fred2/fredview.cpp index c3ca517c733..8249e1ade3c 100644 --- a/fred2/fredview.cpp +++ b/fred2/fredview.cpp @@ -3484,6 +3484,7 @@ char *error_check_initial_orders(ai_goal *goals, int ship, int wing) case AI_GOAL_NONE: case AI_GOAL_CHASE_ANY: case AI_GOAL_CHASE_SHIP_CLASS: + case AI_GOAL_CHASE_SHIP_TYPE: case AI_GOAL_UNDOCK: case AI_GOAL_KEEP_SAFE_DISTANCE: case AI_GOAL_PLAY_DEAD: diff --git a/fred2/management.cpp b/fred2/management.cpp index b1c8b62e978..cfb3ac233b0 100644 --- a/fred2/management.cpp +++ b/fred2/management.cpp @@ -126,6 +126,7 @@ ai_goal_list Ai_goal_list[] = { { "Attack", AI_GOAL_CHASE_WING, 0 }, // duplicate needed because we can no longer use bitwise operators { "Attack any ship", AI_GOAL_CHASE_ANY, 0 }, { "Attack ship class", AI_GOAL_CHASE_SHIP_CLASS, 0 }, + { "Attack ship type", AI_GOAL_CHASE_SHIP_TYPE, 0 }, { "Guard", AI_GOAL_GUARD, 0 }, { "Guard", AI_GOAL_GUARD_WING, 0 }, // duplicate needed because we can no longer use bitwise operators { "Disable ship", AI_GOAL_DISABLE_SHIP, 0 }, diff --git a/fred2/missionsave.cpp b/fred2/missionsave.cpp index 242d2e04591..e664b15e124 100644 --- a/fred2/missionsave.cpp +++ b/fred2/missionsave.cpp @@ -666,6 +666,10 @@ void CFred_mission_save::save_ai_goals(ai_goal *goalp, int ship) str = "ai-chase-ship-class"; break; + case AI_GOAL_CHASE_SHIP_TYPE: + str = "ai-chase-ship-type"; + break; + case AI_GOAL_GUARD: str = "ai-guard"; break; diff --git a/fred2/shipgoalsdlg.cpp b/fred2/shipgoalsdlg.cpp index 00a6c4ca2e7..401f598a93c 100644 --- a/fred2/shipgoalsdlg.cpp +++ b/fred2/shipgoalsdlg.cpp @@ -25,6 +25,7 @@ #define TYPE_WING 0x4000 #define TYPE_WAYPOINT 0x5000 #define TYPE_SHIP_CLASS 0x6000 +#define TYPE_SHIP_TYPE 0x7000 #define TYPE_MASK 0xf000 #define DATA_MASK 0x0fff @@ -508,6 +509,10 @@ void ShipGoalsDlg::initialize(ai_goal *goals, int ship) flag = 16; // target is a ship class break; + case AI_GOAL_CHASE_SHIP_TYPE: + flag = 32; + break; + case AI_GOAL_STAY_STILL: flag = 9; // target is a ship or a waypoint break; @@ -618,6 +623,15 @@ void ShipGoalsDlg::initialize(ai_goal *goals, int ship) } } + if (flag & 0x20) { // data is a ship type + for (i = 0; i (Ship_types.size()); i++) { + if (!stricmp(goalp[item].target_name, Ship_types[i].name)) { + m_data[item] = i | TYPE_SHIP_TYPE; + break; + } + } + } + switch (mode) { case AI_GOAL_DOCK: m_dock2[item] = -1; @@ -706,7 +720,7 @@ void ShipGoalsDlg::set_item(int item, int init) break; } - // for goals that deal with ship classes + // for goals that deal with ship classes and types switch (mode) { case AI_GOAL_CHASE_SHIP_CLASS: for (i = 0; i < ship_info_size(); i++) { @@ -716,6 +730,15 @@ void ShipGoalsDlg::set_item(int item, int init) m_object[item] = z; } break; + + case AI_GOAL_CHASE_SHIP_TYPE: + for (i = 0; i < static_cast(Ship_types.size()); i++) { + z = m_object_box[item] -> AddString(Ship_types[i].name); + m_object_box[item] -> SetItemData(z, i | TYPE_SHIP_TYPE); + if (init && (m_data[item] == (i | TYPE_SHIP_TYPE))) + m_object[item] = z; + } + break; } // for goals that deal with individual ships @@ -1016,6 +1039,7 @@ void ShipGoalsDlg::update_item(int item, int multi) case AI_GOAL_STAY_NEAR_SHIP: case AI_GOAL_STAY_STILL: case AI_GOAL_CHASE_SHIP_CLASS: + case AI_GOAL_CHASE_SHIP_TYPE: break; case AI_GOAL_DESTROY_SUBSYSTEM: @@ -1144,6 +1168,10 @@ void ShipGoalsDlg::update_item(int item, int multi) goalp[item].target_name = ai_get_goal_target_name(Ship_info[m_data[item] & DATA_MASK].name, ¬_used); break; + case TYPE_SHIP_TYPE: + goalp[item].target_name = ai_get_goal_target_name(Ship_types[m_data[item] & DATA_MASK].name, ¬_used); + break; + case 0: case -1: case (-1 & TYPE_MASK): diff --git a/qtfred/src/mission/missionsave.cpp b/qtfred/src/mission/missionsave.cpp index 5df81680e8b..25a7acb9164 100644 --- a/qtfred/src/mission/missionsave.cpp +++ b/qtfred/src/mission/missionsave.cpp @@ -678,6 +678,10 @@ void CFred_mission_save::save_ai_goals(ai_goal* goalp, int ship) str = "ai-chase-ship-class"; break; + case AI_GOAL_CHASE_SHIP_TYPE: + str = "ai-chase-ship-type"; + break; + case AI_GOAL_GUARD: str = "ai-guard"; break; From 75b61efbd1444b4f3bb032069b939626523f7749 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 7 Aug 2025 08:56:52 -0500 Subject: [PATCH 317/466] Custom Data for Campaigns (#6906) * campaign custom data * Assertion instead of assert --- code/mission/missioncampaign.cpp | 5 +++++ code/mission/missioncampaign.h | 1 + code/scripting/api/libs/mission.cpp | 23 +++++++++++++++++++++++ fred2/campaigneditordlg.cpp | 11 +++++++++++ fred2/campaigneditordlg.h | 1 + fred2/customdatadlg.cpp | 13 ++++++++----- fred2/customdatadlg.h | 3 ++- fred2/fred.rc | 1 + fred2/missionnotesdlg.cpp | 2 +- fred2/missionsave.cpp | 18 ++++++++++++++++++ qtfred/src/mission/missionsave.cpp | 18 ++++++++++++++++++ 11 files changed, 89 insertions(+), 7 deletions(-) diff --git a/code/mission/missioncampaign.cpp b/code/mission/missioncampaign.cpp index f52c494ed98..e754f2b2105 100644 --- a/code/mission/missioncampaign.cpp +++ b/code/mission/missioncampaign.cpp @@ -518,6 +518,10 @@ int mission_campaign_load(const char* filename, const char* full_path, player* p stuff_int( &(Campaign.flags) ); } + if (optional_string("$begin_custom_data_map")) { + parse_string_map(Campaign.custom_data, "$end_custom_data_map", "+Val:"); + } + // parse the optional ship/weapon information mission_campaign_get_sw_info(); @@ -1259,6 +1263,7 @@ void mission_campaign_clear() memset(Campaign.name, 0, NAME_LENGTH); memset(Campaign.filename, 0, MAX_FILENAME_LEN); Campaign.type = 0; + Campaign.custom_data.clear(); Campaign.flags = CF_DEFAULT_VALUE; Campaign.num_missions = 0; Campaign.num_missions_completed = 0; diff --git a/code/mission/missioncampaign.h b/code/mission/missioncampaign.h index a4b779ab70e..a31cbed2e7a 100644 --- a/code/mission/missioncampaign.h +++ b/code/mission/missioncampaign.h @@ -136,6 +136,7 @@ class campaign SCP_vector red_alert_variables; // state of the variables in the previous mission of a Red Alert scenario. SCP_vector persistent_containers; // These containers will be saved at the end of a mission SCP_vector red_alert_containers; // state of the containers in the previous mission of a Red Alert scenario. + SCP_map custom_data; // Custom data for the campaign campaign() : desc(nullptr), num_missions(0) diff --git a/code/scripting/api/libs/mission.cpp b/code/scripting/api/libs/mission.cpp index 0e1812973fe..dc89fb9a190 100644 --- a/code/scripting/api/libs/mission.cpp +++ b/code/scripting/api/libs/mission.cpp @@ -3035,6 +3035,29 @@ ADE_FUNC(jumpToMission, l_Campaign, "string filename, [boolean hub]", "Jumps to return ade_set_args(L, "b", success); } +ADE_VIRTVAR(CustomData, l_Campaign, nullptr, "Gets the custom data table for this campaign", "table", "The campaign's custom data table") +{ + if (ADE_SETTING_VAR) { + LuaError(L, "Setting Custom Data is not supported"); + } + + auto table = luacpp::LuaTable::create(L); + + for (const auto& pair : Campaign.custom_data) + { + table.addValue(pair.first, pair.second); + } + + return ade_set_args(L, "t", &table); +} + +ADE_FUNC(hasCustomData, l_Campaign, nullptr, "Detects whether the campaign has any custom data", "boolean", "true if the campaign's custom_data is not empty, false otherwise") +{ + + bool result = !Campaign.custom_data.empty(); + return ade_set_args(L, "b", result); +} + // TODO: add a proper indexer type that returns a handle // something like ca.Mission[filename/index] diff --git a/fred2/campaigneditordlg.cpp b/fred2/campaigneditordlg.cpp index fa93c985669..82abdc96de3 100644 --- a/fred2/campaigneditordlg.cpp +++ b/fred2/campaigneditordlg.cpp @@ -103,6 +103,7 @@ BEGIN_MESSAGE_MAP(campaign_editor, CFormView) ON_EN_CHANGE(IDC_SUBSTITUTE_MAIN_HALL, OnChangeSubstituteMainHall) ON_EN_CHANGE(IDC_DEBRIEFING_PERSONA, OnChangeDebriefingPersona) ON_BN_CLICKED(IDC_CUSTOM_TECH_DB, OnCustomTechDB) + ON_BN_CLICKED(IDC_OPEN_CUSTOM_DATA, OnCustomData) //}}AFX_MSG_MAP END_MESSAGE_MAP() @@ -951,6 +952,16 @@ void campaign_editor::OnCustomTechDB() Campaign.flags &= ~CF_CUSTOM_TECH_DATABASE; } +void campaign_editor::OnCustomData() +{ + UpdateData(TRUE); + + CustomDataDlg dlg(&Campaign.custom_data, this); + dlg.DoModal(); + + UpdateData(FALSE); +} + CString campaign_editor::GetPathWithoutFile() const { if (m_current_campaign_path.IsEmpty()) diff --git a/fred2/campaigneditordlg.h b/fred2/campaigneditordlg.h index adb39ca53da..859c051172c 100644 --- a/fred2/campaigneditordlg.h +++ b/fred2/campaigneditordlg.h @@ -115,6 +115,7 @@ class campaign_editor : public CFormView afx_msg void OnChangeSubstituteMainHall(); afx_msg void OnChangeDebriefingPersona(); afx_msg void OnCustomTechDB(); + afx_msg void OnCustomData(); //}}AFX_MSG DECLARE_MESSAGE_MAP() }; diff --git a/fred2/customdatadlg.cpp b/fred2/customdatadlg.cpp index e16da546fa7..ef13625a783 100644 --- a/fred2/customdatadlg.cpp +++ b/fred2/customdatadlg.cpp @@ -22,8 +22,8 @@ static char THIS_FILE[] = __FILE__; ///////////////////////////////////////////////////////////////////////////// // CustomDataDlg dialog -CustomDataDlg::CustomDataDlg(CWnd* pParent /*=nullptr*/) - : CDialog(CustomDataDlg::IDD, pParent) +CustomDataDlg::CustomDataDlg(SCP_map* data_ptr, CWnd* pParent /*=nullptr*/) + : CDialog(CustomDataDlg::IDD, pParent), m_target_data(data_ptr) { } @@ -65,7 +65,8 @@ BOOL CustomDataDlg::OnInitDialog() CDialog::OnInitDialog(); // grab the existing list of custom data pairs and duplicate it. We only update it if the user clicks OK. - m_custom_data = The_mission.custom_data; + Assertion(m_target_data != nullptr, "Custom Data target is nullptr. Get a coder!"); + m_custom_data = *m_target_data; update_data_lister(); @@ -79,7 +80,8 @@ BOOL CustomDataDlg::OnInitDialog() void CustomDataDlg::OnButtonOk() { // now we set the custom data to our copy - The_mission.custom_data = m_custom_data; + Assertion(m_target_data != nullptr, "Custom Data target is nullptr. Get a coder!"); + *m_target_data = m_custom_data; CDialog::OnOK(); } @@ -314,5 +316,6 @@ void CustomDataDlg::update_help_text(const SCP_string& description) bool CustomDataDlg::query_modified() const { - return The_mission.custom_data != m_custom_data; + Assertion(m_target_data != nullptr, "Custom Data target is nullptr. Get a coder!"); + return *m_target_data != m_custom_data; } diff --git a/fred2/customdatadlg.h b/fred2/customdatadlg.h index 374411d9227..8377f56e041 100644 --- a/fred2/customdatadlg.h +++ b/fred2/customdatadlg.h @@ -11,7 +11,7 @@ class CustomDataDlg : public CDialog { public: - CustomDataDlg(CWnd* pParent = nullptr); + CustomDataDlg(SCP_map* data_ptr, CWnd* pParent = nullptr); enum { IDD = IDD_EDIT_CUSTOM_DATA @@ -55,6 +55,7 @@ class CustomDataDlg : public CDialog { private: bool query_modified() const; + SCP_map* m_target_data = nullptr; SCP_map m_custom_data; // read-only view of data pair keys diff --git a/fred2/fred.rc b/fred2/fred.rc index 3b49fc3c700..dec78972be5 100644 --- a/fred2/fred.rc +++ b/fred2/fred.rc @@ -1760,6 +1760,7 @@ BEGIN LTEXT "Substitute",IDC_STATIC | UDS_ALIGNRIGHT,14,207,58,8 EDITTEXT IDC_SUBSTITUTE_MAIN_HALL,73,205,66,12,ES_AUTOHSCROLL LTEXT "Branches",IDC_STATIC,7,229,31,8 + PUSHBUTTON "Custom Data",IDC_OPEN_CUSTOM_DATA,181,203,71,15 CONTROL "Tree1",IDC_SEXP_TREE,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT | TVS_EDITLABELS | TVS_SHOWSELALWAYS | WS_BORDER | WS_HSCROLL | WS_TABSTOP,7,239,188,111,WS_EX_CLIENTEDGE GROUPBOX "Branch Options",IDC_STATIC,202,229,61,64 PUSHBUTTON "Move Up",IDC_MOVE_UP,206,239,53,14,BS_CENTER,WS_EX_STATICEDGE diff --git a/fred2/missionnotesdlg.cpp b/fred2/missionnotesdlg.cpp index cacea70cff4..4531c5ebfe3 100644 --- a/fred2/missionnotesdlg.cpp +++ b/fred2/missionnotesdlg.cpp @@ -725,7 +725,7 @@ void CMissionNotesDlg::OnCustomData() { UpdateData(TRUE); - CustomDataDlg dlg; + CustomDataDlg dlg(&The_mission.custom_data, this); dlg.DoModal(); UpdateData(FALSE); diff --git a/fred2/missionsave.cpp b/fred2/missionsave.cpp index e664b15e124..eaedf1ba06a 100644 --- a/fred2/missionsave.cpp +++ b/fred2/missionsave.cpp @@ -1354,6 +1354,24 @@ int CFred_mission_save::save_campaign_file(const char *pathname) fout(" %d\n", Campaign.flags); } + if (Mission_save_format != FSO_FORMAT_RETAIL && !Campaign.custom_data.empty()) { + if (optional_string_fred("$begin_custom_data_map")) { + parse_comments(2); + } else { + fout("\n$begin_custom_data_map"); + } + + for (const auto& pair : Campaign.custom_data) { + fout("\n +Val: %s %s", pair.first.c_str(), pair.second.c_str()); + } + + if (optional_string_fred("$end_custom_data_map")) { + parse_comments(); + } else { + fout("\n$end_custom_data_map"); + } + } + // write out the ships and weapons which the player can start the campaign with optional_string_fred("+Starting Ships: ("); parse_comments(2); diff --git a/qtfred/src/mission/missionsave.cpp b/qtfred/src/mission/missionsave.cpp index 25a7acb9164..d52fd0a07e8 100644 --- a/qtfred/src/mission/missionsave.cpp +++ b/qtfred/src/mission/missionsave.cpp @@ -2351,6 +2351,24 @@ int CFred_mission_save::save_campaign_file(const char *pathname) fout(" %d\n", Campaign.flags); } + if (save_format != MissionFormat::RETAIL && !Campaign.custom_data.empty()) { + if (optional_string_fred("$begin_custom_data_map")) { + parse_comments(2); + } else { + fout("\n$begin_custom_data_map"); + } + + for (const auto& pair : Campaign.custom_data) { + fout("\n +Val: %s %s", pair.first.c_str(), pair.second.c_str()); + } + + if (optional_string_fred("$end_custom_data_map")) { + parse_comments(); + } else { + fout("\n$end_custom_data_map"); + } + } + // write out the ships and weapons which the player can start the campaign with optional_string_fred("+Starting Ships: ("); parse_comments(2); From 5cd3b3a3087e54b5301bd585118ceea4c8e3683e Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Fri, 8 Aug 2025 04:29:59 -0700 Subject: [PATCH 318/466] Extend particle interpolation to lifetime (#6910) * the naive method * the correct method --- code/particle/ParticleEffect.cpp | 6 +++++- code/particle/particle.cpp | 2 +- code/particle/particle.h | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/code/particle/ParticleEffect.cpp b/code/particle/ParticleEffect.cpp index 5ec86e96f53..6e7d84a5014 100644 --- a/code/particle/ParticleEffect.cpp +++ b/code/particle/ParticleEffect.cpp @@ -339,12 +339,16 @@ auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& s info.length = m_length.next() * lengthMultiplier; if (m_hasLifetime) { if (m_parentLifetime) - // if we were spawned by a particle, parentLifetime is the parent's remaining liftime and m_lifetime is a factor of that + // if we were spawned by a particle, parentLifetime is the parent's remaining lifetime and m_lifetime is a factor of that info.lifetime = parentLifetime * m_lifetime.next() * lifetimeMultiplier; else info.lifetime = m_lifetime.next() * lifetimeMultiplier; + info.lifetime_from_animation = m_keep_anim_length_if_available; } + + info.starting_age = interp * f2fl(Frametime); + info.size_lifetime_curve = m_size_lifetime_curve; info.vel_lifetime_curve = m_vel_lifetime_curve; diff --git a/code/particle/particle.cpp b/code/particle/particle.cpp index 65cd9bac02e..0a91a0e273c 100644 --- a/code/particle/particle.cpp +++ b/code/particle/particle.cpp @@ -159,7 +159,7 @@ namespace particle part->pos = info->pos; part->velocity = info->vel; - part->age = 0.0f; + part->age = info->starting_age; part->max_life = info->lifetime; part->radius = info->rad; part->bitmap = info->bitmap; diff --git a/code/particle/particle.h b/code/particle/particle.h index 5a0e8c9dc1d..13ff2a5094d 100644 --- a/code/particle/particle.h +++ b/code/particle/particle.h @@ -62,6 +62,7 @@ namespace particle vec3d pos = vmd_zero_vector; vec3d vel = vmd_zero_vector; float lifetime = -1.0f; + float starting_age = 0.0f; float rad = -1.0f; int bitmap = -1; int nframes = -1; From 3900faac87ab118082ec52f7d217955ca284083f Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 8 Aug 2025 06:30:37 -0500 Subject: [PATCH 319/466] Fix issue where QtFred could have multiple Ship Dialogs open (#6909) --- qtfred/src/ui/FredView.cpp | 15 ++++++++++++--- qtfred/src/ui/FredView.h | 6 ++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 5fab2028cb6..17dff870125 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -743,9 +743,18 @@ void FredView::on_actionWaypoint_Paths_triggered(bool) { } void FredView::on_actionShips_triggered(bool) { - auto editorDialog = new dialogs::ShipEditorDialog(this, _viewport); - editorDialog->setAttribute(Qt::WA_DeleteOnClose); - editorDialog->show(); + if (!_shipEditorDialog) { + _shipEditorDialog = new dialogs::ShipEditorDialog(this, _viewport); + _shipEditorDialog->setAttribute(Qt::WA_DeleteOnClose); + // When the user closes it, reset our pointer so we can open a new one later + connect(_shipEditorDialog, &QObject::destroyed, this, [this]() { + _shipEditorDialog = nullptr; + }); + _shipEditorDialog->show(); + } else { + _shipEditorDialog->raise(); + _shipEditorDialog->activateWindow(); + } } void FredView::on_actionCampaign_triggered(bool) { diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index efee90e75cc..463f188cb66 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -19,6 +19,10 @@ namespace fred { class Editor; class RenderWidget; +namespace dialogs { +class ShipEditorDialog; +} + namespace Ui { class FredView; } @@ -206,6 +210,8 @@ class FredView: public QMainWindow, public IDialogProvider { Editor* fred = nullptr; EditorViewport* _viewport = nullptr; + fso::fred::dialogs::ShipEditorDialog* _shipEditorDialog = nullptr; + bool _inKeyPressHandler = false; bool _inKeyReleaseHandler = false; From 7c29656c7ca58ecbef46ad4d466ca3e85257b693 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 8 Aug 2025 09:36:19 -0500 Subject: [PATCH 320/466] let the model decide if the apply was successful and should close (#6912) --- qtfred/src/mission/util.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qtfred/src/mission/util.cpp b/qtfred/src/mission/util.cpp index 2604e9c991e..fe898efefd3 100644 --- a/qtfred/src/mission/util.cpp +++ b/qtfred/src/mission/util.cpp @@ -108,8 +108,7 @@ bool rejectOrCloseHandler(__UNUSED QDialog* dialog, } if (button == fso::fred::DialogButton::Yes) { - model->apply(); - return true; + return model->apply(); // only close if apply was successful } if (button == fso::fred::DialogButton::No) { model->reject(); From 7281f71da4b141ffd1a41158ca8ce102f9bac262 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 8 Aug 2025 20:42:47 -0400 Subject: [PATCH 321/466] clear screen going to PXO lobby (#6914) Widescreen mainhalls can still be visible on the sides when going to the PXO lobby so we need to be sure the screen is cleared. --- code/network/multi_pxo.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/network/multi_pxo.cpp b/code/network/multi_pxo.cpp index 9dc7b076226..398c5db88da 100644 --- a/code/network/multi_pxo.cpp +++ b/code/network/multi_pxo.cpp @@ -998,6 +998,10 @@ void multi_pxo_ban_clicked(); void multi_pxo_init(int use_last_channel, bool api_access) { if (!api_access) { + // clear screen + gr_reset_clip(); + gr_clear(); + // load the background bitmap Multi_pxo_bitmap = bm_load(Multi_pxo_bitmap_fname[gr_screen.res]); if (Multi_pxo_bitmap < 0) { From 515beef61a651e366634898cb7c5c7d91a3f82ad Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 8 Aug 2025 20:43:11 -0400 Subject: [PATCH 322/466] fix cleanup of empty model.subsystems files (#6915) --- code/model/modelread.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 66c24c479c5..9aaafc09c00 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -3035,12 +3035,12 @@ modelread_status read_model_file_no_subsys(polymodel * pm, const char* filename, int size; cfclose(ss_fp); - ss_fp = cfopen(debug_name, "rb"); + ss_fp = cfopen(debug_name, "rb", CF_TYPE_TABLES); if ( ss_fp ) { size = cfilelength(ss_fp); cfclose(ss_fp); if ( size <= 0 ) { - _unlink(debug_name); + cf_delete(debug_name, CF_TYPE_TABLES); } } } From 85b37e098e1040ee72d7ee1182ae7d2e48579ae2 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 8 Aug 2025 20:43:34 -0400 Subject: [PATCH 323/466] remove "safe" versions of FD_SET/FD_ISSET (#6918) The socket is a value to be assigned, not an index to be used. As such there is no safety check to be done here and doing so just breaks things. Fixes networking on Windows. --- code/network/gtrack.cpp | 4 ++-- code/network/psnet2.cpp | 12 ++++++------ code/network/psnet2.h | 3 --- code/network/ptrack.cpp | 2 +- code/network/valid.cpp | 14 +++++++------- 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/code/network/gtrack.cpp b/code/network/gtrack.cpp index 6ff3c822d9d..3da97a08ca3 100644 --- a/code/network/gtrack.cpp +++ b/code/network/gtrack.cpp @@ -382,7 +382,7 @@ void IdleGameTracker() //Check for incoming FD_ZERO(&read_fds); // NOLINT - FD_SET_SAFE(Psnet_socket, &read_fds); + FD_SET(Psnet_socket, &read_fds); if(SELECT(static_cast(Psnet_socket+1),&read_fds,nullptr,nullptr,&timeout, PSNET_TYPE_GAME_TRACKER)) { @@ -689,4 +689,4 @@ static void SendClientHolePunch(sockaddr_in6 *addr) packet_length = SerializeGamePacket(&HolePunchAck, packet_data); SENDTO(Psnet_socket, reinterpret_cast(&packet_data), packet_length, 0, reinterpret_cast(addr), sizeof(sockaddr_in6), PSNET_TYPE_GAME_TRACKER); -} \ No newline at end of file +} diff --git a/code/network/psnet2.cpp b/code/network/psnet2.cpp index 7025f198689..f209051be3e 100644 --- a/code/network/psnet2.cpp +++ b/code/network/psnet2.cpp @@ -810,7 +810,7 @@ int psnet_send(net_addr *who_to_addr, void *data, int len, int np_index) // NOLI } FD_ZERO(&wfds); - FD_SET_SAFE(Psnet_socket, &wfds); + FD_SET(Psnet_socket, &wfds); timeout.tv_sec = 0; timeout.tv_usec = 0; @@ -821,7 +821,7 @@ int psnet_send(net_addr *who_to_addr, void *data, int len, int np_index) // NOLI } // if the write file descriptor is not set, then bail! - if ( !FD_ISSET_SAFE(Psnet_socket, &wfds) ) { + if ( !FD_ISSET(Psnet_socket, &wfds) ) { return 0; } @@ -1997,14 +1997,14 @@ void psnet_rel_connect_to_server(PSNET_SOCKET *socket, net_addr *server_addr) timeout.tv_usec = 0; FD_ZERO(&read_fds); - FD_SET_SAFE(Psnet_socket, &read_fds); + FD_SET(Psnet_socket, &read_fds); if ( SELECT(static_cast(Psnet_socket+1), &read_fds, nullptr, nullptr, &timeout, PSNET_TYPE_RELIABLE) == SOCKET_ERROR ) { break; } // if the file descriptor is not set, then bail! - if ( !FD_ISSET_SAFE(Psnet_socket, &read_fds) ) { + if ( !FD_ISSET(Psnet_socket, &read_fds) ) { break; } @@ -2042,14 +2042,14 @@ void psnet_rel_connect_to_server(PSNET_SOCKET *socket, net_addr *server_addr) timeout.tv_usec = 0; FD_ZERO(&read_fds); - FD_SET_SAFE(Psnet_socket, &read_fds); + FD_SET(Psnet_socket, &read_fds); if ( SELECT(static_cast(Psnet_socket+1), &read_fds, nullptr, nullptr, &timeout, PSNET_TYPE_RELIABLE) == SOCKET_ERROR ) { break; } // if the file descriptor is not set, then bail! - if ( !FD_ISSET_SAFE(Psnet_socket, &read_fds) ) { + if ( !FD_ISSET(Psnet_socket, &read_fds) ) { continue; } diff --git a/code/network/psnet2.h b/code/network/psnet2.h index 91f2f3ccd83..8c809897e88 100644 --- a/code/network/psnet2.h +++ b/code/network/psnet2.h @@ -107,9 +107,6 @@ extern unsigned int Serverconn; #define PSNET_IP_MODE_V6 (1<<1) #define PSNET_IP_MODE_DUAL (PSNET_IP_MODE_V4|PSNET_IP_MODE_V6) -#define FD_SET_SAFE(bit, set) FD_SET((bit < 0 || bit >= FD_SETSIZE ? 0 : bit), set) -#define FD_ISSET_SAFE(bit, set) FD_ISSET((bit < 0 || bit >= FD_SETSIZE ? 0 : bit), set) - // ------------------------------------------------------------------------------------------------------- // PSNET 2 TOP LAYER FUNCTIONS - these functions simply buffer and store packets based upon type (see PSNET_TYPE_* defines) // diff --git a/code/network/ptrack.cpp b/code/network/ptrack.cpp index 303c0d0d194..dd284fc1967 100644 --- a/code/network/ptrack.cpp +++ b/code/network/ptrack.cpp @@ -623,7 +623,7 @@ void PollPTrackNet() timeout.tv_usec=0; FD_ZERO(&read_fds); // NOLINT - FD_SET_SAFE(Psnet_socket, &read_fds); + FD_SET(Psnet_socket, &read_fds); if(SELECT(static_cast(Psnet_socket+1), &read_fds,nullptr,nullptr,&timeout, PSNET_TYPE_USER_TRACKER)){ int bytesin; diff --git a/code/network/valid.cpp b/code/network/valid.cpp index 3458a272752..2413c15bb35 100644 --- a/code/network/valid.cpp +++ b/code/network/valid.cpp @@ -306,7 +306,7 @@ int ValidateUser(validate_id_request *valid_id, char *trackerid) timeout.tv_usec=0; FD_ZERO(&read_fds); // NOLINT - FD_SET_SAFE(Psnet_socket, &read_fds); + FD_SET(Psnet_socket, &read_fds); while(SELECT(static_cast(Psnet_socket+1),&read_fds,nullptr,nullptr,&timeout, PSNET_TYPE_VALIDATION)) { @@ -357,7 +357,7 @@ void ValidIdle() timeout.tv_usec=0; FD_ZERO(&read_fds); // NOLINT - FD_SET_SAFE(Psnet_socket, &read_fds); + FD_SET(Psnet_socket, &read_fds); if(SELECT(static_cast(Psnet_socket+1),&read_fds,nullptr,nullptr,&timeout, PSNET_TYPE_VALIDATION)){ int bytesin; @@ -385,7 +385,7 @@ void ValidIdle() } FD_ZERO(&read_fds); // NOLINT - FD_SET_SAFE(Psnet_socket, &read_fds); + FD_SET(Psnet_socket, &read_fds); //Check to make sure the packets ok if ( (bytesin > 0) && (bytesin == inpacket.len) ) { @@ -614,7 +614,7 @@ int ValidateMission(vmt_validate_mission_req_struct *valid_msn) udp_packet_header inpacket; FD_ZERO(&read_fds); // NOLINT - FD_SET_SAFE(Psnet_socket, &read_fds); + FD_SET(Psnet_socket, &read_fds); addrsize = sizeof(fromaddr); RECVFROM(Psnet_socket, reinterpret_cast(&inpacket), sizeof(udp_packet_header), 0, @@ -700,7 +700,7 @@ int ValidateSquadWar(squad_war_request *sw_req, squad_war_response *sw_resp) udp_packet_header inpacket; FD_ZERO(&read_fds); // NOLINT - FD_SET_SAFE(Psnet_socket, &read_fds); + FD_SET(Psnet_socket, &read_fds); addrsize = sizeof(fromaddr); RECVFROM(Psnet_socket, reinterpret_cast(&inpacket), sizeof(udp_packet_header), 0, @@ -794,7 +794,7 @@ int ValidateData(const vmt_valid_data_req_struct *vreq) udp_packet_header inpacket; FD_ZERO(&read_fds); // NOLINT - FD_SET_SAFE(Psnet_socket, &read_fds); + FD_SET(Psnet_socket, &read_fds); addrsize = sizeof(fromaddr); RECVFROM(Psnet_socket, reinterpret_cast(&inpacket), sizeof(udp_packet_header), 0, @@ -831,4 +831,4 @@ bool IsDataIndexValid(const unsigned int idx) } return (DataValidStatus[count] & 1< Date: Sat, 9 Aug 2025 04:14:22 -0700 Subject: [PATCH 324/466] Allow shockwave orientations to be relative to their parents (#6919) * initial setup * various orientations --- code/ship/ship.cpp | 8 ++++---- code/weapon/shockwave.cpp | 4 ++++ code/weapon/shockwave.h | 1 + code/weapon/weapons.cpp | 7 ++++++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 84cd0994aee..01c1eaba422 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -13408,7 +13408,7 @@ int ship_fire_primary(object * obj, int force, bool rollback_shot) vm_vec_normalized_dir(&firing_vec, &predicted_target_pos, &obj->pos); } - vm_vector_2_matrix_norm(&firing_orient, &firing_vec, nullptr, nullptr); + vm_vector_2_matrix_norm(&firing_orient, &firing_vec, &obj->orient.vec.uvec, &obj->orient.vec.rvec); } else if (std_convergence_flagged || (auto_convergence_flagged && (aip->target_objnum != -1))) { // std & auto convergence vec3d target_vec, firing_vec, convergence_offset; @@ -13435,12 +13435,12 @@ int ship_fire_primary(object * obj, int force, bool rollback_shot) vm_vec_normalized_dir(&firing_vec, &target_vec, &firing_pos); // set orientation - vm_vector_2_matrix_norm(&firing_orient, &firing_vec, nullptr, nullptr); + vm_vector_2_matrix_norm(&firing_orient, &firing_vec, &obj->orient.vec.uvec, &obj->orient.vec.rvec); } else if (sip->flags[Ship::Info_Flags::Gun_convergence]) { // model file defined convergence vec3d firing_vec; vm_vec_unrotate(&firing_vec, &pm->gun_banks[bank_to_fire].norm[pt], &obj->orient); - vm_vector_2_matrix_norm(&firing_orient, &firing_vec, nullptr, nullptr); + vm_vector_2_matrix_norm(&firing_orient, &firing_vec, &obj->orient.vec.uvec, &obj->orient.vec.rvec); } if (winfo_p->wi_flags[Weapon::Info_Flags::Apply_Recoil]){ // Function to add recoil functionality - DahBlount @@ -14281,7 +14281,7 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) { vec3d firing_vec; vm_vec_unrotate(&firing_vec, &pm->missile_banks[bank].norm[pnt_index-1], &obj->orient); - vm_vector_2_matrix_norm(&firing_orient, &firing_vec, nullptr, nullptr); + vm_vector_2_matrix_norm(&firing_orient, &firing_vec, &obj->orient.vec.uvec, &obj->orient.vec.rvec); } // create the weapon -- for multiplayer, the net_signature is assigned inside diff --git a/code/weapon/shockwave.cpp b/code/weapon/shockwave.cpp index 76a3304ad15..907d26f7af0 100644 --- a/code/weapon/shockwave.cpp +++ b/code/weapon/shockwave.cpp @@ -173,6 +173,10 @@ int shockwave_create(int parent_objnum, const vec3d* pos, const shockwave_create orient = vmd_identity_matrix; vm_angles_2_matrix(&orient, &sw->rot_angles); + if (sci->rot_parent_relative) { + orient = orient * Objects[parent_objnum].orient; + } + flagset tmp_flags; objnum = obj_create( OBJ_SHOCKWAVE, real_parent, i, &orient, &sw->pos, sw->outer_radius, tmp_flags + Object::Object_Flags::Renders, false ); diff --git a/code/weapon/shockwave.h b/code/weapon/shockwave.h index cbb6dd25a56..194c492b551 100644 --- a/code/weapon/shockwave.h +++ b/code/weapon/shockwave.h @@ -83,6 +83,7 @@ typedef struct shockwave_create_info { int radius_curve_idx; // curve for shockwave radius over time angles rot_angles; bool rot_defined; // if the modder specified rot_angles + bool rot_parent_relative = false; bool damage_overridden; // did this have shockwave damage specifically set or not int damage_type_idx; diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 07cf9ce4633..34b7aaf72aa 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -618,6 +618,11 @@ void parse_shockwave_info(shockwave_create_info *sci, const char *pre_char) sci->rot_defined = true; } + sprintf(buf, "%sShockwave Rotation Is Relative To Parent:", pre_char); + if(optional_string(buf.c_str())) { + stuff_boolean(&sci->rot_parent_relative); + } + sprintf(buf, "%sShockwave Model:", pre_char); if(optional_string(buf.c_str())) { stuff_string(sci->pof_name, F_NAME, MAX_FILENAME_LEN); @@ -7265,7 +7270,7 @@ void spawn_child_weapons(object *objp, int spawn_index_override) // fire the beam beam_fire(&fire_info); } else { - vm_vector_2_matrix_norm(&orient, &tvec, nullptr, nullptr); + vm_vector_2_matrix_norm(&orient, &tvec, &objp->orient.vec.uvec, &objp->orient.vec.rvec); weapon_objnum = weapon_create(&pos, &orient, child_id, parent_num, -1, wp->weapon_flags[Weapon::Weapon_Flags::Locked_when_fired], true); //if the child inherits parent target, do it only if the parent weapon was locked to begin with From b8b24a67e060410a57456ec852b32bd08c9b7107 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 9 Aug 2025 06:16:52 -0500 Subject: [PATCH 325/466] pass an initialized var instead of a temp var (#6917) --- qtfred/src/main.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/main.cpp b/qtfred/src/main.cpp index d089bd12ad9..1942e794a24 100644 --- a/qtfred/src/main.cpp +++ b/qtfred/src/main.cpp @@ -119,7 +119,7 @@ int main(int argc, char* argv[]) { qGuiApp->processEvents(); std::unique_ptr fred(new Editor()); - auto baseDir = QDir::toNativeSeparators(QDir::current().absolutePath()); + auto baseDir = QDir::toNativeSeparators(QDir::current().absolutePath()).toStdString(); typedef std::unordered_map SubsystemMap; @@ -174,7 +174,7 @@ int main(int argc, char* argv[]) { { SubSystem::ScriptingInitHook, app.tr("Running game init scripting hook") }, }; - auto initSuccess = fso::fred::initialize(baseDir.toStdString(), argc, argv, fred.get(), [&](const SubSystem& which) { + auto initSuccess = fso::fred::initialize(baseDir, argc, argv, fred.get(), [&](const SubSystem& which) { if (initializers.count(which)) { splash.showMessage(initializers.at(which), Qt::AlignHCenter | Qt::AlignBottom, Qt::white); } From 858c116b3e54b099799575fddb948b38c4ba8e61 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Wed, 22 May 2024 07:52:38 +0100 Subject: [PATCH 326/466] Add Weapons Dialog Move Ship Files To there own folder for legibility Set File stucture Create Layout Rough outline established Ship data fully loading limit multiselection to one type Add in missing conditon Add weaponlist loading and move Bank model to its own file Delete Missed file Fix Cmake file Change to size_ts Fix Arrivals Partial Drag and drop / better multi-select Finish drag and drop / add ammo editing Add Saving Feature Complete --- code/ship/ship.cpp | 25 ++ code/ship/ship.h | 2 + qtfred/source_groups.cmake | 11 + .../ShipEditor/ShipWeaponsDialogModel.cpp | 379 ++++++++++++++++ .../ShipEditor/ShipWeaponsDialogModel.h | 80 ++++ .../src/ui/dialogs/ShipEditor/BankModel.cpp | 405 ++++++++++++++++++ qtfred/src/ui/dialogs/ShipEditor/BankModel.h | 92 ++++ .../dialogs/ShipEditor/ShipEditorDialog.cpp | 3 +- .../ui/dialogs/ShipEditor/ShipEditorDialog.h | 1 + .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 136 ++++++ .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 50 +++ qtfred/src/ui/widgets/bankTree.cpp | 105 +++++ qtfred/src/ui/widgets/bankTree.h | 24 ++ qtfred/src/ui/widgets/weaponList.cpp | 103 +++++ qtfred/src/ui/widgets/weaponList.h | 39 ++ qtfred/ui/ShipWeaponsDialog.ui | 216 ++++++++++ 16 files changed, 1670 insertions(+), 1 deletion(-) create mode 100644 qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h create mode 100644 qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp create mode 100644 qtfred/src/ui/dialogs/ShipEditor/BankModel.h create mode 100644 qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp create mode 100644 qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h create mode 100644 qtfred/src/ui/widgets/bankTree.cpp create mode 100644 qtfred/src/ui/widgets/bankTree.h create mode 100644 qtfred/src/ui/widgets/weaponList.cpp create mode 100644 qtfred/src/ui/widgets/weaponList.h create mode 100644 qtfred/ui/ShipWeaponsDialog.ui diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 01c1eaba422..1d6e919fd2a 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -18608,6 +18608,31 @@ int get_max_ammo_count_for_primary_bank(int ship_class, int bank, int ammo_type) return (int)std::lround(capacity / size); } +/** + * The same as above, but for a specific turret's bank. + */ +int get_max_ammo_count_for_primary_turret_bank(ship_weapon* swp, int bank, int ammo_type) +{ + float capacity, size; + + Assertion(bank < MAX_SHIP_PRIMARY_BANKS, + "Invalid primary bank of %d (max is %d); get a coder!\n", + bank, + MAX_SHIP_PRIMARY_BANKS - 1); + Assertion(ammo_type < weapon_info_size(), + "Invalid ammo_type of %d is >= Weapon_info.size() (%d); get a coder!\n", + ammo_type, + weapon_info_size()); + + if (!swp || bank < 0 || ammo_type < 0 || !(Weapon_info[ammo_type].wi_flags[Weapon::Info_Flags::Ballistic])) { + return 0; + } else { + capacity = (float)swp->primary_bank_capacity[bank]; + size = (float)Weapon_info[ammo_type].cargo_size; + return (int)(capacity / size); + } +} + /** * Determine the number of secondary ammo units (missile/bomb) allowed max for a ship */ diff --git a/code/ship/ship.h b/code/ship/ship.h index 5cfb53c0dc9..03ad6762def 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1903,6 +1903,8 @@ float ship_get_secondary_weapon_range(ship *shipp); // Goober5000 int get_max_ammo_count_for_primary_bank(int ship_class, int bank, int ammo_type); +int get_max_ammo_count_for_primary_turret_bank(ship_weapon* swp, int bank, int ammo_type); + int get_max_ammo_count_for_bank(int ship_class, int bank, int ammo_type); int get_max_ammo_count_for_turret_bank(ship_weapon *swp, int bank, int ammo_type); diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index a4bd7250f6a..8ed8f3c3566 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -86,6 +86,8 @@ add_file_folder("Source/Mission/Dialogs/ShipEditor" src/mission/dialogs/ShipEditor/ShipTextureReplacementDialogModel.cpp src/mission/dialogs/ShipEditor/ShipTBLViewerModel.cpp src/mission/dialogs/ShipEditor/ShipTBLViewerModel.h + src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp + src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h src/mission/dialogs/ShipEditor/ShipPathsDialogModel.cpp src/mission/dialogs/ShipEditor/ShipPathsDialogModel.h src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h @@ -156,6 +158,10 @@ add_file_folder("Source/UI/Dialogs/ShipEditor" src/ui/dialogs/ShipEditor/ShipTextureReplacementDialog.cpp src/ui/dialogs/ShipEditor/ShipTBLViewer.h src/ui/dialogs/ShipEditor/ShipTBLViewer.cpp + src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp + src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h + src/ui/dialogs/ShipEditor/BankModel.cpp + src/ui/dialogs/ShipEditor/BankModel.h src/ui/dialogs/ShipEditor/ShipPathsDialog.h src/ui/dialogs/ShipEditor/ShipPathsDialog.cpp src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h @@ -178,6 +184,10 @@ add_file_folder("Source/UI/Widgets" src/ui/widgets/sexp_tree.h src/ui/widgets/ShipFlagCheckbox.h src/ui/widgets/ShipFlagCheckbox.cpp + src/ui/widgets/weaponList.cpp + src/ui/widgets/weaponList.h + src/ui/widgets/BankTree.cpp + src/ui/widgets/BankTree.h ) add_file_folder("UI" @@ -211,6 +221,7 @@ add_file_folder("UI" ui/ShipTBLViewer.ui ui/ShipPathsDialog.ui ui/ShipCustomWarpDialog.ui + ui/ShipWeaponsDialog.ui ) add_file_folder("Resources" diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp new file mode 100644 index 00000000000..efad53ec636 --- /dev/null +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -0,0 +1,379 @@ +#include "ShipWeaponsDialogModel.h" +namespace fso { +namespace fred { +Banks::Banks(const SCP_string& name, int aiIndex, int ship, int multiedit, ship_subsys* subsys) + : name(name), subsys(subsys), ship(ship), m_isMultiEdit(multiedit), initalAI(aiIndex) +{ + aiClass = aiIndex; +} +void Banks::add(Bank* bank) +{ + banks.push_back(bank); +} +Bank* Banks::getByBankId(const int id) +{ + for (auto bank : banks) { + if (id == bank->getWeaponId()) + return bank; + } + return nullptr; +} +SCP_string Banks::getName() const +{ + return name; +} +int Banks::getShip() const +{ + return ship; +} +ship_subsys* Banks::getSubsys() const +{ + return subsys; +} +bool Banks::empty() const +{ + return banks.empty(); +} +const SCP_vector Banks::getBanks() const +{ + return banks; +} +int Banks::getAiClass() const +{ + if (name == "Pilot") { + return Ships[ship].weapons.ai_class; + } else { + return subsys->weapons.ai_class; + } +} +void Banks::setAiClass(int newClass) +{ + if (m_isMultiEdit) { + object* ptr; + int inst; + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { + inst = ptr->instance; + if (name == "Pilot") { + Ships[inst].ai_index = newClass; + } else { + subsys->weapons.ai_class = newClass; + } + } + } + } else { + if (name == "Pilot") { + Ships[ship].weapons.ai_class = newClass; + } else { + subsys->weapons.ai_class = newClass; + } + } +} +int Banks::getInitalAI() +{ + return initalAI; +} +Bank::Bank(const int weaponId, const int bankId, const int ammoMax, const int ammo, Banks* parent) +{ + this->weaponId = weaponId; + this->bankId = bankId; + this->ammo = ammo; + this->ammoMax = ammoMax; + this->parent = parent; +} +int Bank::getWeaponId() const +{ + return weaponId; +} +int Bank::getAmmo() const +{ + return ammo; +} +int Bank::getBankId() const +{ + return bankId; +} +int Bank::getMaxAmmo() const +{ + return ammoMax; +} +void Bank::setWeapon(const int id) +{ + weaponId = id; + if (Weapon_info[id].subtype == WP_LASER || Weapon_info[id].subtype == WP_BEAM) { + if (parent->getName() == "Pilot") { + ammoMax = get_max_ammo_count_for_primary_bank(parent->getShip(), bankId, id); + } else { + ammoMax = get_max_ammo_count_for_primary_turret_bank(&parent->getSubsys()->weapons, bankId, id); + } + } else { + if (parent->getName() == "Pilot") { + ammoMax = get_max_ammo_count_for_bank(parent->getShip(), bankId, id); + } else { + ammoMax = get_max_ammo_count_for_turret_bank(&parent->getSubsys()->weapons, bankId, id); + } + } +} +void Bank::setAmmo(const int newAmmo) +{ + this->ammo = newAmmo; +} +namespace dialogs { +ShipWeaponsDialogModel::ShipWeaponsDialogModel(QObject* parent, EditorViewport* viewport, bool isMultiEdit) + : AbstractDialogModel(parent, viewport) +{ + initializeData(isMultiEdit); +} +void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) +{ + m_isMultiEdit = isMultiEdit; + PrimaryBanks.clear(); + SecondaryBanks.clear(); + int inst; + bool first = true; + object* ptr; + + m_ship = _editor->cur_ship; + if (m_ship == -1) + m_ship = Objects[_editor->currentObject].instance; + + if (m_isMultiEdit) { + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { + inst = ptr->instance; + if (!(Ship_info[Ships[inst].ship_info_index].is_big_or_huge())) + big = 0; + initPrimary(inst, first); + initSecondary(inst, first); + // initTertiary(inst, first); + first = false; + } + } + } else { + if (!(Ship_info[Ships[m_ship].ship_info_index].is_big_or_huge())) + big = 0; + initPrimary(m_ship, true); + initSecondary(m_ship, true); + } +} + +void ShipWeaponsDialogModel::initPrimary(int inst, bool first) +{ + auto pilotBank = new Banks("Pilot", Ships[inst].weapons.ai_class, inst, m_isMultiEdit); + if (first) { + auto pilot = Ships[inst].weapons; + for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { + if (pilot.primary_bank_weapons[i] >= 0) { + const int maxAmmo = + get_max_ammo_count_for_primary_bank(Ships[inst].ship_info_index, i, pilot.primary_bank_weapons[i]); + const int ammo = fl2ir(pilot.primary_bank_ammo[i] * maxAmmo / 100.0f); + pilotBank->add(new Bank(pilot.primary_bank_weapons[i], i, maxAmmo, ammo, pilotBank)); + } + } + PrimaryBanks.push_back(pilotBank); + ship_subsys* ssl = &Ships[inst].subsys_list; + ship_subsys* pss; + for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + model_subsystem* psub = pss->system_info; + if (psub->type == SUBSYSTEM_TURRET) { + auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit, pss); + for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { + if (pss->weapons.primary_bank_weapons[i] >= 0) { + const int maxAmmo = get_max_ammo_count_for_primary_turret_bank(&pss->weapons, + i, + pss->weapons.primary_bank_weapons[i]); + const int ammo = fl2ir(pss->weapons.primary_bank_ammo[i] * maxAmmo / 100.0f); + turretBank->add(new Bank(pss->weapons.primary_bank_weapons[i], i, maxAmmo, ammo, turretBank)); + } + } + if (!turretBank->empty()) { + PrimaryBanks.push_back(turretBank); + } else { + delete turretBank; + } + } + } + } else { + for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { + if (PrimaryBanks[0]->getByBankId(i)->getWeaponId() != Ships[inst].weapons.primary_bank_weapons[i]) { + PrimaryBanks[0]->getByBankId(i)->setWeapon(-2); + } + if (PrimaryBanks[0]->getByBankId(i)->getAmmo() != Ships[inst].weapons.primary_bank_ammo[i]) { + PrimaryBanks[0]->getByBankId(i)->setAmmo(-2); + } + } + ship_subsys* ssl = &Ships[inst].subsys_list; + ship_subsys* pss; + for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + model_subsystem* psub = pss->system_info; + if (psub->type == SUBSYSTEM_TURRET) { + for (auto banks : PrimaryBanks) { + if (banks->getSubsys() == pss) { + for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { + if (banks->getByBankId(i)->getWeaponId() != pss->weapons.primary_bank_weapons[i]) { + banks->getByBankId(i)->setWeapon(-2); + } + if (banks->getByBankId(i)->getAmmo() != pss->weapons.primary_bank_ammo[i]) { + banks->getByBankId(i)->setAmmo(-2); + } + } + } + } + } + } + } +} + +void ShipWeaponsDialogModel::initSecondary(int inst, bool first) +{ + auto pilotBank = new Banks("Pilot", Ships[inst].weapons.ai_class, inst, m_isMultiEdit); + if (first) { + auto pilot = Ships[inst].weapons; + for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { + if (pilot.secondary_bank_weapons[i] >= 0) { + const int maxAmmo = + get_max_ammo_count_for_bank(Ships[inst].ship_info_index, i, pilot.secondary_bank_weapons[i]); + const int ammo = fl2ir(pilot.secondary_bank_ammo[i] * maxAmmo / 100.0f); + pilotBank->add(new Bank(pilot.secondary_bank_weapons[i], i, maxAmmo, ammo, pilotBank)); + } + } + SecondaryBanks.push_back(pilotBank); + ship_subsys* ssl = &Ships[inst].subsys_list; + ship_subsys* pss; + for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + model_subsystem* psub = pss->system_info; + if (psub->type == SUBSYSTEM_TURRET) { + auto turretBank = new Banks(psub->subobj_name, pss->weapons.ai_class, inst, m_isMultiEdit, pss); + for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { + if (pss->weapons.secondary_bank_weapons[i] >= 0) { + const int maxAmmo = get_max_ammo_count_for_turret_bank(&pss->weapons, + i, + pss->weapons.secondary_bank_weapons[i]); + const int ammo = fl2ir(pss->weapons.secondary_bank_ammo[i] * maxAmmo / 100.0f); + turretBank->add(new Bank(pss->weapons.secondary_bank_weapons[i], i, maxAmmo, ammo, turretBank)); + } + } + if (!turretBank->empty()) { + SecondaryBanks.push_back(turretBank); + } else { + delete turretBank; + } + } + } + } else { + for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { + if (SecondaryBanks[0]->getByBankId(i)->getWeaponId() != Ships[inst].weapons.secondary_bank_weapons[i]) { + SecondaryBanks[0]->getByBankId(i)->setWeapon(-2); + } + if (SecondaryBanks[0]->getByBankId(i)->getAmmo() != Ships[inst].weapons.secondary_bank_ammo[i]) { + SecondaryBanks[0]->getByBankId(i)->setAmmo(-2); + } + } + ship_subsys* ssl = &Ships[inst].subsys_list; + ship_subsys* pss; + for (pss = GET_FIRST(ssl); pss != END_OF_LIST(ssl); pss = GET_NEXT(pss)) { + model_subsystem* psub = pss->system_info; + if (psub->type == SUBSYSTEM_TURRET) { + for (auto banks : SecondaryBanks) { + if (banks->getSubsys() == pss) { + for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { + if (banks->getByBankId(i)->getWeaponId() != pss->weapons.secondary_bank_weapons[i]) { + banks->getByBankId(i)->setWeapon(-2); + } + if (banks->getByBankId(i)->getAmmo() != pss->weapons.secondary_bank_ammo[i]) { + banks->getByBankId(i)->setAmmo(-2); + } + } + } + } + } + } + } +} +void ShipWeaponsDialogModel::saveShip(int inst) +{ + for (auto Turret : PrimaryBanks) { + if (Turret->getName() == "Pilot") { + for (auto bank : Turret->getBanks()) { + if (bank->getWeaponId() != -2) { + Ships[inst].weapons.primary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); + Ships[inst].weapons.primary_bank_ammo[bank->getBankId()] = + bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; + } + } + } else { + ship_subsys* pss = Turret->getSubsys(); + for (auto bank : Turret->getBanks()) { + if (bank->getWeaponId() != -2) { + pss->weapons.primary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); + pss->weapons.primary_bank_ammo[bank->getBankId()] = + bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; + } + } + } + } + for (auto Turret : SecondaryBanks) { + if (Turret->getName() == "Pilot") { + for (auto bank : Turret->getBanks()) { + if (bank->getWeaponId() != -2) { + Ships[inst].weapons.secondary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); + Ships[inst].weapons.secondary_bank_ammo[bank->getBankId()] = + bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; + } + } + } else { + ship_subsys* pss = Turret->getSubsys(); + for (auto bank : Turret->getBanks()) { + if (bank->getWeaponId() != -2) { + pss->weapons.secondary_bank_weapons[bank->getBankId()] = bank->getWeaponId(); + pss->weapons.secondary_bank_ammo[bank->getBankId()] = + bank->getMaxAmmo() ? fl2ir(bank->getAmmo() * 100.0f / bank->getMaxAmmo()) : 0; + } + } + } + } +} +bool ShipWeaponsDialogModel::apply() +{ + if (m_isMultiEdit) { + object* ptr; + int inst; + ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { + inst = ptr->instance; + saveShip(inst); + } + } + } else { + saveShip(m_ship); + } + _editor->missionChanged(); + return true; +} +void ShipWeaponsDialogModel::reject() +{ + for (auto Turret : PrimaryBanks) { + Turret->setAiClass(Turret->getInitalAI()); + } + for (auto Turret : SecondaryBanks) { + Turret->setAiClass(Turret->getInitalAI()); + } +} +SCP_vector ShipWeaponsDialogModel::getPrimaryBanks() const +{ + return PrimaryBanks; +} +SCP_vector ShipWeaponsDialogModel::getSecondaryBanks() const +{ + return SecondaryBanks; +} +/* void ShipWeaponsDialogModel::initTertiary(int inst, bool first) { + +} +*/ +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h new file mode 100644 index 00000000000..37adbb4dd89 --- /dev/null +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -0,0 +1,80 @@ +#pragma once + +#include "../AbstractDialogModel.h" + +#include + +namespace fso { +namespace fred { +struct Bank; +struct Banks { + Banks(const SCP_string& name, int aiIndex, int ship,int multiedit, ship_subsys* subsys = nullptr); + + public: + void add(Bank*); + Bank* getByBankId(const int id); + SCP_string getName() const; + int getShip() const; + ship_subsys* getSubsys() const; + bool empty() const; + const SCP_vector getBanks() const; + int getAiClass() const; + void setAiClass(int); + bool m_isMultiEdit; + int getInitalAI(); + private: + SCP_string name; + ship_subsys* subsys; + int aiClass; + int initalAI; + SCP_vector banks; + int ship; +}; +struct Bank { + public: + Bank(const int weaponId, const int bankId, const int ammoMax, const int ammo, Banks* parent); + + int getWeaponId() const; + int getAmmo() const; + int getBankId() const; + int getMaxAmmo() const; + + void setWeapon(const int id); + void setAmmo(const int ammo); + + private: + int weaponId; + int bankId; + int ammo; + int ammoMax; + Banks* parent; +}; +namespace dialogs { +class ShipWeaponsDialogModel : public AbstractDialogModel { + public: + ShipWeaponsDialogModel(QObject* parent, EditorViewport* viewport, bool multi); + + // void initTertiary(int inst, bool first); + + bool apply() override; + void reject() override; + SCP_vector getPrimaryBanks() const; + SCP_vector getSecondaryBanks() const; + // SCP_vector getTertiaryBanks() const; + + private: + void saveShip(int inst); + void initPrimary(const int inst, bool first); + + void initSecondary(int inst, bool first); + void initializeData(bool multi); + int m_isMultiEdit; + int m_ship; + int big = 1; + SCP_vector PrimaryBanks; + SCP_vector SecondaryBanks; + // SCP_vector TertiaryBanks; +}; +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp new file mode 100644 index 00000000000..87e72d0e9f5 --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp @@ -0,0 +1,405 @@ +#include "ShipWeaponsDialog.h" + +#include +#include +namespace fso { +namespace fred { +BankTreeItem::BankTreeItem(BankTreeItem* parentItem) : m_parentItem(parentItem) {} +BankTreeItem::~BankTreeItem() +{ + qDeleteAll(m_childItems); +} +void BankTreeItem::appendChild(BankTreeItem* item) +{ + m_childItems.append(item); +} +BankTreeItem* BankTreeItem::child(int row) const +{ + if (row < 0 || row >= m_childItems.size()) + return nullptr; + return m_childItems.at(row); +} +int BankTreeItem::childCount() const +{ + return m_childItems.count(); +} +int BankTreeItem::childNumber() const +{ + if (m_parentItem) + return m_parentItem->m_childItems.indexOf(const_cast(this)); + return 0; +} +BankTreeItem* BankTreeItem::parentItem() +{ + return m_parentItem; +} + +bool BankTreeItem::insertLabel(int position, const QString& newName, Banks* newBanks) +{ + if (position < 0 || position > m_childItems.size()) + return false; + + auto* item = new BankTreeLabel(newName, newBanks, this); + m_childItems.insert(position, item); + + return true; +} + +bool BankTreeItem::insertBank(int position, Bank* newBank) +{ + if (position < 0 || position > m_childItems.size()) + return false; + + auto* item = new BankTreeBank(newBank, this); + m_childItems.insert(position, item); + + return true; +} + +QString BankTreeItem::getName() const +{ + return name; +} + +int BankTreeBank::getId() const +{ + return bank->getWeaponId(); +} + +BankTreeBank::BankTreeBank(Bank* bank, BankTreeItem* parentItem) : BankTreeItem(parentItem) +{ + this->bank = bank; + switch (bank->getWeaponId()) { + case -2: + this->name = "CONFLICT"; + break; + case -1: + this->name = "None"; + break; + default: + this->name = Weapon_info[bank->getWeaponId()].name; + } +} + +QVariant BankTreeBank::data(int column) const +{ + switch (column) { + case 0: + return name; + break; + case 1: + return bank->getAmmo(); + break; + default: + return {}; + } +} + +Qt::ItemFlags BankTreeBank::getFlags(int column) const +{ + switch (column) { + case 0: + return Qt::ItemIsDropEnabled | Qt::ItemIsSelectable; + break; + case 1: + return Qt::ItemIsEditable; + break; + default: + return {}; + } +} + +void BankTreeBank::setWeapon(int id) +{ + bank->setWeapon(id); + if (id == -1) { + name = "None"; + } else { + name = Weapon_info[id].name; + } +} + +void BankTreeBank::setAmmo(int value) +{ + Assert(bank != nullptr); + bank->setAmmo(value); +} + +BankTreeLabel::BankTreeLabel(const QString& name, Banks* banks, BankTreeItem* parentItem) : BankTreeItem(parentItem) +{ + this->name = name; + this->banks = banks; +} + +QVariant BankTreeLabel::data(int column) const +{ + switch (column) { + case 0: + return name + " (" + Ai_class_names[banks->getAiClass()] + + ")"; + break; + default: + return {}; + } +} + +Qt::ItemFlags BankTreeLabel::getFlags(int column) const +{ + return Qt::ItemIsSelectable; +} + +void BankTreeLabel::setAIClass(int value) +{ + Assert(banks != nullptr); + banks->setAiClass(value); +} + +bool BankTreeLabel::setData(int column, const QVariant& value) +{ + setAIClass(value.toInt()); + return true; +} + +bool BankTreeBank::setData(int column, const QVariant& value) +{ + switch (column) { + case 1: + setAmmo(value.toInt()); + return true; + break; + default: + return false; + } +} +BankTreeModel::BankTreeModel(const SCP_vector& data, QObject* parent) : QAbstractItemModel(parent) +{ + rootItem = new BankTreeRoot(); + + setupModelData(data, rootItem); +} + +void BankTreeModel::setupModelData(const SCP_vector& data, BankTreeItem* parent) +{ + for (auto banks : data) { + parent->insertLabel(parent->childCount(), banks->getName().c_str(), banks); + BankTreeItem* currentParent = parent->child(parent->childCount() - 1); + for (auto bank : banks->getBanks()) { + currentParent->insertBank(currentParent->childCount(), bank); + } + } +} + +QVariant BankTreeModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role == Qt::DisplayRole) { + switch (section) { + case 0: + return tr("Bank Name/Weapon"); + case 1: + return tr("Ammo"); + default: + return QString(""); + } + } + return {}; +} + +BankTreeModel::~BankTreeModel() +{ + delete rootItem; +} + +int BankTreeModel::columnCount(const QModelIndex& parent) const +{ + Q_UNUSED(parent); + return 2; +} + +QVariant BankTreeModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) { + return {}; + } + + if (role != Qt::DisplayRole && role != Qt::EditRole) + return {}; + + BankTreeItem* item = getItem(index); + + return item->data(index.column()); +} + +BankTreeItem* BankTreeModel::getItem(const QModelIndex index) const +{ + if (index.isValid()) { + auto* item = static_cast(index.internalPointer()); + if (item) + return item; + } + return rootItem; +} + +bool BankTreeModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (role != Qt::EditRole) + return false; + + BankTreeItem* item = getItem(index); // getItem(index); + if (!item) { + return false; + } + bool result = item->setData(index.column(), value); + + return result; +} + +int BankTreeModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid() && parent.column() > 0) + return 0; + + const BankTreeItem* parentItem = getItem(parent); + + return parentItem ? parentItem->childCount() : 0; +} + +Qt::ItemFlags BankTreeModel::flags(const QModelIndex& index) const +{ + Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); + defaultFlags.setFlag(Qt::ItemIsSelectable, false); + + if (index.isValid()) { + auto* item = static_cast(index.internalPointer()); + return item->getFlags(index.column()) | defaultFlags; + } else { + return Qt::NoItemFlags; + } +} + +QModelIndex BankTreeModel::index(int row, int column, const QModelIndex& parent) const +{ + if (parent.isValid() && parent.column() != 0) + return {}; + + BankTreeItem* parentItem = getItem(parent); + if (!parentItem) + return {}; + + BankTreeItem* childItem = parentItem->child(row); + if (childItem) + return createIndex(row, column, childItem); + return {}; +} +QModelIndex BankTreeModel::parent(const QModelIndex& index) const +{ + if (!index.isValid()) + return {}; + BankTreeItem* childItem = getItem(index); + BankTreeItem* parentItem = childItem ? childItem->parentItem() : nullptr; + + if (parentItem == rootItem || !parentItem) + return {}; + return createIndex(parentItem->childNumber(), 0, parentItem); +} +QStringList BankTreeModel::mimeTypes() const +{ + QStringList types; + types << "application/weaponid"; + return types; +} + +bool BankTreeModel::canDropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) const +{ + Q_UNUSED(action); + Q_UNUSED(row); + Q_UNUSED(parent); + + if (!data->hasFormat("application/weaponid")) + return false; + BankTreeItem* item = this->getItem(parent); + Qt::ItemFlags flags = item->getFlags(column); + return flags.testFlag(Qt::ItemIsDropEnabled); +} +bool BankTreeModel::dropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) +{ + if (!canDropMimeData(data, action, row, column, parent)) + return false; + + if (action == Qt::IgnoreAction) + return true; + + int beginRow; + + if (row != -1) + beginRow = row; + else if (parent.isValid()) + beginRow = parent.row(); + else + return false; + + QByteArray encodedData = data->data("application/weaponid"); + QDataStream stream(&encodedData, QIODevice::ReadOnly); + while (!stream.atEnd()) { + int id = 0; + stream >> id; + setWeapon(parent, id); + } + return true; +} + +void BankTreeModel::setWeapon(const QModelIndex& index, int data) const +{ + auto item = dynamic_cast(this->getItem(index)); + Assert(item != nullptr); + if (item != nullptr) { + item->setWeapon(data); + } +} + +bool BankTreeRoot::setData(int column, const QVariant& value) +{ + return false; +} +QVariant BankTreeRoot::data(int column) const +{ + switch (column) { + case 0: + return "Name/Weapon"; + break; + case 1: + return "Ammo"; + break; + default: + return {}; + } +} +Qt::ItemFlags BankTreeRoot::getFlags(int column) const +{ + return {}; +} + +int BankTreeModel::checktype(const QModelIndex index) const +{ + int type; + BankTreeItem* item = getItem(index); + auto bankTest = dynamic_cast(item); + auto labelTest = dynamic_cast(item); + if (bankTest) { + type = 0; + } else if (labelTest) { + type = 1; + } else { + type = -1; + } + return type; +} +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h new file mode 100644 index 00000000000..df912ae5f6b --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h @@ -0,0 +1,92 @@ +#pragma once +#include "mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h" +#include +namespace fso { +namespace fred { +class BankTreeItem { + public: + explicit BankTreeItem(BankTreeItem* parentItem = nullptr); + virtual ~BankTreeItem(); + virtual QVariant data(int column) const = 0; + void appendChild(BankTreeItem* child); + BankTreeItem* child(int row) const; + int childCount() const; + int childNumber() const; + BankTreeItem* parentItem(); + bool insertLabel(int position, const QString& name, Banks* banks); + bool insertBank(int position, Bank* banks); + + QString getName() const; + virtual bool setData(int column, const QVariant& value) = 0; + virtual Qt::ItemFlags getFlags(int column) const = 0; + QList m_childItems; + + protected: + QString name; + + private: + BankTreeItem* m_parentItem; +}; +class BankTreeRoot : public BankTreeItem { + bool setData(int column, const QVariant& value) override; + QVariant data(int column) const override; + Qt::ItemFlags getFlags(int column) const override; +}; +class BankTreeBank : public BankTreeItem { + public: + explicit BankTreeBank(Bank* bank, BankTreeItem* parentItem = nullptr); + void setWeapon(int id); + void setAmmo(int value); + int getId() const; + bool setData(int column, const QVariant& value) override; + QVariant data(int column) const override; + Qt::ItemFlags getFlags(int column) const override; + + private: + Bank* bank; +}; +class BankTreeLabel : public BankTreeItem { + public: + explicit BankTreeLabel(const QString& name, Banks* banks, BankTreeItem* parentItem = nullptr); + void setAIClass(int value); + bool setData(int column, const QVariant& value) override; + QVariant data(int column) const override; + Qt::ItemFlags getFlags(int column) const override; + + private: + Banks* banks; +}; + +class BankTreeModel : public QAbstractItemModel { + Q_OBJECT + public: + BankTreeModel(const SCP_vector& data, QObject* parent = nullptr); + ~BankTreeModel() override; + int columnCount(const QModelIndex& parent) const override; + QVariant data(const QModelIndex& index, int role) const override; + + QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex& index) const override; + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + + Qt::ItemFlags flags(const QModelIndex& index) const override; + + QStringList mimeTypes() const override; + bool + canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const; + bool + dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + void setWeapon(const QModelIndex& index, int data) const; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + int checktype(const QModelIndex index) const; + + private: + BankTreeItem* rootItem; + BankTreeItem* getItem(const QModelIndex index) const; + static void setupModelData(const SCP_vector& data, BankTreeItem* parent); +}; +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index 9f371c81960..c4b4a1dd4ca 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -846,7 +846,8 @@ void ShipEditorDialog::on_deleteButton_clicked() } void ShipEditorDialog::on_weaponsButton_clicked() { - //TODO + auto weaponsDialog = new dialogs::ShipWeaponsDialog(this, _viewport, getIfMultipleShips()); + weaponsDialog->show(); } void ShipEditorDialog::on_playerOrdersButton_clicked() { diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h index 6d6fe3ab5d1..18592c81e38 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h @@ -11,6 +11,7 @@ #include "ShipSpecialStatsDialog.h" #include "ShipTextureReplacementDialog.h" #include "ShipTBLViewer.h" +#include "ShipWeaponsDialog.h" #include "ShipPathsDialog.h" #include "ShipCustomWarpDialog.h" diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp new file mode 100644 index 00000000000..d19a4d2210c --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -0,0 +1,136 @@ +#include "ShipWeaponsDialog.h" + +#include "ui_ShipWeaponsDialog.h" + +#include +#include + +#include +#include +namespace fso { +namespace fred { +namespace dialogs { +ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit) + : QDialog(parent), ui(new Ui::ShipWeaponsDialog()), _model(new ShipWeaponsDialogModel(this, viewport, isMultiEdit)), + _viewport(viewport) +{ + ui->setupUi(this); + + connect(ui->radioPrimary, &QRadioButton::toggled, this, [this](bool param) { modeChanged(param, 0); }); + connect(ui->radioSecondary, &QRadioButton::toggled, this, [this](bool param) { modeChanged(param, 1); }); + connect(ui->radioTertiary, &QRadioButton::toggled, this, [this](bool param) { modeChanged(param, 2); }); + + + connect(this, &QDialog::accepted, _model.get(), &ShipWeaponsDialogModel::apply); + if (!_model->getPrimaryBanks().empty()) { + const util::SignalBlockers blockers(this); + bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); + ui->radioPrimary->setChecked(true); + dialogMode = 0; + weapons = new WeaponModel(0); + } else if (!_model->getSecondaryBanks().empty()) { + const util::SignalBlockers blockers(this); + bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); + ui->radioSecondary->setChecked(true); + dialogMode = 1; + weapons = new WeaponModel(1); + } else { + Error("No Valid Weapon banks on ship"); + } + ui->treeBanks->setModel(bankModel); + connect(ui->treeBanks->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &ShipWeaponsDialog::updateUI); + connect(ui->AICombo, + static_cast(&QComboBox::currentIndexChanged), + this, + &ShipWeaponsDialog::aiClassChanged); + ui->listWeapons->setModel(weapons); + ui->treeBanks->expandAll(); + ui->treeBanks->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + updateUI(); +} + +ShipWeaponsDialog::~ShipWeaponsDialog() { + delete bankModel; + delete weapons; +} + +void ShipWeaponsDialog::closeEvent(QCloseEvent* event) +{ + accept(); + QDialog::closeEvent(event); +} +void ShipWeaponsDialog::on_setAllButton_clicked() +{ + int test = 0; + for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { + bankModel->setWeapon(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); + } +} +void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) +{ + if (enabled) { + if (mode == 0) { + bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); + dialogMode = 0; + } else if (mode == 1) { + bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); + dialogMode = 1; + } else if (mode == 2) { + // bankModel = new BankTreeModel(_model->getTertiaryBanks(), this); + dialogMode = 2; + } else { + _viewport->dialogProvider->showButtonDialog(DialogType::Error, + "Illegal Mode", + "Somehow an Illegal mode has been set. Get a coder.\n Illegal mode is " + mode, + {DialogButton::Ok}); + ui->radioPrimary->toggled(true); + bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); + dialogMode = 0; + } + ui->treeBanks->setModel(bankModel); + ui->treeBanks->expandAll(); + } + updateUI(); +} +void ShipWeaponsDialog::updateUI() +{ + const util::SignalBlockers blockers(this); + ui->radioPrimary->setEnabled(!_model->getPrimaryBanks().empty()); + ui->radioSecondary->setEnabled(!_model->getSecondaryBanks().empty()); + ui->radioTertiary->setEnabled(false); + ui->treeBanks->expandAll(); + + if (ui->treeBanks->getTypeSelected() == 0) { + ui->setAllButton->setEnabled(true); + } else { + ui->setAllButton->setEnabled(false); + } + + if (ui->treeBanks->getTypeSelected() == 1) { + ui->AIButton->setEnabled(true); + } else { + ui->AIButton->setEnabled(false); + } + ui->AICombo->clear(); + for (int i = 0; i < Num_ai_classes; i++) { + ui->AICombo->addItem(Ai_class_names[i], QVariant(i)); + } + ui->AICombo->setCurrentIndex(ui->AICombo->findData(m_currentAI)); +} + +void ShipWeaponsDialog::aiClassChanged(const int index) { + m_currentAI = ui->AICombo->itemData(index).toInt(); +} + +void ShipWeaponsDialog::on_AIButton_clicked() { + for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { + bankModel->setData(index, m_currentAI); + } +} + +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h new file mode 100644 index 00000000000..90168a8ea72 --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -0,0 +1,50 @@ +#ifndef SHIPWEAPONSDIALOG_H +#define SHIPWEAPONSDIALOG_H + +#include "ui/dialogs/ShipEditor/BankModel.h" +#include "ui/widgets/weaponList.h" + +#include +#include + +#include +#include + +namespace fso { +namespace fred { +namespace dialogs { + +namespace Ui { +class ShipWeaponsDialog; +} + +class ShipWeaponsDialog : public QDialog { + Q_OBJECT + + public: + explicit ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit); + ~ShipWeaponsDialog() override; + + protected: + void closeEvent(QCloseEvent*) override; + + private slots: + void on_AIButton_clicked(); + void on_setAllButton_clicked(); + + private: + std::unique_ptr ui; + std::unique_ptr _model; + void modeChanged(const bool enabled, const int mode); + EditorViewport* _viewport; + void updateUI(); + BankTreeModel* bankModel; + int dialogMode; + WeaponModel* weapons; + int m_currentAI = 0; + void aiClassChanged(const int index); +}; +} // namespace dialogs +} // namespace fred +} // namespace fso +#endif \ No newline at end of file diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp new file mode 100644 index 00000000000..c61a0ec58d3 --- /dev/null +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -0,0 +1,105 @@ +#include "bankTree.h" +namespace fso { +namespace fred { +bankTree::bankTree(QWidget* parent) : QTreeView(parent) +{ + setAcceptDrops(true); +} +void bankTree::dragEnterEvent(QDragEnterEvent* event) +{ + if (event->mimeData()->hasFormat("application/weaponid")) { + event->acceptProposedAction(); + } +} +void bankTree::dropEvent(QDropEvent* event) +{ + auto item = indexAt(event->pos()); + if (!item.isValid()) { + return; + } + bool accepted = model()->dropMimeData(event->mimeData(), Qt::CopyAction, -1, 0, item); + if (accepted) { + event->acceptProposedAction(); + } +} +void bankTree::dragMoveEvent(QDragMoveEvent* event) +{ + auto pos = QCursor::pos(); + auto index = indexAt(pos); + if (!index.isValid()) { + return; + } + if (dynamic_cast(model())->checktype(index) == 0) { + event->accept(); + } else { + event->ignore(); + } +} +void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) +{ + QItemSelection newlySelected; + QItemSelection select; + QItemSelection deselect(deselected); + if (selected.empty()) { + QTreeView::selectionChanged(selected, deselected); + if (selectionModel()->selectedIndexes().size() == 0) { + typeSelected = -1; + } + return; + } + for (auto& sidx : selected.indexes()) { + bool match = false; + for (auto& didx : deselected.indexes()) { + if (sidx == didx) { + match = true; + break; + } + } + if (!match) { + QItemSelectionRange selection(sidx); + newlySelected.append(selection); + } + } + if (!newlySelected.empty()) { + if (typeSelected == -1) { + typeSelected = dynamic_cast(model())->checktype(newlySelected.indexes().first()); + for (auto& sidx : newlySelected.indexes()) { + if (dynamic_cast(model())->checktype(sidx) == typeSelected) { + QItemSelectionRange selection(sidx); + select.append(selection); + } + } + } else { + int type = dynamic_cast(model())->checktype(newlySelected.indexes().first()); + if (type != typeSelected) { + typeSelected = type; + for (auto& sidx : selected.indexes()) { + QItemSelectionRange selection(sidx); + deselect.append(selection); + } + for (auto& sidx : newlySelected.indexes()) { + if (dynamic_cast(model())->checktype(sidx) == typeSelected) { + QItemSelectionRange selection(sidx); + select.append(selection); + } + } + selectionModel()->clear(); + typeSelected = -1; + } else { + for (auto& sidx : newlySelected.indexes()) { + if (dynamic_cast(model())->checktype(sidx) == typeSelected) { + QItemSelectionRange selection(sidx); + select.append(selection); + } + } + } + } + } + QTreeView::selectionChanged(select, deselect); +} +int bankTree::getTypeSelected() const +{ + return typeSelected; +} +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/widgets/bankTree.h b/qtfred/src/ui/widgets/bankTree.h new file mode 100644 index 00000000000..7a445ca89bc --- /dev/null +++ b/qtfred/src/ui/widgets/bankTree.h @@ -0,0 +1,24 @@ +#pragma once +#include +#include +#include +#include +#include +#include "ui/dialogs/ShipEditor/BankModel.h" +#include +namespace fso { +namespace fred { +class bankTree : public QTreeView { + Q_OBJECT + public: + bankTree(QWidget*); + void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected); + int getTypeSelected() const; + protected: + void dragEnterEvent(QDragEnterEvent*); + void dropEvent(QDropEvent* event); + void dragMoveEvent(QDragMoveEvent*); + int typeSelected = -1; +}; +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/widgets/weaponList.cpp b/qtfred/src/ui/widgets/weaponList.cpp new file mode 100644 index 00000000000..3f67af99348 --- /dev/null +++ b/qtfred/src/ui/widgets/weaponList.cpp @@ -0,0 +1,103 @@ +#include "weaponList.h" + +namespace fso { +namespace fred { +weaponList::weaponList(QWidget* parent) : QListView(parent) {} + +void weaponList::mousePressEvent(QMouseEvent* event) +{ + if (event->button() == Qt::LeftButton) + dragStartPosition = event->pos(); + QListView::mousePressEvent(event); +} +void weaponList::mouseMoveEvent(QMouseEvent* event) +{ + if (!(event->buttons() & Qt::LeftButton)) + return; + if ((event->pos() - dragStartPosition).manhattanLength() < QApplication::startDragDistance()) + return; + QModelIndex idx = currentIndex(); + if (!idx.isValid()) { + return; + } + QDrag* drag = new QDrag(this); + QModelIndexList idxs; + idxs.append(idx); + QMimeData* mimeData = model()->mimeData(idxs); + QPixmap* iconPixmap = new QPixmap(); + QPainter painter(iconPixmap); + painter.setFont(QFont("Arial")); + painter.drawText(QPoint(100, 100), model()->data(idx, Qt::DisplayRole).toString()); + drag->setPixmap(*iconPixmap); + drag->setMimeData(mimeData); + Qt::DropAction dropAction = drag->exec(); +} + +WeaponModel::WeaponModel(int type) +{ + auto noWeapon = new WeaponItem(-1, "None"); + weapons.push_back(noWeapon); + if (type == 0) { + for (int i = 0; i < static_cast(Weapon_info.size()); i++) { + const auto& w = Weapon_info[i]; + if (w.subtype == WP_LASER || w.subtype == WP_BEAM) { + if (!w.wi_flags[Weapon::Info_Flags::No_fred]) { + auto newWeapon = new WeaponItem(i, w.name); + weapons.push_back(newWeapon); + } + } + } + } else if (type == 1) { + for (int i = 0; i < static_cast(Weapon_info.size()); i++) { + const auto& w = Weapon_info[i]; + if (w.subtype == WP_MISSILE) { + if (!w.wi_flags[Weapon::Info_Flags::No_fred]) { + auto newWeapon = new WeaponItem(i, w.name); + weapons.push_back(newWeapon); + } + } + } + } +} +WeaponModel::~WeaponModel() +{ + for (auto pointer : weapons) { + delete pointer; + } +} +int WeaponModel::rowCount(const QModelIndex& parent) const +{ + return static_cast(weapons.size()); +} +QVariant WeaponModel::data(const QModelIndex& index, int role) const +{ + if (role == Qt::DisplayRole) { + const QString out = + weapons[index.row()]->name; + return out; + } + if (role == Qt::UserRole) { + const size_t id = weapons[index.row()]->id; + return id; + } + return {}; +} +QMimeData* WeaponModel::mimeData(const QModelIndexList& indexes) const +{ + auto mimeData = new QMimeData(); + QByteArray encodedData; + QDataStream stream(&encodedData, QIODevice::WriteOnly); + for (auto& index : indexes) { + if (index.isValid()) { + int id = data(index, Qt::UserRole).toInt(); + stream << id; + } + } + + mimeData->setData("application/weaponid", encodedData); + + return mimeData; +} +WeaponItem::WeaponItem(const int id, const QString& name) : name(name), id(id) {} +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/widgets/weaponList.h b/qtfred/src/ui/widgets/weaponList.h new file mode 100644 index 00000000000..9511af93d9f --- /dev/null +++ b/qtfred/src/ui/widgets/weaponList.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +namespace fso { +namespace fred { +struct WeaponItem { + WeaponItem(const int id, const QString& name); + const QString name; + const int id; +}; +class WeaponModel : public QAbstractListModel { + Q_OBJECT + public: + WeaponModel(int type); + ~WeaponModel(); + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QMimeData* mimeData(const QModelIndexList& indexes) const; + QVector weapons; +}; +class weaponList : public QListView { + Q_OBJECT + public: + weaponList(QWidget* parent); + + protected: + void mousePressEvent(QMouseEvent* event); + void mouseMoveEvent(QMouseEvent* event); + QPoint dragStartPosition; + + private: +}; +} +} // namespace fso \ No newline at end of file diff --git a/qtfred/ui/ShipWeaponsDialog.ui b/qtfred/ui/ShipWeaponsDialog.ui new file mode 100644 index 00000000000..db8a7b71cbd --- /dev/null +++ b/qtfred/ui/ShipWeaponsDialog.ui @@ -0,0 +1,216 @@ + + + fso::fred::dialogs::ShipWeaponsDialog + + + + 0 + 0 + 764 + 622 + + + + Weapons Editor + + + true + + + + + + Mode + + + + + + Primary + + + + + + + Secondary + + + + + + + Tertiary + + + + + + + + + + + + Weapons + + + + + + QAbstractScrollArea::AdjustIgnored + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::DragOnly + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Set Selected + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Banks + + + + + + QAbstractScrollArea::AdjustToContentsOnFirstShow + + + QAbstractItemView::DropOnly + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectItems + + + false + + + + + + + + + + + + + + + + + Change AI + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + + + + fso::fred::weaponList + QListView +
ui/widgets/weaponList.h
+
+ + fso::fred::bankTree + QTreeView +
ui/widgets/bankTree.h
+
+
+ + + + + + buttonClose + clicked() + fso::fred::dialogs::ShipWeaponsDialog + accept() + + + 490 + 322 + + + 340 + 335 + + + + +
From 1a1d4bb057aa231c36ca7d4dd71cecbdff38c96c Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sun, 26 May 2024 12:26:27 +0100 Subject: [PATCH 327/466] Add TBL Viewer --- qtfred/source_groups.cmake | 4 + .../ShipEditor/WeaponsTBLViewerModel.cpp | 153 ++++++++++++++++++ .../ShipEditor/WeaponsTBLViewerModel.h | 22 +++ .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 29 ++++ .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 1 + .../dialogs/ShipEditor/WeaponsTBLViewer.cpp | 38 +++++ .../ui/dialogs/ShipEditor/WeaponsTBLViewer.h | 36 +++++ qtfred/ui/ShipWeaponsDialog.ui | 9 +- 8 files changed, 291 insertions(+), 1 deletion(-) create mode 100644 qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.cpp create mode 100644 qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h create mode 100644 qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp create mode 100644 qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 8ed8f3c3566..2a8121cc1a0 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -88,6 +88,8 @@ add_file_folder("Source/Mission/Dialogs/ShipEditor" src/mission/dialogs/ShipEditor/ShipTBLViewerModel.h src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h + src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.cpp + src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h src/mission/dialogs/ShipEditor/ShipPathsDialogModel.cpp src/mission/dialogs/ShipEditor/ShipPathsDialogModel.h src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h @@ -166,6 +168,8 @@ add_file_folder("Source/UI/Dialogs/ShipEditor" src/ui/dialogs/ShipEditor/ShipPathsDialog.cpp src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp + src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp + src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h ) add_file_folder("Source/UI/Util" diff --git a/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.cpp new file mode 100644 index 00000000000..5aa5587651c --- /dev/null +++ b/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.cpp @@ -0,0 +1,153 @@ +#include "WeaponsTBLViewerModel.h" +#include +namespace fso { +namespace fred { +namespace dialogs { +WeaponsTBLViewerModel::WeaponsTBLViewerModel(QObject* parent, EditorViewport* viewport, int wc) + : AbstractDialogModel(parent, viewport) +{ + initializeData(wc); +} +bool WeaponsTBLViewerModel::apply() +{ + return true; +} +void WeaponsTBLViewerModel::reject() {} +void WeaponsTBLViewerModel::initializeData(const int wc) +{ + char line[256], line2[256]{}, file_text[82]{}; + const weapon_info* sip = &Weapon_info[wc]; + int i, j, n, found = 0, comment = 0, num_files = 0; + CFILE* fp = nullptr; + SCP_vector tbl_file_names; + text.clear(); + + if (!sip) { + return; + } + + fp = cfopen("weapons.tbl", "r"); + Assert(fp); + + while (cfgets(line, 255, fp)) { + while (line[strlen(line) - 1] == '\n') + line[strlen(line) - 1] = 0; + + for (i = j = 0; line[i]; i++) { + if (line[i] == '/' && line[i + 1] == '/') + break; + if (line[i] == '/' && line[i + 1] == '*') { + comment = 1; + i++; + continue; + } + + if (line[i] == '*' && line[i + 1] == '/') { + comment = 0; + i++; + continue; + } + + if (!comment) { + line2[j++] = line[i]; + } + } + + line2[j] = 0; + if (!strnicmp(line2, "$Name:", 6)) { + drop_trailing_white_space(line2); + found = 0; + i = 6; + + while (line2[i] == ' ' || line2[i] == '\t' || line2[i] == '@') + i++; + + if (!stricmp(line2 + i, sip->name)) { + text += "-- weapons.tbl -------------------------------\r\n"; + found = 1; + } + } + + if (found) { + text += line; + text += "\r\n"; + } + } + + cfclose(fp); + + // done with ships.tbl, so now check all modular ship tables... + num_files = cf_get_file_list(tbl_file_names, CF_TYPE_TABLES, NOX("*-wep.tbm"), CF_SORT_REVERSE); + + for (n = 0; n < num_files; n++) { + tbl_file_names[n] += ".tbm"; + + fp = cfopen(tbl_file_names[n].c_str(), "r"); + Assert(fp); + + memset(line, 0, sizeof(line)); + memset(line2, 0, sizeof(line2)); + found = 0; + comment = 0; + + while (cfgets(line, 255, fp)) { + while (line[strlen(line) - 1] == '\n') + line[strlen(line) - 1] = 0; + + for (i = j = 0; line[i]; i++) { + if (line[i] == '/' && line[i + 1] == '/') + break; + + if (line[i] == '/' && line[i + 1] == '*') { + comment = 1; + i++; + continue; + } + + if (line[i] == '*' && line[i + 1] == '/') { + comment = 0; + i++; + continue; + } + + if (!comment) + line2[j++] = line[i]; + } + + line2[j] = 0; + if (!strnicmp(line2, "$Name:", 6)) { + drop_trailing_white_space(line2); + found = 0; + i = 6; + + while (line2[i] == ' ' || line2[i] == '\t' || line2[i] == '@') + i++; + + if (!stricmp(line2 + i, sip->name)) { + memset(file_text, 0, sizeof(file_text)); + snprintf(file_text, + sizeof(file_text) - 1, + "-- %s -------------------------------\r\n", + tbl_file_names[n].c_str()); + text += file_text; + found = 1; + } + } + + if (found) { + text += line; + text += "\r\n"; + } + } + + cfclose(fp); + } + modelChanged(); +} +SCP_string WeaponsTBLViewerModel::getText() const +{ + return text; +} +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h b/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h new file mode 100644 index 00000000000..dbcba064aa7 --- /dev/null +++ b/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h @@ -0,0 +1,22 @@ +#pragma once + +#include "../AbstractDialogModel.h" + +namespace fso { +namespace fred { +namespace dialogs { +class WeaponsTBLViewerModel : public AbstractDialogModel { + private: + SCP_string text; + + public: + WeaponsTBLViewerModel(QObject* parent, EditorViewport* viewport, int wc); + bool apply() override; + void reject() override; + void initializeData(const int ship_class); + + SCP_string getText() const; +}; +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index d19a4d2210c..d948026f429 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -1,6 +1,7 @@ #include "ShipWeaponsDialog.h" #include "ui_ShipWeaponsDialog.h" +#include "WeaponsTBLViewer.h" #include #include @@ -48,6 +49,10 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, &ShipWeaponsDialog::aiClassChanged); ui->listWeapons->setModel(weapons); ui->treeBanks->expandAll(); + connect(ui->listWeapons->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &ShipWeaponsDialog::updateUI); ui->treeBanks->header()->setSectionResizeMode(QHeaderView::ResizeToContents); updateUI(); } @@ -69,15 +74,29 @@ void ShipWeaponsDialog::on_setAllButton_clicked() bankModel->setWeapon(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); } } +void ShipWeaponsDialog::on_TBLButton_clicked() { + if (ui->listWeapons->currentIndex().data(Qt::UserRole).toInt() >= 0) { + auto dialog = new WeaponsTBLViewer(this, _viewport, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); + dialog->show(); + } else { + return; + } +} void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) { if (enabled) { if (mode == 0) { bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); dialogMode = 0; + delete weapons; + weapons = new WeaponModel(0); + ui->listWeapons->setModel(weapons); } else if (mode == 1) { bankModel = new BankTreeModel(_model->getSecondaryBanks(), this); dialogMode = 1; + delete weapons; + weapons = new WeaponModel(1); + ui->listWeapons->setModel(weapons); } else if (mode == 2) { // bankModel = new BankTreeModel(_model->getTertiaryBanks(), this); dialogMode = 2; @@ -90,6 +109,10 @@ void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); dialogMode = 0; } + connect(ui->listWeapons->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &ShipWeaponsDialog::updateUI); ui->treeBanks->setModel(bankModel); ui->treeBanks->expandAll(); } @@ -119,6 +142,12 @@ void ShipWeaponsDialog::updateUI() ui->AICombo->addItem(Ai_class_names[i], QVariant(i)); } ui->AICombo->setCurrentIndex(ui->AICombo->findData(m_currentAI)); + if (ui->listWeapons->selectionModel()->hasSelection() && + ui->listWeapons->currentIndex().data(Qt::UserRole).toInt() != -1) { + ui->TBLButton->setEnabled(true); + } else { + ui->TBLButton->setEnabled(false); + } } void ShipWeaponsDialog::aiClassChanged(const int index) { diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index 90168a8ea72..278cdd23b93 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -31,6 +31,7 @@ class ShipWeaponsDialog : public QDialog { private slots: void on_AIButton_clicked(); void on_setAllButton_clicked(); + void on_TBLButton_clicked(); private: std::unique_ptr ui; diff --git a/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp new file mode 100644 index 00000000000..1f9e7765540 --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp @@ -0,0 +1,38 @@ +#include "WeaponsTBLViewer.h" +#include "ui_ShipTBLViewer.h" + +#include + +#include + +namespace fso { +namespace fred { +namespace dialogs { +WeaponsTBLViewer::WeaponsTBLViewer(QWidget* parent, EditorViewport* viewport, int wc) + : QDialog(parent), ui(new Ui::ShipTBLViewer()), _viewport(viewport), + _model(new WeaponsTBLViewerModel(this, viewport, wc)) +{ + + ui->setupUi(this); + this->setWindowTitle("Weapon TBL Data"); + connect(_model.get(), &AbstractDialogModel::modelChanged, this, &WeaponsTBLViewer::updateUI); + + updateUI(); + + // Resize the dialog to the minimum size + resize(QDialog::sizeHint()); +} + +WeaponsTBLViewer::~WeaponsTBLViewer() = default; +void WeaponsTBLViewer::closeEvent(QCloseEvent* event) +{ + QDialog::closeEvent(event); +} +void WeaponsTBLViewer::updateUI() +{ + util::SignalBlockers blockers(this); + ui->TBLData->setPlainText(_model->getText().c_str()); +} +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h new file mode 100644 index 00000000000..ca9608af7af --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include + +namespace fso { +namespace fred { +namespace dialogs { + +namespace Ui { +class ShipTBLViewer; +} +class WeaponsTBLViewer : public QDialog { + Q_OBJECT + + public: + explicit WeaponsTBLViewer(QWidget* parent, EditorViewport* viewport, int wc); + ~WeaponsTBLViewer() override; + + protected: + void closeEvent(QCloseEvent*) override; + + private: + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + + int sc; + + void updateUI(); +}; +} // namespace dialogs +} // namespace fred +} // namespace fso \ No newline at end of file diff --git a/qtfred/ui/ShipWeaponsDialog.ui b/qtfred/ui/ShipWeaponsDialog.ui index db8a7b71cbd..899c32326b3 100644 --- a/qtfred/ui/ShipWeaponsDialog.ui +++ b/qtfred/ui/ShipWeaponsDialog.ui @@ -54,7 +54,7 @@ Weapons - + @@ -71,6 +71,13 @@ + + + + View Table + + +
From f5b2b1bbd0b303361b0515480a45518c8ca272c7 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sun, 26 May 2024 12:58:48 +0100 Subject: [PATCH 328/466] Add Comments --- .../ShipEditor/ShipWeaponsDialogModel.h | 9 ++++++ .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 31 ++++++++++++++----- .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 15 ++++++++- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index 37adbb4dd89..8006c054ba2 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -50,8 +50,17 @@ struct Bank { Banks* parent; }; namespace dialogs { +/** + * @brief QTFred's Weapons Editor Model + */ class ShipWeaponsDialogModel : public AbstractDialogModel { public: + /** + * @brief QTFred's Weapons Editor Model Constructer. + * @param [in/out] parent The dialogs parent. + * @param [in/out] viewport Editor viewport. + * @param [in] multi If editing multiple ships. + */ ShipWeaponsDialogModel(QObject* parent, EditorViewport* viewport, bool multi); // void initTertiary(int inst, bool first); diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index d948026f429..c2ccb5250db 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -17,12 +17,15 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, { ui->setupUi(this); + //Connect the mode change buttons connect(ui->radioPrimary, &QRadioButton::toggled, this, [this](bool param) { modeChanged(param, 0); }); connect(ui->radioSecondary, &QRadioButton::toggled, this, [this](bool param) { modeChanged(param, 1); }); connect(ui->radioTertiary, &QRadioButton::toggled, this, [this](bool param) { modeChanged(param, 2); }); connect(this, &QDialog::accepted, _model.get(), &ShipWeaponsDialogModel::apply); + + //Build the model of ship weapons and set inital mode. if (!_model->getPrimaryBanks().empty()) { const util::SignalBlockers blockers(this); bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); @@ -39,20 +42,26 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, Error("No Valid Weapon banks on ship"); } ui->treeBanks->setModel(bankModel); + ui->listWeapons->setModel(weapons); + + //Update the UI whenever selections change connect(ui->treeBanks->selectionModel(), &QItemSelectionModel::selectionChanged, this, &ShipWeaponsDialog::updateUI); + connect(ui->listWeapons->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &ShipWeaponsDialog::updateUI); + + //Setup ai combo box connect(ui->AICombo, static_cast(&QComboBox::currentIndexChanged), this, &ShipWeaponsDialog::aiClassChanged); - ui->listWeapons->setModel(weapons); + + //Resize Bank view ui->treeBanks->expandAll(); - connect(ui->listWeapons->selectionModel(), - &QItemSelectionModel::selectionChanged, - this, - &ShipWeaponsDialog::updateUI); ui->treeBanks->header()->setSectionResizeMode(QHeaderView::ResizeToContents); updateUI(); } @@ -109,6 +118,11 @@ void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); dialogMode = 0; } + //Reconnect Meacuse the model has changed + connect(ui->treeBanks->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &ShipWeaponsDialog::updateUI); connect(ui->listWeapons->selectionModel(), &QItemSelectionModel::selectionChanged, this, @@ -121,22 +135,25 @@ void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) void ShipWeaponsDialog::updateUI() { const util::SignalBlockers blockers(this); + //Radio Buttons ui->radioPrimary->setEnabled(!_model->getPrimaryBanks().empty()); ui->radioSecondary->setEnabled(!_model->getSecondaryBanks().empty()); ui->radioTertiary->setEnabled(false); - ui->treeBanks->expandAll(); + ui->treeBanks->expandAll(); + // Setall button if (ui->treeBanks->getTypeSelected() == 0) { ui->setAllButton->setEnabled(true); } else { ui->setAllButton->setEnabled(false); } - + // Change AI Button if (ui->treeBanks->getTypeSelected() == 1) { ui->AIButton->setEnabled(true); } else { ui->AIButton->setEnabled(false); } + // AI Combo Box ui->AICombo->clear(); for (int i = 0; i < Num_ai_classes; i++) { ui->AICombo->addItem(Ai_class_names[i], QVariant(i)); diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index 278cdd23b93..ab721579077 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -17,11 +17,19 @@ namespace dialogs { namespace Ui { class ShipWeaponsDialog; } - +/** + * @brief QTFred's Weapons Editor + */ class ShipWeaponsDialog : public QDialog { Q_OBJECT public: + /** + * @brief QTFred's Weapons Editor Constructer. + * @param [in/out] parent The dialogs parent. + * @param [in/out] viewport Editor viewport. + * @param [in] isMultiEdit If editing multiple ships. + */ explicit ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit); ~ShipWeaponsDialog() override; @@ -36,6 +44,11 @@ class ShipWeaponsDialog : public QDialog { private: std::unique_ptr ui; std::unique_ptr _model; + /** + * @brief Changes current weapon type. + * @param [in] enabled Always True + * @param [in] mode The mode to change to. 0 = Primary, 1 = Secondary + */ void modeChanged(const bool enabled, const int mode); EditorViewport* _viewport; void updateUI(); From 38f747950055f7499d17073a79f64f944eaf5723 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Mon, 27 May 2024 08:43:05 +0100 Subject: [PATCH 329/466] Appease GCC / Clang Appease GCC This is a silly error AAAAAGH Fix Model Sick of this Fix Supposed Shadowing Hopefully the last Or maybe not Please --- qtfred/source_groups.cmake | 4 +-- .../ShipEditor/ShipWeaponsDialogModel.cpp | 16 +++++------ .../src/ui/dialogs/ShipEditor/BankModel.cpp | 28 +++++++++---------- qtfred/src/ui/dialogs/ShipEditor/BankModel.h | 11 +++++--- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 3 +- .../dialogs/ShipEditor/WeaponsTBLViewer.cpp | 5 ++-- qtfred/src/ui/widgets/weaponList.cpp | 15 +++++----- qtfred/src/ui/widgets/weaponList.h | 2 +- 8 files changed, 43 insertions(+), 41 deletions(-) diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 2a8121cc1a0..cd10b93d19b 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -190,8 +190,8 @@ add_file_folder("Source/UI/Widgets" src/ui/widgets/ShipFlagCheckbox.cpp src/ui/widgets/weaponList.cpp src/ui/widgets/weaponList.h - src/ui/widgets/BankTree.cpp - src/ui/widgets/BankTree.h + src/ui/widgets/bankTree.cpp + src/ui/widgets/bankTree.h ) add_file_folder("UI" diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index efad53ec636..4ba51620ee4 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -1,8 +1,8 @@ #include "ShipWeaponsDialogModel.h" namespace fso { namespace fred { -Banks::Banks(const SCP_string& name, int aiIndex, int ship, int multiedit, ship_subsys* subsys) - : name(name), subsys(subsys), ship(ship), m_isMultiEdit(multiedit), initalAI(aiIndex) +Banks::Banks(const SCP_string& _name, int aiIndex, int _ship, int multiedit, ship_subsys* _subsys) + : m_isMultiEdit(multiedit) ,name(_name), subsys(_subsys), initalAI(aiIndex), ship(_ship) { aiClass = aiIndex; } @@ -74,13 +74,13 @@ int Banks::getInitalAI() { return initalAI; } -Bank::Bank(const int weaponId, const int bankId, const int ammoMax, const int ammo, Banks* parent) +Bank::Bank(const int _weaponId, const int _bankId, const int _ammoMax, const int _ammo, Banks* _parent) { - this->weaponId = weaponId; - this->bankId = bankId; - this->ammo = ammo; - this->ammoMax = ammoMax; - this->parent = parent; + this->weaponId = _weaponId; + this->bankId = _bankId; + this->ammo = _ammo; + this->ammoMax = _ammoMax; + this->parent = _parent; } int Bank::getWeaponId() const { diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp index 87e72d0e9f5..3f37aab3a62 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp @@ -4,7 +4,8 @@ #include namespace fso { namespace fred { -BankTreeItem::BankTreeItem(BankTreeItem* parentItem) : m_parentItem(parentItem) {} +BankTreeItem::BankTreeItem(BankTreeItem* parentItem, const QString& inName) : name(inName), m_parentItem(parentItem) { +} BankTreeItem::~BankTreeItem() { qDeleteAll(m_childItems); @@ -66,9 +67,8 @@ int BankTreeBank::getId() const return bank->getWeaponId(); } -BankTreeBank::BankTreeBank(Bank* bank, BankTreeItem* parentItem) : BankTreeItem(parentItem) +BankTreeBank::BankTreeBank(Bank* inBank, BankTreeItem* parentItem) : BankTreeItem(parentItem), bank(inBank) { - this->bank = bank; switch (bank->getWeaponId()) { case -2: this->name = "CONFLICT"; @@ -125,11 +125,9 @@ void BankTreeBank::setAmmo(int value) bank->setAmmo(value); } -BankTreeLabel::BankTreeLabel(const QString& name, Banks* banks, BankTreeItem* parentItem) : BankTreeItem(parentItem) -{ - this->name = name; - this->banks = banks; -} +BankTreeLabel::BankTreeLabel(const QString& inName, Banks* inBanks, BankTreeItem* parentItem) + : BankTreeItem(parentItem, inName), banks(inBanks) +{} QVariant BankTreeLabel::data(int column) const { @@ -145,6 +143,7 @@ QVariant BankTreeLabel::data(int column) const Qt::ItemFlags BankTreeLabel::getFlags(int column) const { + Q_UNUSED(column); return Qt::ItemIsSelectable; } @@ -156,6 +155,7 @@ void BankTreeLabel::setAIClass(int value) bool BankTreeLabel::setData(int column, const QVariant& value) { + Q_UNUSED(column); setAIClass(value.toInt()); return true; } @@ -191,6 +191,7 @@ void BankTreeModel::setupModelData(const SCP_vector& data, BankTreeItem* QVariant BankTreeModel::headerData(int section, Qt::Orientation orientation, int role) const { + Q_UNUSED(orientation); if (role == Qt::DisplayRole) { switch (section) { case 0: @@ -336,13 +337,7 @@ bool BankTreeModel::dropMimeData(const QMimeData* data, if (action == Qt::IgnoreAction) return true; - int beginRow; - - if (row != -1) - beginRow = row; - else if (parent.isValid()) - beginRow = parent.row(); - else + if (!(row != -1 || parent.isValid())) return false; QByteArray encodedData = data->data("application/weaponid"); @@ -366,6 +361,8 @@ void BankTreeModel::setWeapon(const QModelIndex& index, int data) const bool BankTreeRoot::setData(int column, const QVariant& value) { + Q_UNUSED(column); + Q_UNUSED(value); return false; } QVariant BankTreeRoot::data(int column) const @@ -383,6 +380,7 @@ QVariant BankTreeRoot::data(int column) const } Qt::ItemFlags BankTreeRoot::getFlags(int column) const { + Q_UNUSED(column); return {}; } diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h index df912ae5f6b..3fc99df4180 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h @@ -5,7 +5,7 @@ namespace fso { namespace fred { class BankTreeItem { public: - explicit BankTreeItem(BankTreeItem* parentItem = nullptr); + explicit BankTreeItem(BankTreeItem* parentItem = nullptr, const QString& inName = ""); virtual ~BankTreeItem(); virtual QVariant data(int column) const = 0; void appendChild(BankTreeItem* child); @@ -34,7 +34,7 @@ class BankTreeRoot : public BankTreeItem { }; class BankTreeBank : public BankTreeItem { public: - explicit BankTreeBank(Bank* bank, BankTreeItem* parentItem = nullptr); + explicit BankTreeBank(Bank* inBank, BankTreeItem* parentItem = nullptr); void setWeapon(int id); void setAmmo(int value); int getId() const; @@ -73,8 +73,11 @@ class BankTreeModel : public QAbstractItemModel { Qt::ItemFlags flags(const QModelIndex& index) const override; QStringList mimeTypes() const override; - bool - canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const; + bool canDropMimeData(const QMimeData* data, + Qt::DropAction action, + int row, + int column, + const QModelIndex& parent) const override; bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; void setWeapon(const QModelIndex& index, int data) const; diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index c2ccb5250db..2285e2d3157 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -78,7 +78,6 @@ void ShipWeaponsDialog::closeEvent(QCloseEvent* event) } void ShipWeaponsDialog::on_setAllButton_clicked() { - int test = 0; for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { bankModel->setWeapon(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); } @@ -112,7 +111,7 @@ void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) } else { _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Illegal Mode", - "Somehow an Illegal mode has been set. Get a coder.\n Illegal mode is " + mode, + "Somehow an Illegal mode has been set. Get a coder.\n Illegal mode is " + std::to_string(mode), {DialogButton::Ok}); ui->radioPrimary->toggled(true); bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); diff --git a/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp index 1f9e7765540..20f0f7bff85 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp @@ -1,4 +1,5 @@ #include "WeaponsTBLViewer.h" + #include "ui_ShipTBLViewer.h" #include @@ -9,8 +10,8 @@ namespace fso { namespace fred { namespace dialogs { WeaponsTBLViewer::WeaponsTBLViewer(QWidget* parent, EditorViewport* viewport, int wc) - : QDialog(parent), ui(new Ui::ShipTBLViewer()), _viewport(viewport), - _model(new WeaponsTBLViewerModel(this, viewport, wc)) + : QDialog(parent), ui(new Ui::ShipTBLViewer()), _model(new WeaponsTBLViewerModel(this, viewport, wc)), + _viewport(viewport) { ui->setupUi(this); diff --git a/qtfred/src/ui/widgets/weaponList.cpp b/qtfred/src/ui/widgets/weaponList.cpp index 3f67af99348..ede822c8481 100644 --- a/qtfred/src/ui/widgets/weaponList.cpp +++ b/qtfred/src/ui/widgets/weaponList.cpp @@ -6,9 +6,10 @@ weaponList::weaponList(QWidget* parent) : QListView(parent) {} void weaponList::mousePressEvent(QMouseEvent* event) { - if (event->button() == Qt::LeftButton) + if (event->button() == Qt::LeftButton) { dragStartPosition = event->pos(); - QListView::mousePressEvent(event); + } + QListView::mousePressEvent(event); } void weaponList::mouseMoveEvent(QMouseEvent* event) { @@ -30,7 +31,7 @@ void weaponList::mouseMoveEvent(QMouseEvent* event) painter.drawText(QPoint(100, 100), model()->data(idx, Qt::DisplayRole).toString()); drag->setPixmap(*iconPixmap); drag->setMimeData(mimeData); - Qt::DropAction dropAction = drag->exec(); + drag->exec(); } WeaponModel::WeaponModel(int type) @@ -67,17 +68,17 @@ WeaponModel::~WeaponModel() } int WeaponModel::rowCount(const QModelIndex& parent) const { + Q_UNUSED(parent); return static_cast(weapons.size()); } QVariant WeaponModel::data(const QModelIndex& index, int role) const { if (role == Qt::DisplayRole) { - const QString out = - weapons[index.row()]->name; + const QString out = weapons[index.row()]->name; return out; } if (role == Qt::UserRole) { - const size_t id = weapons[index.row()]->id; + const int id = weapons[index.row()]->id; return id; } return {}; @@ -98,6 +99,6 @@ QMimeData* WeaponModel::mimeData(const QModelIndexList& indexes) const return mimeData; } -WeaponItem::WeaponItem(const int id, const QString& name) : name(name), id(id) {} +WeaponItem::WeaponItem(const int inID, const QString& inName) : name(inName), id(inID) {} } // namespace fred } // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/widgets/weaponList.h b/qtfred/src/ui/widgets/weaponList.h index 9511af93d9f..8a9d02a0b3e 100644 --- a/qtfred/src/ui/widgets/weaponList.h +++ b/qtfred/src/ui/widgets/weaponList.h @@ -20,7 +20,7 @@ class WeaponModel : public QAbstractListModel { ~WeaponModel(); int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; - QMimeData* mimeData(const QModelIndexList& indexes) const; + QMimeData* mimeData(const QModelIndexList& indexes) const override; QVector weapons; }; class weaponList : public QListView { From 8bf7f66483ff3566bdc5fe0efbc25306c6d1772b Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sat, 9 Aug 2025 15:50:32 +0100 Subject: [PATCH 330/466] Adress Feedback and other bug fixes --- .../ShipEditor/ShipWeaponsDialogModel.cpp | 61 +++++++-------- .../ShipEditor/ShipWeaponsDialogModel.h | 6 +- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 77 +++++++++++++------ .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 12 ++- qtfred/ui/ShipWeaponsDialog.ui | 6 +- 5 files changed, 99 insertions(+), 63 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index 4ba51620ee4..5a272e9c521 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -49,18 +49,17 @@ int Banks::getAiClass() const void Banks::setAiClass(int newClass) { if (m_isMultiEdit) { - object* ptr; - int inst; - ptr = GET_FIRST(&obj_used_list); + object* ptr = GET_FIRST(&obj_used_list); while (ptr != END_OF_LIST(&obj_used_list)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { - inst = ptr->instance; + int inst = ptr->instance; if (name == "Pilot") { Ships[inst].ai_index = newClass; } else { subsys->weapons.ai_class = newClass; } } + ptr = GET_NEXT(ptr); } } else { if (name == "Pilot") { @@ -70,7 +69,7 @@ void Banks::setAiClass(int newClass) } } } -int Banks::getInitalAI() +int Banks::getInitalAI() const { return initalAI; } @@ -130,30 +129,29 @@ void ShipWeaponsDialogModel::initializeData(bool isMultiEdit) m_isMultiEdit = isMultiEdit; PrimaryBanks.clear(); SecondaryBanks.clear(); - int inst; - bool first = true; - object* ptr; m_ship = _editor->cur_ship; if (m_ship == -1) m_ship = Objects[_editor->currentObject].instance; if (m_isMultiEdit) { - ptr = GET_FIRST(&obj_used_list); + object* ptr = GET_FIRST(&obj_used_list); + bool first = true; while (ptr != END_OF_LIST(&obj_used_list)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { - inst = ptr->instance; + int inst = ptr->instance; if (!(Ship_info[Ships[inst].ship_info_index].is_big_or_huge())) - big = 0; + big = false; initPrimary(inst, first); initSecondary(inst, first); // initTertiary(inst, first); first = false; } + ptr = GET_NEXT(ptr); } } else { if (!(Ship_info[Ships[m_ship].ship_info_index].is_big_or_huge())) - big = 0; + big = false; initPrimary(m_ship, true); initSecondary(m_ship, true); } @@ -196,12 +194,12 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) } } } else { - for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { - if (PrimaryBanks[0]->getByBankId(i)->getWeaponId() != Ships[inst].weapons.primary_bank_weapons[i]) { - PrimaryBanks[0]->getByBankId(i)->setWeapon(-2); + for (int i = 0; i < PrimaryBanks[0]->getBanks().size(); i++) { + if (PrimaryBanks[0]->getBanks()[i]->getWeaponId() != Ships[inst].weapons.primary_bank_weapons[i]) { + PrimaryBanks[0]->getBanks()[i]->setWeapon(-2); } - if (PrimaryBanks[0]->getByBankId(i)->getAmmo() != Ships[inst].weapons.primary_bank_ammo[i]) { - PrimaryBanks[0]->getByBankId(i)->setAmmo(-2); + if (PrimaryBanks[0]->getBanks()[i]->getAmmo() != Ships[inst].weapons.primary_bank_ammo[i]) { + PrimaryBanks[0]->getBanks()[i]->setAmmo(-2); } } ship_subsys* ssl = &Ships[inst].subsys_list; @@ -211,12 +209,12 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) if (psub->type == SUBSYSTEM_TURRET) { for (auto banks : PrimaryBanks) { if (banks->getSubsys() == pss) { - for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) { - if (banks->getByBankId(i)->getWeaponId() != pss->weapons.primary_bank_weapons[i]) { - banks->getByBankId(i)->setWeapon(-2); + for (int i = 0; i < banks->getBanks().size(); i++) { + if (banks->getBanks()[i]->getWeaponId() != pss->weapons.primary_bank_weapons[i]) { + banks->getBanks()[i]->setWeapon(-2); } - if (banks->getByBankId(i)->getAmmo() != pss->weapons.primary_bank_ammo[i]) { - banks->getByBankId(i)->setAmmo(-2); + if (banks->getBanks()[i]->getAmmo() != pss->weapons.primary_bank_ammo[i]) { + banks->getBanks()[i]->setAmmo(-2); } } } @@ -263,12 +261,12 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) } } } else { - for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { - if (SecondaryBanks[0]->getByBankId(i)->getWeaponId() != Ships[inst].weapons.secondary_bank_weapons[i]) { - SecondaryBanks[0]->getByBankId(i)->setWeapon(-2); + for (int i = 0; i < SecondaryBanks[0]->getBanks().size(); i++) { + if (SecondaryBanks[0]->getBanks()[i]->getWeaponId() != Ships[inst].weapons.secondary_bank_weapons[i]) { + SecondaryBanks[0]->getBanks()[i]->setWeapon(-2); } - if (SecondaryBanks[0]->getByBankId(i)->getAmmo() != Ships[inst].weapons.secondary_bank_ammo[i]) { - SecondaryBanks[0]->getByBankId(i)->setAmmo(-2); + if (SecondaryBanks[0]->getBanks()[i]->getAmmo() != Ships[inst].weapons.secondary_bank_ammo[i]) { + SecondaryBanks[0]->getBanks()[i]->setAmmo(-2); } } ship_subsys* ssl = &Ships[inst].subsys_list; @@ -278,7 +276,7 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) if (psub->type == SUBSYSTEM_TURRET) { for (auto banks : SecondaryBanks) { if (banks->getSubsys() == pss) { - for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) { + for (int i = 0; i < banks->getBanks().size(); i++) { if (banks->getByBankId(i)->getWeaponId() != pss->weapons.secondary_bank_weapons[i]) { banks->getByBankId(i)->setWeapon(-2); } @@ -338,14 +336,13 @@ void ShipWeaponsDialogModel::saveShip(int inst) bool ShipWeaponsDialogModel::apply() { if (m_isMultiEdit) { - object* ptr; - int inst; - ptr = GET_FIRST(&obj_used_list); + object* ptr = GET_FIRST(&obj_used_list); while (ptr != END_OF_LIST(&obj_used_list)) { if (((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) && (ptr->flags[Object::Object_Flags::Marked])) { - inst = ptr->instance; + int inst = ptr->instance; saveShip(inst); } + ptr = GET_NEXT(ptr); } } else { saveShip(m_ship); diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index 8006c054ba2..f4e1edf4aca 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -21,7 +21,7 @@ struct Banks { int getAiClass() const; void setAiClass(int); bool m_isMultiEdit; - int getInitalAI(); + int getInitalAI() const; private: SCP_string name; ship_subsys* subsys; @@ -77,9 +77,9 @@ class ShipWeaponsDialogModel : public AbstractDialogModel { void initSecondary(int inst, bool first); void initializeData(bool multi); - int m_isMultiEdit; + bool m_isMultiEdit; int m_ship; - int big = 1; + bool big = true; SCP_vector PrimaryBanks; SCP_vector SecondaryBanks; // SCP_vector TertiaryBanks; diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 2285e2d3157..74ec1c1ddce 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -17,13 +17,8 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, { ui->setupUi(this); - //Connect the mode change buttons - connect(ui->radioPrimary, &QRadioButton::toggled, this, [this](bool param) { modeChanged(param, 0); }); - connect(ui->radioSecondary, &QRadioButton::toggled, this, [this](bool param) { modeChanged(param, 1); }); - connect(ui->radioTertiary, &QRadioButton::toggled, this, [this](bool param) { modeChanged(param, 2); }); - - connect(this, &QDialog::accepted, _model.get(), &ShipWeaponsDialogModel::apply); + //connect(this, &QDialog::accepted, _model.get(), &ShipWeaponsDialogModel::apply); //Build the model of ship weapons and set inital mode. if (!_model->getPrimaryBanks().empty()) { @@ -55,10 +50,10 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, &ShipWeaponsDialog::updateUI); //Setup ai combo box - connect(ui->AICombo, - static_cast(&QComboBox::currentIndexChanged), - this, - &ShipWeaponsDialog::aiClassChanged); + //connect(ui->AICombo, + //static_cast(&QComboBox::currentIndexChanged), + //this, + //&ShipWeaponsDialog::aiClassChanged); //Resize Bank view ui->treeBanks->expandAll(); @@ -71,10 +66,28 @@ ShipWeaponsDialog::~ShipWeaponsDialog() { delete weapons; } +void ShipWeaponsDialog::accept() { + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void ShipWeaponsDialog::reject() { + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close +} + void ShipWeaponsDialog::closeEvent(QCloseEvent* event) { - accept(); - QDialog::closeEvent(event); + reject(); + event->ignore(); } void ShipWeaponsDialog::on_setAllButton_clicked() { @@ -82,7 +95,7 @@ void ShipWeaponsDialog::on_setAllButton_clicked() bankModel->setWeapon(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); } } -void ShipWeaponsDialog::on_TBLButton_clicked() { +void ShipWeaponsDialog::on_tblButton_clicked() { if (ui->listWeapons->currentIndex().data(Qt::UserRole).toInt() >= 0) { auto dialog = new WeaponsTBLViewer(this, _viewport, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); dialog->show(); @@ -90,6 +103,19 @@ void ShipWeaponsDialog::on_TBLButton_clicked() { return; } } +void ShipWeaponsDialog::on_radioPrimary_toggled(bool checked) { + modeChanged(checked, 0); +} +void ShipWeaponsDialog::on_radioSecondary_toggled(bool checked) +{ + modeChanged(checked, 1); +} +void ShipWeaponsDialog::on_radioTertiary_toggled(bool checked) { + modeChanged(checked, 2); +} +void ShipWeaponsDialog::on_aiCombo_currentIndexChanged(int index) { + aiClassChanged(index); +} void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) { if (enabled) { @@ -117,7 +143,7 @@ void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); dialogMode = 0; } - //Reconnect Meacuse the model has changed + //Reconnect beacuse the model has changed connect(ui->treeBanks->selectionModel(), &QItemSelectionModel::selectionChanged, this, @@ -148,34 +174,39 @@ void ShipWeaponsDialog::updateUI() } // Change AI Button if (ui->treeBanks->getTypeSelected() == 1) { - ui->AIButton->setEnabled(true); + ui->aiButton->setEnabled(true); } else { - ui->AIButton->setEnabled(false); + ui->aiButton->setEnabled(false); } // AI Combo Box - ui->AICombo->clear(); + ui->aiCombo->clear(); for (int i = 0; i < Num_ai_classes; i++) { - ui->AICombo->addItem(Ai_class_names[i], QVariant(i)); + ui->aiCombo->addItem(Ai_class_names[i], QVariant(i)); } - ui->AICombo->setCurrentIndex(ui->AICombo->findData(m_currentAI)); + ui->aiCombo->setCurrentIndex(ui->aiCombo->findData(m_currentAI)); if (ui->listWeapons->selectionModel()->hasSelection() && ui->listWeapons->currentIndex().data(Qt::UserRole).toInt() != -1) { - ui->TBLButton->setEnabled(true); + ui->tblButton->setEnabled(true); } else { - ui->TBLButton->setEnabled(false); + ui->tblButton->setEnabled(false); } } void ShipWeaponsDialog::aiClassChanged(const int index) { - m_currentAI = ui->AICombo->itemData(index).toInt(); + m_currentAI = ui->aiCombo->itemData(index).toInt(); } -void ShipWeaponsDialog::on_AIButton_clicked() { +void ShipWeaponsDialog::on_aiButton_clicked() { for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { bankModel->setData(index, m_currentAI); } } +void ShipWeaponsDialog::on_aiButton_clicked() +{ + accept(); +} + } // namespace dialogs } // namespace fred } // namespace fso \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index ab721579077..a635b2b743b 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -33,13 +33,21 @@ class ShipWeaponsDialog : public QDialog { explicit ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit); ~ShipWeaponsDialog() override; + void accept() override; + void reject() override; + protected: void closeEvent(QCloseEvent*) override; private slots: - void on_AIButton_clicked(); + void on_buttonClose_clicked(); + void on_aiButton_clicked(); void on_setAllButton_clicked(); - void on_TBLButton_clicked(); + void on_tblButton_clicked(); + void on_radioPrimary_toggled(bool checked); + void on_radioSecondary_toggled(bool checked); + void on_radioTertiary_toggled(bool checked); + void on_aiCombo_currentIndexChanged(int index); private: std::unique_ptr ui; diff --git a/qtfred/ui/ShipWeaponsDialog.ui b/qtfred/ui/ShipWeaponsDialog.ui index 899c32326b3..4d0b1c0a5ce 100644 --- a/qtfred/ui/ShipWeaponsDialog.ui +++ b/qtfred/ui/ShipWeaponsDialog.ui @@ -72,7 +72,7 @@
- + View Table @@ -150,10 +150,10 @@ - + - + Change AI From 95fea193aed1e4b8ff3062ba38893d34a4ca8591 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sat, 9 Aug 2025 16:05:00 +0100 Subject: [PATCH 331/466] Couple More --- .../dialogs/ShipEditor/ShipEditorDialog.cpp | 5 +++-- qtfred/ui/ShipWeaponsDialog.ui | 19 +------------------ 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index c4b4a1dd4ca..0a68ad235b6 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -846,8 +846,9 @@ void ShipEditorDialog::on_deleteButton_clicked() } void ShipEditorDialog::on_weaponsButton_clicked() { - auto weaponsDialog = new dialogs::ShipWeaponsDialog(this, _viewport, getIfMultipleShips()); - weaponsDialog->show(); + auto dialog = new dialogs::PlayerOrdersDialog(this, _viewport, getIfMultipleShips()); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); } void ShipEditorDialog::on_playerOrdersButton_clicked() { diff --git a/qtfred/ui/ShipWeaponsDialog.ui b/qtfred/ui/ShipWeaponsDialog.ui index 4d0b1c0a5ce..0d6329025fa 100644 --- a/qtfred/ui/ShipWeaponsDialog.ui +++ b/qtfred/ui/ShipWeaponsDialog.ui @@ -202,22 +202,5 @@ - - - buttonClose - clicked() - fso::fred::dialogs::ShipWeaponsDialog - accept() - - - 490 - 322 - - - 340 - 335 - - - - +
From cf4f1b2ac6ae9550fffb872cf07f25972382bb11 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sat, 9 Aug 2025 16:09:14 +0100 Subject: [PATCH 332/466] Missed this --- qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 74ec1c1ddce..ff3acfec546 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -8,6 +8,7 @@ #include #include +#include namespace fso { namespace fred { namespace dialogs { @@ -202,7 +203,7 @@ void ShipWeaponsDialog::on_aiButton_clicked() { } } -void ShipWeaponsDialog::on_aiButton_clicked() +void ShipWeaponsDialog::on_buttonClose_clicked() { accept(); } From 3524679c9e956f3f266d2464e4be36531baff4d7 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sat, 9 Aug 2025 16:11:15 +0100 Subject: [PATCH 333/466] Fix Copy/Paste Error --- qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index 0a68ad235b6..f3fccd36992 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -846,7 +846,7 @@ void ShipEditorDialog::on_deleteButton_clicked() } void ShipEditorDialog::on_weaponsButton_clicked() { - auto dialog = new dialogs::PlayerOrdersDialog(this, _viewport, getIfMultipleShips()); + auto dialog = new dialogs::ShipWeaponsDialog(this, _viewport, getIfMultipleShips()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } From ec3fc3477df5fb1c230166fcd476bc1b4fb852c0 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sat, 9 Aug 2025 16:59:27 +0100 Subject: [PATCH 334/466] Refresh UI on Drag Drop --- qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp | 9 +++++++-- qtfred/src/ui/dialogs/ShipEditor/BankModel.h | 2 +- qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp | 8 ++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp index 3f37aab3a62..9b5b7f560a0 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp @@ -250,7 +250,9 @@ bool BankTreeModel::setData(const QModelIndex& index, const QVariant& value, int return false; } bool result = item->setData(index.column(), value); - + QVector roles; + roles.append(role); + QAbstractItemModel::dataChanged(index, index, roles); return result; } @@ -348,14 +350,17 @@ bool BankTreeModel::dropMimeData(const QMimeData* data, setWeapon(parent, id); } return true; + } -void BankTreeModel::setWeapon(const QModelIndex& index, int data) const +void BankTreeModel::setWeapon(const QModelIndex& index, int data) { auto item = dynamic_cast(this->getItem(index)); Assert(item != nullptr); if (item != nullptr) { item->setWeapon(data); + QVector roles; + QAbstractItemModel::dataChanged(index, index, roles); } } diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h index 3fc99df4180..c87854608ce 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h @@ -80,7 +80,7 @@ class BankTreeModel : public QAbstractItemModel { const QModelIndex& parent) const override; bool dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; - void setWeapon(const QModelIndex& index, int data) const; + void setWeapon(const QModelIndex& index, int data); QVariant headerData(int section, Qt::Orientation orientation, int role) const override; bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index ff3acfec546..9cf9db6892e 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -40,6 +40,10 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, ui->treeBanks->setModel(bankModel); ui->listWeapons->setModel(weapons); + connect(ui->treeBanks->selectionModel()->model(), + &QAbstractItemModel::dataChanged, + this, + &ShipWeaponsDialog::updateUI); //Update the UI whenever selections change connect(ui->treeBanks->selectionModel(), &QItemSelectionModel::selectionChanged, @@ -145,6 +149,10 @@ void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) dialogMode = 0; } //Reconnect beacuse the model has changed + connect(ui->treeBanks->selectionModel()->model(), + &QAbstractItemModel::dataChanged, + this, + &ShipWeaponsDialog::updateUI); connect(ui->treeBanks->selectionModel(), &QItemSelectionModel::selectionChanged, this, From c1c16c2fcf2b27497be74dfbd461339273b20f69 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sat, 9 Aug 2025 17:14:14 +0100 Subject: [PATCH 335/466] GCC Apeasment --- .../mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index 5a272e9c521..9e162b3edb1 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -194,7 +194,7 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) } } } else { - for (int i = 0; i < PrimaryBanks[0]->getBanks().size(); i++) { + for (int i = 0; i < static_cast(PrimaryBanks[0]->getBanks().size()); i++) { if (PrimaryBanks[0]->getBanks()[i]->getWeaponId() != Ships[inst].weapons.primary_bank_weapons[i]) { PrimaryBanks[0]->getBanks()[i]->setWeapon(-2); } @@ -209,7 +209,7 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) if (psub->type == SUBSYSTEM_TURRET) { for (auto banks : PrimaryBanks) { if (banks->getSubsys() == pss) { - for (int i = 0; i < banks->getBanks().size(); i++) { + for (int i = 0; i < static_cast (banks->getBanks().size()); i++) { if (banks->getBanks()[i]->getWeaponId() != pss->weapons.primary_bank_weapons[i]) { banks->getBanks()[i]->setWeapon(-2); } @@ -261,7 +261,7 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) } } } else { - for (int i = 0; i < SecondaryBanks[0]->getBanks().size(); i++) { + for (int i = 0; i < static_cast (SecondaryBanks[0]->getBanks().size()); i++) { if (SecondaryBanks[0]->getBanks()[i]->getWeaponId() != Ships[inst].weapons.secondary_bank_weapons[i]) { SecondaryBanks[0]->getBanks()[i]->setWeapon(-2); } @@ -276,7 +276,7 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) if (psub->type == SUBSYSTEM_TURRET) { for (auto banks : SecondaryBanks) { if (banks->getSubsys() == pss) { - for (int i = 0; i < banks->getBanks().size(); i++) { + for (int i = 0; i < static_cast (banks->getBanks().size()); i++) { if (banks->getByBankId(i)->getWeaponId() != pss->weapons.secondary_bank_weapons[i]) { banks->getByBankId(i)->setWeapon(-2); } From a7ec8d958b160013dee38ab64f3e7ed4ed76851d Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sat, 9 Aug 2025 18:35:57 +0100 Subject: [PATCH 336/466] And now Clang --- .../ShipEditor/ShipWeaponsDialogModel.cpp | 18 +++--- .../ShipEditor/ShipWeaponsDialogModel.h | 11 ++-- .../ShipEditor/WeaponsTBLViewerModel.cpp | 13 ++-- .../src/ui/dialogs/ShipEditor/BankModel.cpp | 28 ++++----- qtfred/src/ui/dialogs/ShipEditor/BankModel.h | 9 ++- .../dialogs/ShipEditor/ShipWeaponsDialog.cpp | 62 ++++++++++--------- .../ui/dialogs/ShipEditor/ShipWeaponsDialog.h | 10 +-- .../dialogs/ShipEditor/WeaponsTBLViewer.cpp | 8 +-- .../ui/dialogs/ShipEditor/WeaponsTBLViewer.h | 9 +-- qtfred/src/ui/widgets/bankTree.cpp | 8 +-- qtfred/src/ui/widgets/bankTree.h | 25 ++++---- qtfred/src/ui/widgets/weaponList.cpp | 12 ++-- qtfred/src/ui/widgets/weaponList.h | 21 +++---- 13 files changed, 105 insertions(+), 129 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp index 9e162b3edb1..bbd03657db4 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -1,8 +1,7 @@ #include "ShipWeaponsDialogModel.h" -namespace fso { -namespace fred { -Banks::Banks(const SCP_string& _name, int aiIndex, int _ship, int multiedit, ship_subsys* _subsys) - : m_isMultiEdit(multiedit) ,name(_name), subsys(_subsys), initalAI(aiIndex), ship(_ship) +namespace fso::fred { +Banks::Banks(SCP_string _name, int aiIndex, int _ship, int multiedit, ship_subsys* _subsys) + : m_isMultiEdit(multiedit), name(std::move(_name)), subsys(_subsys), initalAI(aiIndex), ship(_ship) { aiClass = aiIndex; } @@ -34,7 +33,7 @@ bool Banks::empty() const { return banks.empty(); } -const SCP_vector Banks::getBanks() const +SCP_vector Banks::getBanks() const { return banks; } @@ -209,7 +208,7 @@ void ShipWeaponsDialogModel::initPrimary(int inst, bool first) if (psub->type == SUBSYSTEM_TURRET) { for (auto banks : PrimaryBanks) { if (banks->getSubsys() == pss) { - for (int i = 0; i < static_cast (banks->getBanks().size()); i++) { + for (int i = 0; i < static_cast(banks->getBanks().size()); i++) { if (banks->getBanks()[i]->getWeaponId() != pss->weapons.primary_bank_weapons[i]) { banks->getBanks()[i]->setWeapon(-2); } @@ -261,7 +260,7 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) } } } else { - for (int i = 0; i < static_cast (SecondaryBanks[0]->getBanks().size()); i++) { + for (int i = 0; i < static_cast(SecondaryBanks[0]->getBanks().size()); i++) { if (SecondaryBanks[0]->getBanks()[i]->getWeaponId() != Ships[inst].weapons.secondary_bank_weapons[i]) { SecondaryBanks[0]->getBanks()[i]->setWeapon(-2); } @@ -276,7 +275,7 @@ void ShipWeaponsDialogModel::initSecondary(int inst, bool first) if (psub->type == SUBSYSTEM_TURRET) { for (auto banks : SecondaryBanks) { if (banks->getSubsys() == pss) { - for (int i = 0; i < static_cast (banks->getBanks().size()); i++) { + for (int i = 0; i < static_cast(banks->getBanks().size()); i++) { if (banks->getByBankId(i)->getWeaponId() != pss->weapons.secondary_bank_weapons[i]) { banks->getByBankId(i)->setWeapon(-2); } @@ -372,5 +371,4 @@ SCP_vector ShipWeaponsDialogModel::getSecondaryBanks() const } */ } // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h index f4e1edf4aca..d63908e1c6b 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -4,11 +4,10 @@ #include -namespace fso { -namespace fred { +namespace fso::fred { struct Bank; struct Banks { - Banks(const SCP_string& name, int aiIndex, int ship,int multiedit, ship_subsys* subsys = nullptr); + Banks(SCP_string name, int aiIndex, int ship, int multiedit, ship_subsys* subsys = nullptr); public: void add(Bank*); @@ -17,11 +16,12 @@ struct Banks { int getShip() const; ship_subsys* getSubsys() const; bool empty() const; - const SCP_vector getBanks() const; + SCP_vector getBanks() const; int getAiClass() const; void setAiClass(int); bool m_isMultiEdit; int getInitalAI() const; + private: SCP_string name; ship_subsys* subsys; @@ -85,5 +85,4 @@ class ShipWeaponsDialogModel : public AbstractDialogModel { // SCP_vector TertiaryBanks; }; } // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.cpp index 5aa5587651c..54f5a292eca 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.cpp @@ -1,8 +1,7 @@ #include "WeaponsTBLViewerModel.h" + #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { WeaponsTBLViewerModel::WeaponsTBLViewerModel(QObject* parent, EditorViewport* viewport, int wc) : AbstractDialogModel(parent, viewport) { @@ -34,8 +33,8 @@ void WeaponsTBLViewerModel::initializeData(const int wc) line[strlen(line) - 1] = 0; for (i = j = 0; line[i]; i++) { - if (line[i] == '/' && line[i + 1] == '/') - break; + if (line[i] == '/' && line[i + 1] == '/') + break; if (line[i] == '/' && line[i + 1] == '*') { comment = 1; i++; @@ -148,6 +147,4 @@ SCP_string WeaponsTBLViewerModel::getText() const { return text; } -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp index 9b5b7f560a0..e5d6f148004 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp @@ -2,10 +2,8 @@ #include #include -namespace fso { -namespace fred { -BankTreeItem::BankTreeItem(BankTreeItem* parentItem, const QString& inName) : name(inName), m_parentItem(parentItem) { -} +namespace fso::fred { +BankTreeItem::BankTreeItem(BankTreeItem* parentItem, QString inName) : name(std::move(inName)), m_parentItem(parentItem) {} BankTreeItem::~BankTreeItem() { qDeleteAll(m_childItems); @@ -127,14 +125,14 @@ void BankTreeBank::setAmmo(int value) BankTreeLabel::BankTreeLabel(const QString& inName, Banks* inBanks, BankTreeItem* parentItem) : BankTreeItem(parentItem, inName), banks(inBanks) -{} +{ +} QVariant BankTreeLabel::data(int column) const { switch (column) { case 0: - return name + " (" + Ai_class_names[banks->getAiClass()] + - ")"; + return name + " (" + Ai_class_names[banks->getAiClass()] + ")"; break; default: return {}; @@ -144,20 +142,20 @@ QVariant BankTreeLabel::data(int column) const Qt::ItemFlags BankTreeLabel::getFlags(int column) const { Q_UNUSED(column); - return Qt::ItemIsSelectable; + return Qt::ItemIsSelectable; } void BankTreeLabel::setAIClass(int value) { Assert(banks != nullptr); - banks->setAiClass(value); + banks->setAiClass(value); } bool BankTreeLabel::setData(int column, const QVariant& value) { - Q_UNUSED(column); - setAIClass(value.toInt()); - return true; + Q_UNUSED(column); + setAIClass(value.toInt()); + return true; } bool BankTreeBank::setData(int column, const QVariant& value) @@ -339,7 +337,7 @@ bool BankTreeModel::dropMimeData(const QMimeData* data, if (action == Qt::IgnoreAction) return true; - if (!(row != -1 || parent.isValid())) + if (row == -1 && !parent.isValid()) return false; QByteArray encodedData = data->data("application/weaponid"); @@ -350,7 +348,6 @@ bool BankTreeModel::dropMimeData(const QMimeData* data, setWeapon(parent, id); } return true; - } void BankTreeModel::setWeapon(const QModelIndex& index, int data) @@ -404,5 +401,4 @@ int BankTreeModel::checktype(const QModelIndex index) const } return type; } -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h index c87854608ce..a0b42d475e4 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/BankModel.h +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h @@ -1,11 +1,11 @@ #pragma once #include "mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h" + #include -namespace fso { -namespace fred { +namespace fso::fred { class BankTreeItem { public: - explicit BankTreeItem(BankTreeItem* parentItem = nullptr, const QString& inName = ""); + explicit BankTreeItem(BankTreeItem* parentItem = nullptr, QString inName = ""); virtual ~BankTreeItem(); virtual QVariant data(int column) const = 0; void appendChild(BankTreeItem* child); @@ -91,5 +91,4 @@ class BankTreeModel : public QAbstractItemModel { BankTreeItem* getItem(const QModelIndex index) const; static void setupModelData(const SCP_vector& data, BankTreeItem* parent); }; -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp index 9cf9db6892e..87be9cc36be 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -1,27 +1,24 @@ #include "ShipWeaponsDialog.h" -#include "ui_ShipWeaponsDialog.h" #include "WeaponsTBLViewer.h" +#include "ui_ShipWeaponsDialog.h" +#include #include #include #include #include -#include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, bool isMultiEdit) : QDialog(parent), ui(new Ui::ShipWeaponsDialog()), _model(new ShipWeaponsDialogModel(this, viewport, isMultiEdit)), _viewport(viewport) { ui->setupUi(this); + // connect(this, &QDialog::accepted, _model.get(), &ShipWeaponsDialogModel::apply); - //connect(this, &QDialog::accepted, _model.get(), &ShipWeaponsDialogModel::apply); - - //Build the model of ship weapons and set inital mode. + // Build the model of ship weapons and set inital mode. if (!_model->getPrimaryBanks().empty()) { const util::SignalBlockers blockers(this); bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); @@ -44,7 +41,7 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, &QAbstractItemModel::dataChanged, this, &ShipWeaponsDialog::updateUI); - //Update the UI whenever selections change + // Update the UI whenever selections change connect(ui->treeBanks->selectionModel(), &QItemSelectionModel::selectionChanged, this, @@ -54,24 +51,26 @@ ShipWeaponsDialog::ShipWeaponsDialog(QDialog* parent, EditorViewport* viewport, this, &ShipWeaponsDialog::updateUI); - //Setup ai combo box - //connect(ui->AICombo, - //static_cast(&QComboBox::currentIndexChanged), - //this, - //&ShipWeaponsDialog::aiClassChanged); + // Setup ai combo box + // connect(ui->AICombo, + // static_cast(&QComboBox::currentIndexChanged), + // this, + //&ShipWeaponsDialog::aiClassChanged); - //Resize Bank view + // Resize Bank view ui->treeBanks->expandAll(); ui->treeBanks->header()->setSectionResizeMode(QHeaderView::ResizeToContents); updateUI(); } -ShipWeaponsDialog::~ShipWeaponsDialog() { +ShipWeaponsDialog::~ShipWeaponsDialog() +{ delete bankModel; delete weapons; } -void ShipWeaponsDialog::accept() { +void ShipWeaponsDialog::accept() +{ // If apply() returns true, close the dialog if (_model->apply()) { QDialog::accept(); @@ -79,7 +78,8 @@ void ShipWeaponsDialog::accept() { // else: validation failed, don’t close } -void ShipWeaponsDialog::reject() { +void ShipWeaponsDialog::reject() +{ // Asks the user if they want to save changes, if any // If they do, it runs _model->apply() and returns the success value // If they don't, it runs _model->reject() and returns true @@ -100,7 +100,8 @@ void ShipWeaponsDialog::on_setAllButton_clicked() bankModel->setWeapon(index, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); } } -void ShipWeaponsDialog::on_tblButton_clicked() { +void ShipWeaponsDialog::on_tblButton_clicked() +{ if (ui->listWeapons->currentIndex().data(Qt::UserRole).toInt() >= 0) { auto dialog = new WeaponsTBLViewer(this, _viewport, ui->listWeapons->currentIndex().data(Qt::UserRole).toInt()); dialog->show(); @@ -108,17 +109,20 @@ void ShipWeaponsDialog::on_tblButton_clicked() { return; } } -void ShipWeaponsDialog::on_radioPrimary_toggled(bool checked) { +void ShipWeaponsDialog::on_radioPrimary_toggled(bool checked) +{ modeChanged(checked, 0); } void ShipWeaponsDialog::on_radioSecondary_toggled(bool checked) { modeChanged(checked, 1); } -void ShipWeaponsDialog::on_radioTertiary_toggled(bool checked) { +void ShipWeaponsDialog::on_radioTertiary_toggled(bool checked) +{ modeChanged(checked, 2); } -void ShipWeaponsDialog::on_aiCombo_currentIndexChanged(int index) { +void ShipWeaponsDialog::on_aiCombo_currentIndexChanged(int index) +{ aiClassChanged(index); } void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) @@ -148,7 +152,7 @@ void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) bankModel = new BankTreeModel(_model->getPrimaryBanks(), this); dialogMode = 0; } - //Reconnect beacuse the model has changed + // Reconnect beacuse the model has changed connect(ui->treeBanks->selectionModel()->model(), &QAbstractItemModel::dataChanged, this, @@ -169,7 +173,7 @@ void ShipWeaponsDialog::modeChanged(const bool enabled, const int mode) void ShipWeaponsDialog::updateUI() { const util::SignalBlockers blockers(this); - //Radio Buttons + // Radio Buttons ui->radioPrimary->setEnabled(!_model->getPrimaryBanks().empty()); ui->radioSecondary->setEnabled(!_model->getSecondaryBanks().empty()); ui->radioTertiary->setEnabled(false); @@ -201,11 +205,13 @@ void ShipWeaponsDialog::updateUI() } } -void ShipWeaponsDialog::aiClassChanged(const int index) { +void ShipWeaponsDialog::aiClassChanged(const int index) +{ m_currentAI = ui->aiCombo->itemData(index).toInt(); } -void ShipWeaponsDialog::on_aiButton_clicked() { +void ShipWeaponsDialog::on_aiButton_clicked() +{ for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { bankModel->setData(index, m_currentAI); } @@ -216,6 +222,4 @@ void ShipWeaponsDialog::on_buttonClose_clicked() accept(); } -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h index a635b2b743b..7b5efb83578 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -10,9 +10,7 @@ #include #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class ShipWeaponsDialog; @@ -49,7 +47,7 @@ class ShipWeaponsDialog : public QDialog { void on_radioTertiary_toggled(bool checked); void on_aiCombo_currentIndexChanged(int index); - private: + private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; /** @@ -66,7 +64,5 @@ class ShipWeaponsDialog : public QDialog { int m_currentAI = 0; void aiClassChanged(const int index); }; -} // namespace dialogs -} // namespace fred -} // namespace fso +} // namespace fso::fred::dialogs #endif \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp index 20f0f7bff85..78a33cb8d62 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp @@ -6,9 +6,7 @@ #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { WeaponsTBLViewer::WeaponsTBLViewer(QWidget* parent, EditorViewport* viewport, int wc) : QDialog(parent), ui(new Ui::ShipTBLViewer()), _model(new WeaponsTBLViewerModel(this, viewport, wc)), _viewport(viewport) @@ -34,6 +32,4 @@ void WeaponsTBLViewer::updateUI() util::SignalBlockers blockers(this); ui->TBLData->setPlainText(_model->getText().c_str()); } -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h index ca9608af7af..a6bba8fe23e 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h +++ b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h @@ -4,9 +4,7 @@ #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class ShipTBLViewer; @@ -26,11 +24,8 @@ class WeaponsTBLViewer : public QDialog { std::unique_ptr _model; EditorViewport* _viewport; - int sc; void updateUI(); }; -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp index c61a0ec58d3..f2d28dd22ee 100644 --- a/qtfred/src/ui/widgets/bankTree.cpp +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -1,6 +1,5 @@ #include "bankTree.h" -namespace fso { -namespace fred { +namespace fso::fred { bankTree::bankTree(QWidget* parent) : QTreeView(parent) { setAcceptDrops(true); @@ -42,7 +41,7 @@ void bankTree::selectionChanged(const QItemSelection& selected, const QItemSelec QItemSelection deselect(deselected); if (selected.empty()) { QTreeView::selectionChanged(selected, deselected); - if (selectionModel()->selectedIndexes().size() == 0) { + if (selectionModel()->selectedIndexes().empty()) { typeSelected = -1; } return; @@ -101,5 +100,4 @@ int bankTree::getTypeSelected() const { return typeSelected; } -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/widgets/bankTree.h b/qtfred/src/ui/widgets/bankTree.h index 7a445ca89bc..81268d173cf 100644 --- a/qtfred/src/ui/widgets/bankTree.h +++ b/qtfred/src/ui/widgets/bankTree.h @@ -1,24 +1,25 @@ #pragma once -#include +#include "ui/dialogs/ShipEditor/BankModel.h" + +#include + #include +#include #include #include -#include -#include "ui/dialogs/ShipEditor/BankModel.h" -#include -namespace fso { -namespace fred { +#include +namespace fso::fred { class bankTree : public QTreeView { Q_OBJECT public: bankTree(QWidget*); - void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected); + void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; int getTypeSelected() const; + protected: - void dragEnterEvent(QDragEnterEvent*); - void dropEvent(QDropEvent* event); - void dragMoveEvent(QDragMoveEvent*); + void dragEnterEvent(QDragEnterEvent*) override; + void dropEvent(QDropEvent* event) override; + void dragMoveEvent(QDragMoveEvent*) override; int typeSelected = -1; }; -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/widgets/weaponList.cpp b/qtfred/src/ui/widgets/weaponList.cpp index ede822c8481..78f40afd018 100644 --- a/qtfred/src/ui/widgets/weaponList.cpp +++ b/qtfred/src/ui/widgets/weaponList.cpp @@ -1,7 +1,6 @@ #include "weaponList.h" -namespace fso { -namespace fred { +namespace fso::fred { weaponList::weaponList(QWidget* parent) : QListView(parent) {} void weaponList::mousePressEvent(QMouseEvent* event) @@ -21,11 +20,11 @@ void weaponList::mouseMoveEvent(QMouseEvent* event) if (!idx.isValid()) { return; } - QDrag* drag = new QDrag(this); + auto drag = new QDrag(this); QModelIndexList idxs; idxs.append(idx); QMimeData* mimeData = model()->mimeData(idxs); - QPixmap* iconPixmap = new QPixmap(); + auto iconPixmap = new QPixmap(); QPainter painter(iconPixmap); painter.setFont(QFont("Arial")); painter.drawText(QPoint(100, 100), model()->data(idx, Qt::DisplayRole).toString()); @@ -99,6 +98,5 @@ QMimeData* WeaponModel::mimeData(const QModelIndexList& indexes) const return mimeData; } -WeaponItem::WeaponItem(const int inID, const QString& inName) : name(inName), id(inID) {} -} // namespace fred -} // namespace fso \ No newline at end of file +WeaponItem::WeaponItem(const int inID, QString inName) : name(std::move(inName)), id(inID) {} +} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/widgets/weaponList.h b/qtfred/src/ui/widgets/weaponList.h index 8a9d02a0b3e..ed15b120798 100644 --- a/qtfred/src/ui/widgets/weaponList.h +++ b/qtfred/src/ui/widgets/weaponList.h @@ -1,15 +1,15 @@ #pragma once -#include -#include +#include + #include #include +#include #include +#include #include -#include -namespace fso { -namespace fred { +namespace fso::fred { struct WeaponItem { - WeaponItem(const int id, const QString& name); + WeaponItem(const int id, QString name); const QString name; const int id; }; @@ -17,7 +17,7 @@ class WeaponModel : public QAbstractListModel { Q_OBJECT public: WeaponModel(int type); - ~WeaponModel(); + ~WeaponModel() override; int rowCount(const QModelIndex& parent = QModelIndex()) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QMimeData* mimeData(const QModelIndexList& indexes) const override; @@ -29,11 +29,10 @@ class weaponList : public QListView { weaponList(QWidget* parent); protected: - void mousePressEvent(QMouseEvent* event); - void mouseMoveEvent(QMouseEvent* event); + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; QPoint dragStartPosition; private: }; -} -} // namespace fso \ No newline at end of file +} // namespace fso::fred \ No newline at end of file From 1a72471543c5c3561fa1deb9ca412e7b5e1653de Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sat, 9 Aug 2025 19:31:04 +0100 Subject: [PATCH 337/466] Last One --- .../mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h b/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h index dbcba064aa7..97a0e3eee5e 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h @@ -2,9 +2,7 @@ #include "../AbstractDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { class WeaponsTBLViewerModel : public AbstractDialogModel { private: SCP_string text; @@ -17,6 +15,4 @@ class WeaponsTBLViewerModel : public AbstractDialogModel { SCP_string getText() const; }; -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred::dialogs \ No newline at end of file From 726c954e372711675914ef60bcfcaab660cca77f Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Sun, 10 Aug 2025 18:25:02 +0100 Subject: [PATCH 338/466] Fix Crash On selecting different ship types (#6920) --- qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp | 2 +- qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp index 0e6f575cda2..f0e55ac9d2d 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp @@ -1366,7 +1366,7 @@ namespace fso { return Editor::wing_is_player_wing(wing); } - std::set ShipEditorDialogModel::getShipOrders() const + const std::set &ShipEditorDialogModel::getShipOrders() const { return ship_orders; } diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h index 27fae31e081..77fab5574d4 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h @@ -182,7 +182,7 @@ class ShipEditorDialogModel : public AbstractDialogModel { * @param wing Takes an integer id of the wing */ static bool wing_is_player_wing(const int); - std::set getShipOrders() const; + const std::set &getShipOrders() const; bool getTexEditEnable() const; /** From f903180411ea894d681cfc4c7590c5f9c7fddf86 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Mon, 11 Aug 2025 10:56:28 -0400 Subject: [PATCH 339/466] Fix/enhance skybox submodel rendering Background: Currently, skybox models can be set in FRED to have `No Z-Buffer` on or off. It is off by default, which is good because it ensures ships and other game objects cannot be rendered behind a skybox. Critically though, skybox models may have sub-models, especially now that animated sub-models are a feature (thanks to BMagnu!). If a skybox is using sub-models and `No Z-Buffer` is turned off, then there will be no proper model occlusion for the skybox and there will be ugly z fighting. Overall, BMangu stated "we could clear the depth buffer after the skybox with another flag". Update This PR fixes that issue by adding a new FRED game settings flag, `$Skybox internal depth consistency:` which when set to yes properly ensures there is internally consistency z-sorting with the skybox and any submodels it may have while at the same time ensuring that if `No Z-Buffer` is set in FRED that the skybox still correctly is always drawn behind mission objects like ships/debris. Many thanks to @Bmagnu for the discussion and help in identifying and creating a proper fix for this issue that is clean, surgical, and supports all existing uses of `No Z-Buffer`. Tested and works as expected. Fixes #6913. --- code/mod_table/mod_table.cpp | 7 +++++++ code/mod_table/mod_table.h | 1 + code/starfield/starfield.cpp | 12 ++++++++++++ 3 files changed, 20 insertions(+) diff --git a/code/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index 7ac76af3427..d7df83afbb9 100644 --- a/code/mod_table/mod_table.cpp +++ b/code/mod_table/mod_table.cpp @@ -27,6 +27,7 @@ int Directive_wait_time; bool True_loop_argument_sexps; +bool Skybox_internal_depth_consistency; bool Fixed_turret_collisions; bool Fixed_missile_detonation; bool Damage_impacted_subsystem_first; @@ -1137,6 +1138,10 @@ void parse_mod_table(const char *filename) } + if (optional_string("$Skybox internal depth consistency:")) { + stuff_boolean(&Skybox_internal_depth_consistency); + } + optional_string("#OTHER SETTINGS"); if (optional_string("$Fixed Turret Collisions:")) { @@ -1640,6 +1645,7 @@ void mod_table_reset() { Directive_wait_time = 3000; True_loop_argument_sexps = false; + Skybox_internal_depth_consistency = false; Fixed_turret_collisions = false; Fixed_missile_detonation = false; Damage_impacted_subsystem_first = false; @@ -1820,5 +1826,6 @@ void mod_table_set_version_flags() Use_model_eyepoint_normals = true; Fix_asteroid_bounding_box_check = true; Disable_expensive_turret_target_check = true; + Skybox_internal_depth_consistency = true; } } diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index 463fbf9b5cd..9c1d0927e9e 100644 --- a/code/mod_table/mod_table.h +++ b/code/mod_table/mod_table.h @@ -41,6 +41,7 @@ struct splash_screen { extern int Directive_wait_time; extern bool True_loop_argument_sexps; +extern bool Skybox_internal_depth_consistency; extern bool Fixed_turret_collisions; extern bool Fixed_missile_detonation; extern bool Damage_impacted_subsystem_first; diff --git a/code/starfield/starfield.cpp b/code/starfield/starfield.cpp index 0f1a7168639..5ecae120852 100644 --- a/code/starfield/starfield.cpp +++ b/code/starfield/starfield.cpp @@ -2322,7 +2322,19 @@ void stars_draw_background() if (Nmodel_instance_num >= 0) render_info.set_replacement_textures(model_get_instance(Nmodel_instance_num)->texture_replace); + // if No Z-Buffer is on in FRED then check mod flag to see + // if skybox submodels should still have proper z-sorting + // wookieejedi + bool special_z_buff = ((Nmodel_flags & MR_NO_ZBUFFER) && Skybox_internal_depth_consistency); + if (special_z_buff) { + render_info.set_flags(Nmodel_flags & ~MR_NO_ZBUFFER); + } + model_render_immediate(&render_info, Nmodel_num, Nmodel_instance_num, &Nmodel_orient, &Eye_position, MODEL_RENDER_ALL, false); + + if (special_z_buff) { + gr_zbuffer_clear(TRUE); + } } void stars_set_background_model(int new_model, int new_bitmap, uint64_t flags, float alpha) From d0c5fe6bcca0910a12f1e03adbe7eac72332f4a1 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 11 Aug 2025 11:06:46 -0400 Subject: [PATCH 340/466] fix series of bugs due to improper multi targeting (#6916) Multiple issues were discovered to be from improper targeting data in multi tracing back to the addition of multi-lock. This was due to differences in how targeting works in single/multi and de-sync of this info between server and client. Collecting accurate targeting data on the server and making sure that the client will use that data resolves the tracking and sync issues. --- code/network/multimsgs.cpp | 19 ++++++-------- code/network/multimsgs.h | 3 ++- code/ship/ship.cpp | 54 ++++++++++++++++---------------------- code/weapon/weapon.h | 12 +++++++++ 4 files changed, 45 insertions(+), 43 deletions(-) diff --git a/code/network/multimsgs.cpp b/code/network/multimsgs.cpp index 3b4835f1272..4725ef2b05a 100644 --- a/code/network/multimsgs.cpp +++ b/code/network/multimsgs.cpp @@ -3044,14 +3044,13 @@ void process_cargo_hidden_packet( ubyte *data, header *hinfo ) #define SFPF_TARGET_LOCKED (1<<5) // send a packet indicating a secondary weapon was fired -void send_secondary_fired_packet( ship *shipp, ushort starting_sig, int /*starting_count*/, int num_fired, int allow_swarm ) +void send_secondary_fired_packet( ship *shipp, ushort starting_sig, tracking_info &tinfo, int num_fired, int allow_swarm ) { int packet_size, net_player_num; ubyte data[MAX_PACKET_SIZE], sinfo, current_bank; object *objp; ushort target_net_signature; int s_index; - ai_info *aip; // Assert ( starting_count < UCHAR_MAX ); @@ -3064,8 +3063,6 @@ void send_secondary_fired_packet( ship *shipp, ushort starting_sig, int /*start } } - aip = &Ai_info[shipp->ai_index]; - current_bank = (ubyte)shipp->weapons.current_secondary_bank; Assert( (current_bank < MAX_SHIP_SECONDARY_BANKS) ); @@ -3086,7 +3083,7 @@ void send_secondary_fired_packet( ship *shipp, ushort starting_sig, int /*start sinfo |= SFPF_DUAL_FIRE; } - if ( aip->current_target_is_locked ){ + if ( tinfo.locked ){ sinfo |= SFPF_TARGET_LOCKED; } @@ -3095,14 +3092,14 @@ void send_secondary_fired_packet( ship *shipp, ushort starting_sig, int /*start // add the ship's target and any targeted subsystem target_net_signature = 0; s_index = -1; - if ( aip->target_objnum != -1) { - target_net_signature = Objects[aip->target_objnum].net_signature; - if ( (Objects[aip->target_objnum].type == OBJ_SHIP) && (aip->targeted_subsys != NULL) ) { - s_index = ship_get_subsys_index( aip->targeted_subsys ); + if (tinfo.objnum != -1) { + target_net_signature = Objects[tinfo.objnum].net_signature; + if ( (Objects[tinfo.objnum].type == OBJ_SHIP) && (tinfo.subsys != nullptr) ) { + s_index = ship_get_subsys_index( tinfo.subsys ); } - if ( Objects[aip->target_objnum].type == OBJ_WEAPON ) { - Assert(Weapon_info[Weapons[Objects[aip->target_objnum].instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]); + if ( Objects[tinfo.objnum].type == OBJ_WEAPON ) { + Assert(Weapon_info[Weapons[Objects[tinfo.objnum].instance].weapon_info_index].wi_flags[Weapon::Info_Flags::Bomb]); } } diff --git a/code/network/multimsgs.h b/code/network/multimsgs.h index 836c090be89..8ba0641bf1d 100644 --- a/code/network/multimsgs.h +++ b/code/network/multimsgs.h @@ -30,6 +30,7 @@ class ship_subsys; struct log_entry; struct beam_fire_info; namespace animation { enum class ModelAnimationDirection; } +struct tracking_info; // macros for building up packets -- to save on time and typing. Important to note that local variables // must be named correctly @@ -270,7 +271,7 @@ void send_game_info_packet( void ); void send_leave_game_packet(short player_id = -1,int kicked_reason = -1,net_player *target = NULL); // send a packet indicating a secondary weapon was fired -void send_secondary_fired_packet( ship *shipp, ushort starting_sig, int starting_count, int num_fired, int allow_swarm ); +void send_secondary_fired_packet( ship *shipp, ushort starting_sig, tracking_info &tinfo, int num_fired, int allow_swarm ); // send a packet indicating a countermeasure was fired void send_countermeasure_fired_packet( object *objp, int cmeasure_count, int rand_val ); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 01c1eaba422..a828eb790f7 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -13836,7 +13836,7 @@ extern void ai_maybe_announce_shockwave_weapon(object *firing_objp, int weapon_i // need to avoid firing when normally called int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) { - int n, weapon_idx, j, bank, bank_adjusted, starting_bank_count = -1, num_fired; + int n, weapon_idx, j, bank, bank_adjusted, num_fired; ushort starting_sig = 0; ship *shipp; ship_weapon *swp; @@ -13846,6 +13846,7 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) polymodel *pm; vec3d missile_point, pnt, firing_pos; bool has_fired = false; // Used to determine whether to fire the scripting hook + tracking_info tinfo; Assert( obj != NULL ); @@ -13932,14 +13933,13 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) if ( MULTIPLAYER_MASTER ) { starting_sig = multi_get_next_network_signature( MULTI_SIG_NON_PERMANENT ); - starting_bank_count = swp->secondary_bank_ammo[bank]; } if (ship_fire_secondary_detonate(obj, swp)) { // in multiplayer, master sends a secondary fired packet with starting signature of -1 -- indicates // to client code to set the detonate timer to 0. if ( MULTIPLAYER_MASTER ) { - send_secondary_fired_packet( shipp, 0, starting_bank_count, 1, allow_swarm ); + send_secondary_fired_packet( shipp, 0, tinfo, 1, allow_swarm ); } // For all banks, if ok to fire a weapon, make it wait a bit. @@ -14112,10 +14112,6 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) return 0; // we can make a quick out here!!! } - int target_objnum; - ship_subsys *target_subsys; - int locked; - if ( obj == Player_obj || ( MULTIPLAYER_MASTER && obj->flags[Object::Object_Flags::Player_ship] ) ) { // use missile lock slots if ( !shipp->missile_locks_firing.empty() ) { @@ -14132,39 +14128,35 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) } if (lock_data.obj != nullptr) { - target_objnum = OBJ_INDEX(lock_data.obj); - target_subsys = lock_data.subsys; - locked = 1; + tinfo.objnum = OBJ_INDEX(lock_data.obj); + tinfo.subsys = lock_data.subsys; + tinfo.locked = true; } else { - target_objnum = -1; - target_subsys = nullptr; - locked = 0; + tinfo.objnum = -1; + tinfo.subsys = nullptr; + tinfo.locked = false; } - } else if (wip->wi_flags[Weapon::Info_Flags::Homing_heat]) { - target_objnum = aip->target_objnum; - target_subsys = aip->targeted_subsys; - locked = aip->current_target_is_locked; } else { - target_objnum = -1; - target_subsys = nullptr; - locked = 0; + tinfo.objnum = aip->target_objnum; + tinfo.subsys = aip->targeted_subsys; + tinfo.locked = aip->current_target_is_locked == 1; } } else if (wip->multi_lock && !aip->ai_missile_locks_firing.empty()) { - target_objnum = aip->ai_missile_locks_firing.back().first; - target_subsys = aip->ai_missile_locks_firing.back().second; - locked = 1; + tinfo.objnum = aip->ai_missile_locks_firing.back().first; + tinfo.subsys = aip->ai_missile_locks_firing.back().second; + tinfo.locked = true; aip->ai_missile_locks_firing.pop_back(); } else { - target_objnum = aip->target_objnum; - target_subsys = aip->targeted_subsys; - locked = aip->current_target_is_locked; + tinfo.objnum = aip->target_objnum; + tinfo.subsys = aip->targeted_subsys; + tinfo.locked = aip->current_target_is_locked == 1; } num_slots = pm->missile_banks[bank].num_slots; float target_radius = 0.f; - if (target_objnum >= 0) { - target_radius = Objects[target_objnum].radius; + if (tinfo.objnum >= 0) { + target_radius = Objects[tinfo.objnum].radius; } auto launch_curve_data = WeaponLaunchCurveData { @@ -14286,7 +14278,7 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) // create the weapon -- for multiplayer, the net_signature is assigned inside // of weapon_create - weapon_num = weapon_create( &firing_pos, &firing_orient, weapon_idx, OBJ_INDEX(obj), -1, locked, false, 0.f, nullptr, launch_curve_data); + weapon_num = weapon_create( &firing_pos, &firing_orient, weapon_idx, OBJ_INDEX(obj), -1, tinfo.locked, false, 0.f, nullptr, launch_curve_data); if (weapon_num == -1) { // Weapon most likely failed to fire @@ -14298,7 +14290,7 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) if (weapon_num >= 0) { weapon_idx = Weapons[Objects[weapon_num].instance].weapon_info_index; - weapon_set_tracking_info(weapon_num, OBJ_INDEX(obj), target_objnum, locked, target_subsys); + weapon_set_tracking_info(weapon_num, OBJ_INDEX(obj), tinfo); has_fired = true; // create the muzzle flash effect @@ -14362,7 +14354,7 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) // Cyborg17 - If this is a rollback shot, the server will let the player know within the packet. if ( MULTIPLAYER_MASTER ) { Assert(starting_sig != 0); - send_secondary_fired_packet( shipp, starting_sig, starting_bank_count, num_fired, allow_swarm ); + send_secondary_fired_packet( shipp, starting_sig, tinfo, num_fired, allow_swarm ); } // Handle Player only stuff, including stats and client secondary packets diff --git a/code/weapon/weapon.h b/code/weapon/weapon.h index 73bae6fecbb..9b9a39e02ed 100644 --- a/code/weapon/weapon.h +++ b/code/weapon/weapon.h @@ -938,6 +938,13 @@ extern SCP_vector Player_weapon_precedence; // Vector of weapon types, prec #define WEAPON_INDEX(wp) (int)(wp-Weapons) +typedef struct tracking_info { + ship_subsys *subsys; + int objnum; + bool locked; + + tracking_info() : subsys(nullptr), objnum(-1), locked(false) {} +} tracking_info; int weapon_info_lookup(const char *name); int weapon_info_get_index(const weapon_info *wip); @@ -990,6 +997,11 @@ int weapon_create( const vec3d *pos, }); void weapon_set_tracking_info(int weapon_objnum, int parent_objnum, int target_objnum, int target_is_locked = 0, ship_subsys *target_subsys = NULL); +inline void weapon_set_tracking_info(int weapon_objnum, int parent_objnum, tracking_info &tinfo) +{ + weapon_set_tracking_info(weapon_objnum, parent_objnum, tinfo.objnum, tinfo.locked, tinfo.subsys); +} + // gets the substitution pattern pointer for a given weapon // src_turret may be null size_t* get_pointer_to_weapon_fire_pattern_index(int weapon_type, int ship_idx, ship_subsys* src_turret); From 1ffcaafc89d655ac88a9c3eac53b6ddf4271c94d Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 13 Aug 2025 06:26:55 -0500 Subject: [PATCH 341/466] Allow customizing the fs2 selection effect (#6868) * allow customizing the fs2 selection effect * undo color color shift --- code/missionui/missionscreencommon.cpp | 38 +++++++++++-------- code/missionui/missionscreencommon.h | 15 +++++++- code/missionui/missionshipchoice.cpp | 9 ++++- code/missionui/missionweaponchoice.cpp | 9 ++++- code/mod_table/mod_table.cpp | 46 +++++++++++++++++++++++ code/mod_table/mod_table.h | 4 ++ code/scripting/api/objs/shipclass.cpp | 9 ++++- code/scripting/api/objs/weaponclass.cpp | 9 ++++- code/ship/ship.cpp | 50 +++++++++++++++++++++++++ code/ship/ship.h | 4 ++ code/weapon/weapon.h | 4 ++ code/weapon/weapons.cpp | 46 +++++++++++++++++++++++ 12 files changed, 222 insertions(+), 21 deletions(-) diff --git a/code/missionui/missionscreencommon.cpp b/code/missionui/missionscreencommon.cpp index d8053933fdb..e8212e5964f 100644 --- a/code/missionui/missionscreencommon.cpp +++ b/code/missionui/missionscreencommon.cpp @@ -1663,7 +1663,7 @@ void draw_model_icon(int model_id, uint64_t flags, float closeup_zoom, int x, in gr_reset_clip(); } -void draw_model_rotating(model_render_params *render_info, int model_id, int x1, int y1, int x2, int y2, float *rotation_buffer, const vec3d *closeup_pos, float closeup_zoom, float rev_rate, uint64_t flags, int resize_mode, int effect) +void draw_model_rotating(model_render_params *render_info, int model_id, int x1, int y1, int x2, int y2, float *rotation_buffer, const vec3d *closeup_pos, float closeup_zoom, float rev_rate, uint64_t flags, int resize_mode, select_effect_params effect_params) { //WMC - Can't draw a non-model if (model_id < 0) @@ -1680,7 +1680,7 @@ void draw_model_rotating(model_render_params *render_info, int model_id, int x1, const bool& shadow_disable_override = flags & MR_IS_MISSILE ? Shadow_disable_overrides.disable_mission_select_weapons : Shadow_disable_overrides.disable_mission_select_ships; - if (effect == 2) { // FS2 Effect; Phase 0 Expand scanline, Phase 1 scan the grid and wireframe, Phase 2 scan up and reveal the ship, Phase 3 tilt the camera, Phase 4 start rotating the ship + if (effect_params.effect == 2) { // FS2 Effect; Phase 0 Expand scanline, Phase 1 scan the grid and wireframe, Phase 2 scan up and reveal the ship, Phase 3 tilt the camera, Phase 4 start rotating the ship // rotate the ship as much as required for this frame if (time >= 3.6f) // Phase 4 *rotation_buffer += PI2 * flFrametime / rev_rate; @@ -1743,7 +1743,7 @@ void draw_model_rotating(model_render_params *render_info, int model_id, int x1, g3_start_instance_angles(&vmd_zero_vector,&view_angles); if (time < 0.5f) { // Do the expanding scanline in phase 0 - gr_set_color(0,255,0); + gr_set_color(effect_params.fs2_scanline_color.red, effect_params.fs2_scanline_color.green, effect_params.fs2_scanline_color.blue); start.xyz.x = size*start_scale; start.xyz.y = 0.0f; start.xyz.z = -clip; @@ -1758,32 +1758,38 @@ void draw_model_rotating(model_render_params *render_info, int model_id, int x1, gr_zbuffer_set(GR_ZBUFF_NONE); // Turn off Depthbuffer so we don't get gridlines over the ship or a disappearing scanline Glowpoint_use_depth_buffer = false; // Since we don't have one if (time >= 0.5f) { // Phase 1 onward draw the grid - int i; start.xyz.y = -offset; start.xyz.z = size+offset*0.5f; stop.xyz.y = -offset; stop.xyz.z = -size+offset*0.5f; - gr_set_color(0,200,0); + gr_set_color(effect_params.fs2_grid_color.red, effect_params.fs2_grid_color.green, effect_params.fs2_grid_color.blue); g3_start_instance_angles(&vmd_zero_vector,&view_angles); if (time < 1.5f) { stop.xyz.z = -clip; } - for (i = -3; i < 4; i++) { - start.xyz.x = stop.xyz.x = size*0.333f*i; - //g3_draw_htl_line(&start,&stop); + int num_lines = std::max(3, effect_params.fs2_grid_density); + float x_step = (size * 2.0f) / (num_lines - 1); + float x_start = -size; + + for (int i = 0; i < num_lines; ++i) { + start.xyz.x = stop.xyz.x = x_start + i * x_step; g3_render_line_3d(false, &start, &stop); } start.xyz.x = size; stop.xyz.x = -size; - for (i = 3; i > -4; i--) { - start.xyz.z = stop.xyz.z = size*0.333f*i+offset*0.5f; - if ((time < 1.5f) && (start.xyz.z <= -clip)) + float z_step = (size * 2.0f) / (num_lines - 1); + float z_start = size + offset * 0.5f; + + for (int i = 0; i < num_lines; ++i) { + float z = z_start - i * z_step; + if ((time < 1.5f) && (z <= -clip)) break; - //g3_draw_htl_line(&start,&stop); + + start.xyz.z = stop.xyz.z = z; g3_render_line_3d(false, &start, &stop); } @@ -1825,8 +1831,8 @@ void draw_model_rotating(model_render_params *render_info, int model_id, int x1, gr_set_view_matrix(&Eye_position, &Eye_matrix); } gr_zbuffer_set(false); - gr_set_color(80,49,160); - render_info->set_color(80, 49, 160); + gr_set_color(effect_params.fs2_wireframe_color.red, effect_params.fs2_wireframe_color.green, effect_params.fs2_wireframe_color.blue); + render_info->set_color(effect_params.fs2_wireframe_color.red, effect_params.fs2_wireframe_color.green, effect_params.fs2_wireframe_color.blue); render_info->set_animated_effect(ANIMATED_SHADER_LOADOUTSELECT_FS2, -clip); @@ -1847,7 +1853,7 @@ void draw_model_rotating(model_render_params *render_info, int model_id, int x1, } if (time < 2.5f) { // Render the scanline in Phase 1 and 2 - gr_set_color(0,255,0); + gr_set_color(effect_params.fs2_scanline_color.red, effect_params.fs2_scanline_color.green, effect_params.fs2_scanline_color.blue); start.xyz.x = size*1.25f; start.xyz.y = 0.0f; start.xyz.z = -clip; @@ -1924,7 +1930,7 @@ void draw_model_rotating(model_render_params *render_info, int model_id, int x1, gr_set_color(0,128,0); - if (effect == 1) { // FS1 effect + if (effect_params.effect == 1) { // FS1 effect render_info->set_animated_effect(ANIMATED_SHADER_LOADOUTSELECT_FS1, MIN(time*0.5f,2.0f)); render_info->set_flags(flags); } else { diff --git a/code/missionui/missionscreencommon.h b/code/missionui/missionscreencommon.h index 322a3a43a40..c15f8b07312 100644 --- a/code/missionui/missionscreencommon.h +++ b/code/missionui/missionscreencommon.h @@ -14,6 +14,7 @@ #include "globalincs/globals.h" #include "gamesnd/gamesnd.h" +#include "mod_table/mod_table.h" #include "model/model.h" #include "ui/ui.h" @@ -212,6 +213,18 @@ typedef struct loadout_data extern loadout_data Player_loadout; +struct select_effect_params { + int effect; // effect type (0 = none/rotate, 1 = FS1, 2 = FS2) + color fs2_grid_color; // color of the grid in FS2 effect + color fs2_scanline_color; // color of the scanlines in FS2 effect + int fs2_grid_density; // density of the grid in FS2 effect + color fs2_wireframe_color; // color of the model wireframe in FS2 effect + + select_effect_params() : effect(2), fs2_grid_color(Default_fs2_effect_grid_color), fs2_scanline_color(Default_fs2_effect_scanline_color), fs2_grid_density(Default_fs2_effect_grid_density), fs2_wireframe_color(Default_fs2_effect_wireframe_color) + { + } +}; + void wss_save_loadout(); void wss_maybe_restore_loadout(); void wss_direct_restore_loadout(); @@ -222,7 +235,7 @@ int restore_wss_data(ubyte *data); class ship_info; void draw_model_icon(int model_id, uint64_t flags, float closeup_zoom, int x1, int x2, int y1, int y2, ship_info* sip = NULL, int resize_mode = GR_RESIZE_FULL, const vec3d *closeup_pos = &vmd_zero_vector); -void draw_model_rotating(model_render_params *render_info, int model_id, int x1, int y1, int x2, int y2, float *rotation_buffer, const vec3d *closeup_pos=nullptr, float closeup_zoom = .65f, float rev_rate = REVOLUTION_RATE, uint64_t flags = MR_AUTOCENTER | MR_NO_FOGGING, int resize_mode=GR_RESIZE_FULL, int effect = 2); +void draw_model_rotating(model_render_params *render_info, int model_id, int x1, int y1, int x2, int y2, float *rotation_buffer, const vec3d *closeup_pos=nullptr, float closeup_zoom = .65f, float rev_rate = REVOLUTION_RATE, uint64_t flags = MR_AUTOCENTER | MR_NO_FOGGING, int resize_mode=GR_RESIZE_FULL, select_effect_params effect_params = select_effect_params{}); void common_set_team_pointers(int team); void common_reset_team_pointers(); diff --git a/code/missionui/missionshipchoice.cpp b/code/missionui/missionshipchoice.cpp index d9505e63383..507593265b3 100644 --- a/code/missionui/missionshipchoice.cpp +++ b/code/missionui/missionshipchoice.cpp @@ -1452,6 +1452,13 @@ void ship_select_do(float frametime) render_info.set_replacement_textures(ShipSelectModelNum, sip->replacement_textures); } + select_effect_params params; + params.effect = sip->selection_effect; + params.fs2_grid_color = sip->fs2_effect_grid_color; + params.fs2_scanline_color = sip->fs2_effect_scanline_color; + params.fs2_grid_density = sip->fs2_effect_grid_density; + params.fs2_wireframe_color = sip->fs2_effect_wireframe_color; + draw_model_rotating( &render_info, ShipSelectModelNum, @@ -1465,7 +1472,7 @@ void ship_select_do(float frametime) rev_rate, MR_AUTOCENTER | MR_NO_FOGGING, GR_RESIZE_MENU, - sip->selection_effect); + params); } } diff --git a/code/missionui/missionweaponchoice.cpp b/code/missionui/missionweaponchoice.cpp index fa197e1ed4e..6eb71f17dae 100644 --- a/code/missionui/missionweaponchoice.cpp +++ b/code/missionui/missionweaponchoice.cpp @@ -2813,6 +2813,13 @@ void weapon_select_do(float frametime) modelIdx = model_load(wip->pofbitmap_name, nullptr, ErrorType::FATAL_ERROR); } + select_effect_params params; + params.effect = wip->selection_effect; + params.fs2_grid_color = wip->fs2_effect_grid_color; + params.fs2_scanline_color = wip->fs2_effect_scanline_color; + params.fs2_grid_density = wip->fs2_effect_grid_density; + params.fs2_wireframe_color = wip->fs2_effect_wireframe_color; + model_render_params render_info; draw_model_rotating(&render_info, modelIdx, @@ -2826,7 +2833,7 @@ void weapon_select_do(float frametime) REVOLUTION_RATE, MR_IS_MISSILE | MR_AUTOCENTER | MR_NO_FOGGING, GR_RESIZE_MENU, - wip->selection_effect); + params); } else if ( Weapon_anim_class != -1 && ( Selected_wl_class == Weapon_anim_class )) { Assert(Selected_wl_class >= 0 && Selected_wl_class < weapon_info_size()); if ( Weapon_anim_class != Selected_wl_class ) diff --git a/code/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index d7df83afbb9..917dbec1fc6 100644 --- a/code/mod_table/mod_table.cpp +++ b/code/mod_table/mod_table.cpp @@ -46,6 +46,10 @@ bool Use_3d_overhead_ship; overhead_style Default_overhead_ship_style; int Default_ship_select_effect; int Default_weapon_select_effect; +color Default_fs2_effect_grid_color; +color Default_fs2_effect_scanline_color; +color Default_fs2_effect_wireframe_color; +int Default_fs2_effect_grid_density; int Default_fiction_viewer_ui; bool Enable_external_shaders; bool Enable_external_default_scripts; @@ -1217,6 +1221,44 @@ void parse_mod_table(const char *filename) mprintf(("Game Settings Table: Using 3D weapon icons\n")); } + if (optional_string("$FS2 effect grid color:")) { + int rgb[3]; + stuff_int_list(rgb, 3); + CLAMP(rgb[0], 0, 255); + CLAMP(rgb[1], 0, 255); + CLAMP(rgb[2], 0, 255); + gr_init_color(&Default_fs2_effect_grid_color, rgb[0], rgb[1], rgb[2]); + } + + if (optional_string("$FS2 effect scanline color:")) { + int rgb[3]; + stuff_int_list(rgb, 3); + CLAMP(rgb[0], 0, 255); + CLAMP(rgb[1], 0, 255); + CLAMP(rgb[2], 0, 255); + gr_init_color(&Default_fs2_effect_scanline_color, rgb[0], rgb[1], rgb[2]); + } + + if (optional_string("$FS2 effect grid density:")) { + int tmp; + stuff_int(&tmp); + // only set value if it is above 0 + if (tmp > 0) { + Default_fs2_effect_grid_density = tmp; + } else { + Warning(LOCATION, "The $FS2 effect grid density must be above 0.\n"); + } + } + + if (optional_string("$FS2 effect wireframe color:")) { + int rgb[3]; + stuff_int_list(rgb, 3); + CLAMP(rgb[0], 0, 255); + CLAMP(rgb[1], 0, 255); + CLAMP(rgb[2], 0, 255); + gr_init_color(&Default_fs2_effect_wireframe_color, rgb[0], rgb[1], rgb[2]); + } + if (optional_string("$Use 3d overhead ship:")) { stuff_boolean(&Use_3d_overhead_ship); if (Use_3d_overhead_ship) @@ -1658,6 +1700,10 @@ void mod_table_reset() Always_show_directive_value_count = false; Default_ship_select_effect = 2; Default_weapon_select_effect = 2; + gr_init_color(&Default_fs2_effect_grid_color, 0, 200, 0); + gr_init_color(&Default_fs2_effect_scanline_color, 0, 255, 0); + Default_fs2_effect_grid_density = 7; // Default original value + gr_init_color(&Default_fs2_effect_wireframe_color, 80, 49, 160); Default_overhead_ship_style = OH_TOP_VIEW; Default_fiction_viewer_ui = -1; Enable_external_shaders = false; diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index 9c1d0927e9e..f1172e0ec76 100644 --- a/code/mod_table/mod_table.h +++ b/code/mod_table/mod_table.h @@ -59,6 +59,10 @@ extern bool Use_3d_weapon_select; extern int Default_weapon_select_effect; extern bool Use_3d_weapon_icons; extern bool Use_3d_overhead_ship; +extern color Default_fs2_effect_grid_color; +extern color Default_fs2_effect_scanline_color; +extern color Default_fs2_effect_wireframe_color; +extern int Default_fs2_effect_grid_density; extern overhead_style Default_overhead_ship_style; extern int Default_fiction_viewer_ui; extern bool Enable_external_shaders; diff --git a/code/scripting/api/objs/shipclass.cpp b/code/scripting/api/objs/shipclass.cpp index cc1abd7fccd..38df49ddd34 100644 --- a/code/scripting/api/objs/shipclass.cpp +++ b/code/scripting/api/objs/shipclass.cpp @@ -1262,6 +1262,13 @@ ADE_FUNC(renderSelectModel, render_info.set_replacement_textures(modelNum, sip->replacement_textures); } + select_effect_params params; + params.effect = effect; + params.fs2_grid_color = sip->fs2_effect_grid_color; + params.fs2_scanline_color = sip->fs2_effect_scanline_color; + params.fs2_grid_density = sip->fs2_effect_grid_density; + params.fs2_wireframe_color = sip->fs2_effect_wireframe_color; + draw_model_rotating(&render_info, modelNum, x1, @@ -1274,7 +1281,7 @@ ADE_FUNC(renderSelectModel, rev_rate, MR_AUTOCENTER | MR_NO_FOGGING, GR_RESIZE_NONE, - effect); + params); return ade_set_args(L, "b", true); } diff --git a/code/scripting/api/objs/weaponclass.cpp b/code/scripting/api/objs/weaponclass.cpp index d62d02d84b3..84e8c06f979 100644 --- a/code/scripting/api/objs/weaponclass.cpp +++ b/code/scripting/api/objs/weaponclass.cpp @@ -1151,6 +1151,13 @@ ADE_FUNC(renderSelectModel, model_render_params render_info; + select_effect_params params; + params.effect = effect; + params.fs2_grid_color = wip->fs2_effect_grid_color; + params.fs2_scanline_color = wip->fs2_effect_scanline_color; + params.fs2_grid_density = wip->fs2_effect_grid_density; + params.fs2_wireframe_color = wip->fs2_effect_wireframe_color; + draw_model_rotating(&render_info, modelNum, x1, @@ -1163,7 +1170,7 @@ ADE_FUNC(renderSelectModel, REVOLUTION_RATE, MR_IS_MISSILE | MR_AUTOCENTER | MR_NO_FOGGING, GR_RESIZE_NONE, - effect); + params); return ade_set_args(L, "b", true); } diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index a828eb790f7..ff32b6fe716 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -1209,6 +1209,10 @@ void ship_info::clone(const ship_info& other) strcpy_s(anim_filename, other.anim_filename); strcpy_s(overhead_filename, other.overhead_filename); selection_effect = other.selection_effect; + fs2_effect_grid_color = other.fs2_effect_grid_color; + fs2_effect_scanline_color = other.fs2_effect_scanline_color; + fs2_effect_grid_density = other.fs2_effect_grid_density; + fs2_effect_wireframe_color = other.fs2_effect_wireframe_color; wingmen_status_dot_override = other.wingmen_status_dot_override; @@ -1549,6 +1553,10 @@ void ship_info::move(ship_info&& other) std::swap(anim_filename, other.anim_filename); std::swap(overhead_filename, other.overhead_filename); selection_effect = other.selection_effect; + fs2_effect_grid_color = other.fs2_effect_grid_color; + fs2_effect_scanline_color = other.fs2_effect_scanline_color; + fs2_effect_grid_density = other.fs2_effect_grid_density; + fs2_effect_wireframe_color = other.fs2_effect_wireframe_color; wingmen_status_dot_override = other.wingmen_status_dot_override; @@ -1926,6 +1934,10 @@ ship_info::ship_info() overhead_filename[0] = '\0'; selection_effect = Default_ship_select_effect; + fs2_effect_grid_color = Default_fs2_effect_grid_color; + fs2_effect_scanline_color = Default_fs2_effect_scanline_color; + fs2_effect_grid_density = Default_fs2_effect_grid_density; + fs2_effect_wireframe_color = Default_fs2_effect_wireframe_color; wingmen_status_dot_override = -1; @@ -3036,6 +3048,44 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool sip->selection_effect = 0; } + if (optional_string("$FS2 effect grid color:")) { + int rgb[3]; + stuff_int_list(rgb, 3); + CLAMP(rgb[0], 0, 255); + CLAMP(rgb[1], 0, 255); + CLAMP(rgb[2], 0, 255); + gr_init_color(&sip->fs2_effect_grid_color, rgb[0], rgb[1], rgb[2]); + } + + if (optional_string("$FS2 effect scanline color:")) { + int rgb[3]; + stuff_int_list(rgb, 3); + CLAMP(rgb[0], 0, 255); + CLAMP(rgb[1], 0, 255); + CLAMP(rgb[2], 0, 255); + gr_init_color(&sip->fs2_effect_scanline_color, rgb[0], rgb[1], rgb[2]); + } + + if (optional_string("$FS2 effect grid density:")) { + int tmp; + stuff_int(&tmp); + // only set value if it is above 0 + if (tmp > 0) { + sip->fs2_effect_grid_density = tmp; + } else { + Warning(LOCATION, "The $FS2 effect grid density must be above 0.\n"); + } + } + + if (optional_string("$FS2 effect wireframe color:")) { + int rgb[3]; + stuff_int_list(rgb, 3); + CLAMP(rgb[0], 0, 255); + CLAMP(rgb[1], 0, 255); + CLAMP(rgb[2], 0, 255); + gr_init_color(&sip->fs2_effect_wireframe_color, rgb[0], rgb[1], rgb[2]); + } + // This only works if the hud gauge defined uses $name assignment if (optional_string("$HUD Gauge Configs:")) { SCP_vector gauge_configs; diff --git a/code/ship/ship.h b/code/ship/ship.h index 5cfb53c0dc9..d66caf9d540 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1354,6 +1354,10 @@ class ship_info char anim_filename[MAX_FILENAME_LEN]; // filename for animation that plays in ship selection char overhead_filename[MAX_FILENAME_LEN]; // filename for animation that plays weapons loadout int selection_effect; + color fs2_effect_grid_color; // color of the grid effect in the ship selection screen + color fs2_effect_scanline_color; // color of the scanline effect in the ship selection screen + int fs2_effect_grid_density; // density of the grid effect in the ship selection screen + color fs2_effect_wireframe_color; // color of the wireframe effect in the ship selection screen int wingmen_status_dot_override; // optional wingmen dot status animation to use instead of default --wookieejedi diff --git a/code/weapon/weapon.h b/code/weapon/weapon.h index 9b9a39e02ed..9e01545bdc0 100644 --- a/code/weapon/weapon.h +++ b/code/weapon/weapon.h @@ -535,6 +535,10 @@ struct weapon_info char icon_filename[MAX_FILENAME_LEN]; // filename for icon that is displayed in weapon selection char anim_filename[MAX_FILENAME_LEN]; // filename for animation that plays in weapon selection int selection_effect; + color fs2_effect_grid_color; // color of the grid effect in the weapon selection screen + color fs2_effect_scanline_color; // color of the scanline effect in the weapon selection screen + int fs2_effect_grid_density; // density of the grid effect in the weapon selection screen + color fs2_effect_wireframe_color; // color of the wireframe effect in the weapon selection screen float shield_impact_effect_radius; // shield surface effect radius float shield_impact_explosion_radius; // shield-specific particle effect radius diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 34b7aaf72aa..76ab37b09df 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -996,6 +996,10 @@ int parse_weapon(int subtype, bool replace, const char *filename) // Weapon fadein effect, used when no ani is specified or weapon_select_3d is active if (first_time) { wip->selection_effect = Default_weapon_select_effect; // By default, use the FS2 effect + wip->fs2_effect_grid_color = Default_fs2_effect_grid_color; + wip->fs2_effect_scanline_color = Default_fs2_effect_scanline_color; + wip->fs2_effect_grid_density = Default_fs2_effect_grid_density; + wip->fs2_effect_wireframe_color = Default_fs2_effect_wireframe_color; } if(optional_string("$Selection Effect:")) { char effect[NAME_LENGTH]; @@ -1008,6 +1012,44 @@ int parse_weapon(int subtype, bool replace, const char *filename) wip->selection_effect = 0; } + if (optional_string("$FS2 effect grid color:")) { + int rgb[3]; + stuff_int_list(rgb, 3); + CLAMP(rgb[0], 0, 255); + CLAMP(rgb[1], 0, 255); + CLAMP(rgb[2], 0, 255); + gr_init_color(&wip->fs2_effect_grid_color, rgb[0], rgb[1], rgb[2]); + } + + if (optional_string("$FS2 effect scanline color:")) { + int rgb[3]; + stuff_int_list(rgb, 3); + CLAMP(rgb[0], 0, 255); + CLAMP(rgb[1], 0, 255); + CLAMP(rgb[2], 0, 255); + gr_init_color(&wip->fs2_effect_scanline_color, rgb[0], rgb[1], rgb[2]); + } + + if (optional_string("$FS2 effect grid density:")) { + int tmp; + stuff_int(&tmp); + // only set value if it is above 0 + if (tmp > 0) { + wip->fs2_effect_grid_density = tmp; + } else { + Warning(LOCATION, "The $FS2 effect grid density must be above 0.\n"); + } + } + + if (optional_string("$FS2 effect wireframe color:")) { + int rgb[3]; + stuff_int_list(rgb, 3); + CLAMP(rgb[0], 0, 255); + CLAMP(rgb[1], 0, 255); + CLAMP(rgb[2], 0, 255); + gr_init_color(&wip->fs2_effect_wireframe_color, rgb[0], rgb[1], rgb[2]); + } + //Check for the HUD image string if(optional_string("$HUD Image:")) { stuff_string(wip->hud_filename, F_NAME, MAX_FILENAME_LEN); @@ -9508,6 +9550,10 @@ void weapon_info::reset() memset(this->icon_filename, 0, sizeof(this->icon_filename)); memset(this->anim_filename, 0, sizeof(this->anim_filename)); this->selection_effect = Default_weapon_select_effect; + this->fs2_effect_grid_color = Default_fs2_effect_grid_color; + this->fs2_effect_scanline_color = Default_fs2_effect_scanline_color; + this->fs2_effect_grid_density = Default_fs2_effect_grid_density; + this->fs2_effect_wireframe_color = Default_fs2_effect_wireframe_color; this->shield_impact_effect_radius = -1.0f; this->shield_impact_explosion_radius = 1.0f; From 966ac4d87875db39224fe517008cf099a03c5065 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 13 Aug 2025 07:36:53 -0500 Subject: [PATCH 342/466] Allow testing bay paths in the lab (#6902) * allow testing bay paths in the lab * unused param * static member methods --- code/ai/aicode.cpp | 26 ++- code/lab/dialogs/lab_ui.cpp | 270 ++++++++++++++++++---------- code/lab/dialogs/lab_ui.h | 7 + code/lab/dialogs/lab_ui_helpers.cpp | 16 ++ code/lab/dialogs/lab_ui_helpers.h | 2 + code/lab/manager/lab_manager.cpp | 136 +++++++++++++- code/lab/manager/lab_manager.h | 16 ++ 7 files changed, 370 insertions(+), 103 deletions(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index 1d8796114e9..e10342bb0d3 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -14048,20 +14048,28 @@ void ai_bay_depart() return; } + ship* shipp; + // check if parent ship valid; if not, abort depart - auto anchor_ship_entry = ship_registry_get(Parse_names[Ships[Pl_objp->instance].departure_anchor]); - if (!anchor_ship_entry || !ship_useful_for_departure(anchor_ship_entry->shipnum, Ships[Pl_objp->instance].departure_path_mask)) - { - mprintf(("Aborting bay departure!\n")); - aip->mode = AIM_NONE; - - Ships[Pl_objp->instance].flags.remove(Ship::Ship_Flags::Depart_dockbay); - return; + if (gameseq_get_state() != GS_STATE_LAB) { + auto anchor_ship_entry = ship_registry_get(Parse_names[Ships[Pl_objp->instance].departure_anchor]); + if (!anchor_ship_entry || + !ship_useful_for_departure(anchor_ship_entry->shipnum, Ships[Pl_objp->instance].departure_path_mask)) { + mprintf(("Aborting bay departure!\n")); + aip->mode = AIM_NONE; + + Ships[Pl_objp->instance].flags.remove(Ship::Ship_Flags::Depart_dockbay); + return; + } + + shipp = anchor_ship_entry->shipp(); + } else { + shipp = &Ships[Objects[aip->goal_objnum].instance]; } ai_manage_bay_doors(Pl_objp, aip, false); - if ( anchor_ship_entry->shipp()->bay_doors_status != MA_POS_READY ) + if ( shipp->bay_doors_status != MA_POS_READY ) return; // follow the path to the final point diff --git a/code/lab/dialogs/lab_ui.cpp b/code/lab/dialogs/lab_ui.cpp index 2d5030b3dc7..2770deab4c4 100644 --- a/code/lab/dialogs/lab_ui.cpp +++ b/code/lab/dialogs/lab_ui.cpp @@ -1086,6 +1086,182 @@ void LabUi::maybe_show_animation_category(const SCP_vectorship_info_index].model_num); + + if (!dockee_dock_map.empty()) { + + if (ImGui::BeginCombo("Docker Ship Class", Ship_info[getLabManager()->DockerClass].name)) { + for (size_t i = 0; i < Ship_info.size(); ++i) { + bool is_selected = (static_cast(i) == getLabManager()->DockerClass); + if (ImGui::Selectable(Ship_info[i].name, is_selected)) { + getLabManager()->DockerClass = static_cast(i); + // Load model if needed + auto& dsip = Ship_info[getLabManager()->DockerClass]; + if (dsip.model_num < 0) { + dsip.model_num = model_load(dsip.pof_file, &dsip); + } + auto new_dock_map = get_docking_point_map(dsip.model_num); + + // Auto-select first available dockpoint (or clear if none) + if (!new_dock_map.empty()) { + getLabManager()->DockerDockPoint = new_dock_map.begin()->second; + } else { + getLabManager()->DockerDockPoint.clear(); + } + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + auto& dsip = Ship_info[getLabManager()->DockerClass]; + if (dsip.model_num < 0) { + dsip.model_num = model_load(dsip.pof_file, &dsip); + } + auto dock_map = get_docking_point_map(dsip.model_num); + + // Ensure DockerDockPoint is initialized once based on the current DockerClass + if (getLabManager()->DockerDockPoint.empty()) { + if (!dock_map.empty()) { + getLabManager()->DockerDockPoint = dock_map.begin()->second; + } + } + + const char* docker_label = getLabManager()->DockerDockPoint.c_str(); + + if (ImGui::BeginCombo("Docker Dockpoint", docker_label)) { + if (!dock_map.empty()) { + for (const auto& [index, name] : dock_map) { + bool is_selected = (name == getLabManager()->DockerDockPoint); + if (ImGui::Selectable(name.c_str(), is_selected)) { + getLabManager()->DockerDockPoint = name; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + } + ImGui::EndCombo(); + } + + // Auto-select first dockpoint if none currently selected + if (getLabManager()->DockeeDockPoint.empty()) { + getLabManager()->DockeeDockPoint = dockee_dock_map.begin()->second; + } + + const char* dockee_label = getLabManager()->DockeeDockPoint.c_str(); + + if (ImGui::BeginCombo("Dockee Dockpoint", dockee_label)) { + for (const auto& [index, name] : dockee_dock_map) { + bool is_selected = (name == getLabManager()->DockeeDockPoint); + if (ImGui::Selectable(name.c_str(), is_selected)) { + getLabManager()->DockeeDockPoint = name; + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + if (Button("Begin Docking Test")) { + getLabManager()->beginDockingTest(); + } + + if (Button("Begin Undocking Test")) { + getLabManager()->beginUndockingTest(); + } + } + } +} + +void LabUi::build_bay_test_options(ship_info* sip) +{ + with_TreeNode("Fighterbay Tests") + { + auto bay_paths_map = get_bay_paths_map(sip->model_num); + + if (!bay_paths_map.empty()) { + + if (ImGui::BeginCombo("Bay Arrival/Departure Ship Class", Ship_info[getLabManager()->BayClass].name)) { + for (size_t i = 0; i < Ship_info.size(); ++i) { + bool is_selected = (static_cast(i) == getLabManager()->BayClass); + if (ImGui::Selectable(Ship_info[i].name, is_selected)) { + getLabManager()->BayClass = static_cast(i); + } + if (is_selected) + ImGui::SetItemDefaultFocus(); + } + ImGui::EndCombo(); + } + + auto& bsip = Ship_info[getLabManager()->BayClass]; + + // Load the model + if (bsip.model_num < 0) { + bsip.model_num = model_load(bsip.pof_file, &bsip); + } + + // Pick the first entry in bay_paths_map if it's set to 0 + if (getLabManager()->BayPathMask == 0) { + int first_idx = bay_paths_map.begin()->first; + getLabManager()->BayPathMask = (1u << first_idx); + } + + // Get the preview label by finding the one bit that's set + const char* path_label = "??"; + for (const auto& [idx, name] : bay_paths_map) { + if (getLabManager()->BayPathMask & (1u << idx)) { + path_label = name.c_str(); + break; + } + } + + if (ImGui::BeginCombo("Bay Path", path_label)) { + for (const auto& [idx, name] : bay_paths_map) { + bool is_selected = (getLabManager()->BayPathMask & (1u << idx)) != 0; + if (ImGui::Selectable(name.c_str(), is_selected)) { + getLabManager()->BayPathMask = (1u << idx); + } + if (is_selected) { + ImGui::SetItemDefaultFocus(); + } + } + ImGui::EndCombo(); + } + + static const char* mode_names[] = {"Arrival", "Departure"}; + int mode_idx = static_cast(getLabManager()->BayTestMode); + + Assertion(mode_idx == 0 || mode_idx == 1, "Bay test mode is not valid!"); // only two valid modes + + if (ImGui::BeginCombo("Bay Test Mode", mode_names[mode_idx])) { + // Arrival + if (ImGui::Selectable("Arrival", getLabManager()->BayTestMode == BayMode::Arrival)) { + getLabManager()->BayTestMode = BayMode::Arrival; + } + + // Departure + if (ImGui::Selectable("Departure", getLabManager()->BayTestMode == BayMode::Departure)) { + getLabManager()->BayTestMode = BayMode::Departure; + } + + ImGui::SetItemDefaultFocus(); + ImGui::EndCombo(); + } + + if (Button("Begin Bay Path Test")) { + getLabManager()->beginBayTest(); + } + } + } +} + void LabUi::build_animation_options(ship* shipp, ship_info* sip) const { with_TreeNode("Animations") @@ -1228,8 +1404,8 @@ void LabUi::show_object_options() const if (getLabManager()->isSafeForShips()) { if (Button("Destroy ship")) { if (Objects[getLabManager()->CurrentObject].type == OBJ_SHIP) { - // If we have an undocker, delete it before destroying the current ship - getLabManager()->deleteDockerObject(); + // If we have testing objects, delete them + getLabManager()->deleteTestObjects(); auto obj = &Objects[getLabManager()->CurrentObject]; @@ -1238,95 +1414,9 @@ void LabUi::show_object_options() const } } - const ship* dockee_shipp = &Ships[Objects[getLabManager()->CurrentObject].instance]; - auto dockee_dock_map = get_docking_point_map(Ship_info[dockee_shipp->ship_info_index].model_num); - - if (!dockee_dock_map.empty()) { - - if (ImGui::BeginCombo("Docker Ship Class", Ship_info[getLabManager()->DockerClass].name)) { - for (size_t i = 0; i < Ship_info.size(); ++i) { - bool is_selected = (static_cast(i) == getLabManager()->DockerClass); - if (ImGui::Selectable(Ship_info[i].name, is_selected)) { - getLabManager()->DockerClass = static_cast(i); - // Load model if needed - auto& dsip = Ship_info[getLabManager()->DockerClass]; - if (dsip.model_num < 0) { - dsip.model_num = model_load(dsip.pof_file, &dsip); - } - auto new_dock_map = get_docking_point_map(dsip.model_num); - - // Auto-select first available dockpoint (or clear if none) - if (!new_dock_map.empty()) { - getLabManager()->DockerDockPoint = new_dock_map.begin()->second; - } else { - getLabManager()->DockerDockPoint.clear(); - } - } - if (is_selected) - ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - - auto& dsip = Ship_info[getLabManager()->DockerClass]; - if (dsip.model_num < 0) { - dsip.model_num = model_load(dsip.pof_file, &dsip); - } - auto dock_map = get_docking_point_map(dsip.model_num); - - // Ensure DockerDockPoint is initialized once based on the current DockerClass - if (getLabManager()->DockerDockPoint.empty()) { - if (!dock_map.empty()) { - getLabManager()->DockerDockPoint = dock_map.begin()->second; - } - } - - const char* docker_label = getLabManager()->DockerDockPoint.c_str(); - - if (ImGui::BeginCombo("Docker Dockpoint", docker_label)) { - if (!dock_map.empty()) { - for (const auto& [index, name] : dock_map) { - bool is_selected = (name == getLabManager()->DockerDockPoint); - if (ImGui::Selectable(name.c_str(), is_selected)) { - getLabManager()->DockerDockPoint = name; - } - if (is_selected) { - ImGui::SetItemDefaultFocus(); - } - } - } - ImGui::EndCombo(); - } - - // Auto-select first dockpoint if none currently selected - if (getLabManager()->DockeeDockPoint.empty()) { - getLabManager()->DockeeDockPoint = dockee_dock_map.begin()->second; - } - - const char* dockee_label = getLabManager()->DockeeDockPoint.c_str(); - - if (ImGui::BeginCombo("Dockee Dockpoint", dockee_label)) { - for (const auto& [index, name] : dockee_dock_map) { - bool is_selected = (name == getLabManager()->DockeeDockPoint); - if (ImGui::Selectable(name.c_str(), is_selected)) { - getLabManager()->DockeeDockPoint = name; - } - if (is_selected) { - ImGui::SetItemDefaultFocus(); - } - } - ImGui::EndCombo(); - } - - if (Button("Begin Docking Test")) { - getLabManager()->beginDockingTest(); - } - - if (Button("Begin Undocking Test")) { - getLabManager()->beginUndockingTest(); - } - } + build_dock_test_options(shipp); + build_bay_test_options(sip); build_animation_options(shipp, sip); } diff --git a/code/lab/dialogs/lab_ui.h b/code/lab/dialogs/lab_ui.h index 888baf7b232..34560fe474b 100644 --- a/code/lab/dialogs/lab_ui.h +++ b/code/lab/dialogs/lab_ui.h @@ -10,6 +10,11 @@ enum class LabTurretAimType { INITIAL, }; +enum class BayMode { + Arrival, + Departure, +}; + class LabUi { public: bool show_thrusters = false; // So that it can be toggled on/off based on the lab mode being changed @@ -49,6 +54,8 @@ class LabUi { weapon_info* wip, int& primary_slot) const; void build_secondary_weapon_combobox(SCP_string& text, weapon_info* wip, int& secondary_slot) const; + static void build_dock_test_options(ship* shipp); + static void build_bay_test_options(ship_info* sip); void build_animation_options(ship* shipp, ship_info* sip) const; void create_afterburner_animation_node( const SCP_vector& anim_triggers) const; diff --git a/code/lab/dialogs/lab_ui_helpers.cpp b/code/lab/dialogs/lab_ui_helpers.cpp index 9248cbbb2f0..9bb45c57bca 100644 --- a/code/lab/dialogs/lab_ui_helpers.cpp +++ b/code/lab/dialogs/lab_ui_helpers.cpp @@ -19,6 +19,22 @@ SCP_map get_docking_point_map(int model_index) return result; } +SCP_map get_bay_paths_map(int model_index) +{ + SCP_map result; + + polymodel* pm = model_get(model_index); + if (pm == nullptr || pm->ship_bay->num_paths <= 0) + return result; + + for (int i = 0; i < pm->ship_bay->num_paths; ++i) { + const char* name = pm->paths[pm->ship_bay->path_indexes[i]].name; + result[i] = (name && *name) ? SCP_string(name) : SCP_string(""); + } + + return result; +} + SCP_string get_ship_table_text(ship_info* sip) { diff --git a/code/lab/dialogs/lab_ui_helpers.h b/code/lab/dialogs/lab_ui_helpers.h index ef463c45a1c..808827dc02b 100644 --- a/code/lab/dialogs/lab_ui_helpers.h +++ b/code/lab/dialogs/lab_ui_helpers.h @@ -5,6 +5,8 @@ SCP_map get_docking_point_map(int model_index); +SCP_map get_bay_paths_map(int model_index); + SCP_string get_ship_table_text(ship_info* sip); SCP_string get_weapon_table_text(weapon_info* wip); diff --git a/code/lab/manager/lab_manager.cpp b/code/lab/manager/lab_manager.cpp index ab230da6d8b..c72a9c4aa56 100644 --- a/code/lab/manager/lab_manager.cpp +++ b/code/lab/manager/lab_manager.cpp @@ -350,6 +350,17 @@ void LabManager::onFrame(float frametime) { } } + // Check if we have finished a bay test. If so, delete the bay ship + if (BayObject >= 0) { + object* bay_objp = &Objects[BayObject]; + ship* shipp = &Ships[bay_objp->instance]; + ai_info* aip = &Ai_info[shipp->ai_index]; + bool hasBayGoal = aip->mode == AIM_BAY_EMERGE || aip->mode == AIM_BAY_DEPART; + if (!hasBayGoal) { + deleteBayObject(); + } + } + } // get correct revolution rate @@ -397,8 +408,8 @@ void LabManager::cleanup() { // Remove all beams beam_delete_all(); - // Properly clean up the docker object - deleteDockerObject(); + // Properly clean up the test objects + deleteTestObjects(); // Remove all objects obj_delete_all(); @@ -416,6 +427,7 @@ void LabManager::cleanup() { CurrentClass = -1; DockerDockPoint.clear(); DockeeDockPoint.clear(); + BayPathMask = 0; CurrentPosition = vmd_zero_vector; CurrentOrientation = vmd_identity_matrix; ModelFilename = ""; @@ -423,7 +435,11 @@ void LabManager::cleanup() { Lab_object_detail_level = -1; } +} +void LabManager::deleteTestObjects() { + deleteDockerObject(); + deleteBayObject(); } void LabManager::deleteDockerObject() { @@ -440,9 +456,27 @@ void LabManager::deleteDockerObject() { } } +void LabManager::deleteBayObject() +{ + if (BayObject >= 0) { + object* bay_objp = &Objects[BayObject]; + ai_info* aip = &Ai_info[Ships[bay_objp->instance].ai_index]; + + // This is kind of a hack but it gets the job done + // Since we're deleting the bay object we can flag bay doors to close + Ships[Objects[CurrentObject].instance].bay_doors_wanting_open = 0; + extern void ai_manage_bay_doors(object * pl_objp, ai_info * aip, bool done); + ai_manage_bay_doors(bay_objp, aip, true); + + obj_delete(BayObject); + BayObject = -1; + reset_ai_path_points(); + } +} + void LabManager::spawnDockerObject() { - deleteDockerObject(); + deleteTestObjects(); if (DockerDockPoint.empty() || DockeeDockPoint.empty()) { return; @@ -480,6 +514,49 @@ void LabManager::spawnDockerObject() { new_objp->pos = final_pos; } +void LabManager::spawnBayObject() +{ + + deleteTestObjects(); + + // Technically this would work but that's not the intention of the test + // and suggests something went wrong + if (BayPathMask == 0) { + return; + } + + // Check ship class index + if (!SCP_vector_inbounds(Ship_info, BayClass)) { + mprintf(("Invalid ship class index %d\n", BayClass)); + return; + } + + object* obj = &Objects[CurrentObject]; + + // Spawn near the target + vec3d spawn_pos = obj->pos; + vec3d offset = {{{0.0f, -50000.0f, -50000.0f}}}; // Spawn it far away then we can move it based on its radius + vec3d final_pos; + vm_vec_add(&final_pos, &spawn_pos, &offset); + + matrix spawn_orient = vmd_identity_matrix; + + BayObject = ship_create(&spawn_orient, &final_pos, BayClass, nullptr, true); + + if (BayObject < 0) { + mprintf(("Failed to create bay test object with ship class index %d!\n", BayClass)); + return; + } + + object* new_objp = &Objects[BayObject]; + + // Set a more reasonable starting position + float offset_radius = obj->radius + new_objp->radius; + offset = {{{0.0f, obj->pos.xyz.y + offset_radius, obj->pos.xyz.z - offset_radius}}}; // Make this selectable or random? + vm_vec_add(&final_pos, &spawn_pos, &offset); + new_objp->pos = final_pos; +} + void LabManager::beginDockingTest() { // Spawn a docker object spawnDockerObject(); @@ -489,6 +566,8 @@ void LabManager::beginDockingTest() { ship* new_shipp = &Ships[new_objp->instance]; ai_info* aip = &Ai_info[new_shipp->ai_index]; + // TODO: Get the pos of the first dock path point and set the ship's position to that + // Ensure AI is ready ai_clear_ship_goals(aip); @@ -573,6 +652,51 @@ void LabManager::beginUndockingTest() { } } +void LabManager::beginBayTest() +{ + // Spawn a bay object + spawnBayObject(); + + if (BayObject < 0) + return; + + // Reset the bay status of the current ship + ship* shipp = &Ships[Objects[CurrentObject].instance]; + shipp->bay_doors_wanting_open = 0; + shipp->bay_doors_status = MA_POS_NOT_SET; + + if (BayTestMode == BayMode::Arrival) { + object* new_objp = &Objects[BayObject]; + ship* new_shipp = &Ships[new_objp->instance]; + ai_info* aip = &Ai_info[new_shipp->ai_index]; + + // Ensure AI is ready + ai_clear_ship_goals(aip); + + if (ai_acquire_emerge_path(new_objp, CurrentObject, BayPathMask) == -1) { + mprintf(("Unable to acquire arrival path on anchor ship %s\n", Ships[Objects[CurrentObject].instance].ship_name)); // Warning instead of print? + deleteBayObject(); + return; + } + } + + if (BayTestMode == BayMode::Departure) { + object* new_objp = &Objects[BayObject]; + ship* new_shipp = &Ships[new_objp->instance]; + ai_info* aip = &Ai_info[new_shipp->ai_index]; + // Ensure AI is ready + ai_clear_ship_goals(aip); + if (ai_acquire_depart_path(new_objp, CurrentObject, BayPathMask) == -1) { + mprintf(("Unable to acquire departure path on anchor ship %s\n", Ships[Objects[CurrentObject].instance].ship_name)); // Warning instead of print? + deleteBayObject(); + return; + } + + // Set the object's position to the start of the path + new_objp->pos = Path_points[aip->path_start].pos; + } +} + void LabManager::changeDisplayedObject(LabMode mode, int info_index, int subtype) { // Removing this allows reseting by clicking on the object again, // making it easier to respawn destroyed objects @@ -608,6 +732,8 @@ void LabManager::changeDisplayedObject(LabMode mode, int info_index, int subtype DockeeDockPoint.clear(); DockerDockPoint.clear(); + BayPathMask = 0; + switch (CurrentMode) { case LabMode::Ship: CurrentObject = ship_create(&CurrentOrientation, &CurrentPosition, CurrentClass); @@ -616,7 +742,9 @@ void LabManager::changeDisplayedObject(LabMode mode, int info_index, int subtype Player_ship = &Ships[Objects[CurrentObject].instance]; ai_paused = 0; - // Set the ship to play dead so it doesn't move. There is a special carveout to still allow subsystem rotations/translations in the lab, though + // Set the ship to play dead so it doesn't move. For the lab there are two special carveouts: + // 1: Allow subsystem rotations/translations + // 2: Allow subystems to be processed ai_add_ship_goal_scripting(AI_GOAL_PLAY_DEAD_PERSISTENT, -1, 100, nullptr, &Ai_info[Player_ship->ai_index], 0, 0); } break; diff --git a/code/lab/manager/lab_manager.h b/code/lab/manager/lab_manager.h index 376b3aa32c2..6d0418a2a66 100644 --- a/code/lab/manager/lab_manager.h +++ b/code/lab/manager/lab_manager.h @@ -36,18 +36,30 @@ class LabManager { // displayed object void changeDisplayedObject(LabMode type, int info_index, int subtype = -1); + // Deletes all testing objects + void deleteTestObjects(); + // Deletes the docker object if exists void deleteDockerObject(); + // Deletes the bay object if exists + void deleteBayObject(); + // Spawns a docker object to use with dock or undock tests. Deletes the current docker object if it exists void spawnDockerObject(); + // Spawns a bay object to use with bay tests. Deletes the current bay object if it exists + void spawnBayObject(); + // Begins the docking test void beginDockingTest(); // Begins the undocking test void beginUndockingTest(); + // Begins the bay test + void beginBayTest(); + void close() { animation::ModelAnimationSet::stopAnimations(); @@ -78,9 +90,13 @@ class LabManager { int CurrentSubtype = -1; int CurrentClass = -1; int DockerObject = -1; + int BayObject = -1; int DockerClass = 0; + int BayClass = 0; SCP_string DockerDockPoint; SCP_string DockeeDockPoint; + int BayPathMask = 0; + BayMode BayTestMode = BayMode::Arrival; vec3d CurrentPosition = vmd_zero_vector; matrix CurrentOrientation = vmd_identity_matrix; SCP_string ModelFilename; From 4d8dbcb4578c2b2fdb07fd3cad19d2b2f09b1436 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 13 Aug 2025 08:35:11 -0500 Subject: [PATCH 343/466] Cherry pick recent qtfred generalized dialogs --- qtfred/source_groups.cmake | 9 + .../ui/dialogs/General/CheckBoxListDialog.cpp | 71 +++++++ .../ui/dialogs/General/CheckBoxListDialog.h | 27 +++ .../ui/dialogs/General/ImagePickerDialog.cpp | 175 ++++++++++++++++++ .../ui/dialogs/General/ImagePickerDialog.h | 47 +++++ qtfred/src/ui/util/ImageRenderer.cpp | 97 ++++++++++ qtfred/src/ui/util/ImageRenderer.h | 24 +++ qtfred/ui/CheckBoxListDialog.ui | 79 ++++++++ 8 files changed, 529 insertions(+) create mode 100644 qtfred/src/ui/dialogs/General/CheckBoxListDialog.cpp create mode 100644 qtfred/src/ui/dialogs/General/CheckBoxListDialog.h create mode 100644 qtfred/src/ui/dialogs/General/ImagePickerDialog.cpp create mode 100644 qtfred/src/ui/dialogs/General/ImagePickerDialog.h create mode 100644 qtfred/src/ui/util/ImageRenderer.cpp create mode 100644 qtfred/src/ui/util/ImageRenderer.h create mode 100644 qtfred/ui/CheckBoxListDialog.ui diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index a4bd7250f6a..17801b50898 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -161,8 +161,16 @@ add_file_folder("Source/UI/Dialogs/ShipEditor" src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp ) +add_file_folder("Source/UI/General" + src/ui/dialogs/General/CheckBoxListDialog.cpp + src/ui/dialogs/General/CheckBoxListDialog.h + src/ui/dialogs/General/ImagePickerDialog.cpp + src/ui/dialogs/General/ImagePickerDialog.h +) add_file_folder("Source/UI/Util" + src/ui/util/ImageRenderer.cpp + src/ui/util/ImageRenderer.h src/ui/util/menu.cpp src/ui/util/menu.h src/ui/util/SignalBlockers.cpp @@ -186,6 +194,7 @@ add_file_folder("UI" ui/BackgroundEditor.ui ui/BriefingEditorDialog.ui ui/CampaignEditorDialog.ui + ui/CheckBoxListDialog.ui ui/CommandBriefingDialog.ui ui/CustomWingNamesDialog.ui ui/EventEditorDialog.ui diff --git a/qtfred/src/ui/dialogs/General/CheckBoxListDialog.cpp b/qtfred/src/ui/dialogs/General/CheckBoxListDialog.cpp new file mode 100644 index 00000000000..2a3b973b28d --- /dev/null +++ b/qtfred/src/ui/dialogs/General/CheckBoxListDialog.cpp @@ -0,0 +1,71 @@ +#include "ui/dialogs/General/CheckBoxListDialog.h" + +#include "ui_CheckBoxListDialog.h" + +#include +#include +#include + +namespace fso::fred::dialogs { + +CheckBoxListDialog::CheckBoxListDialog(QWidget* parent) : QDialog(parent), ui(new Ui::CheckBoxListDialog) +{ + ui->setupUi(this); + + // Allow resizing + this->setSizeGripEnabled(true); + + // clear placeholder layout contents if any + if (ui->checkboxContainer->layout()) { + QLayoutItem* item; + while ((item = ui->checkboxContainer->layout()->takeAt(0)) != nullptr) { + delete item->widget(); + delete item; + } + delete ui->checkboxContainer->layout(); + } + + // Set a fresh layout + auto* layout = new QVBoxLayout(ui->checkboxContainer); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(4); +} + +void CheckBoxListDialog::setCaption(const QString& text) +{ + this->setWindowTitle(text); +} + +void CheckBoxListDialog::setOptions(const QVector>& options) +{ + // Clear previous checkboxes + for (auto* cb : _checkboxes) { + cb->deleteLater(); + } + _checkboxes.clear(); + + auto* layout = qobject_cast(ui->checkboxContainer->layout()); + if (!layout) { + return; + } + + for (const auto& [label, checked] : options) { + auto* cb = new QCheckBox(label, this); + cb->setChecked(checked); + layout->addWidget(cb); + _checkboxes.append(cb); + } + // Add spacer to push items to top + layout->addStretch(); +} + +QVector CheckBoxListDialog::getCheckedStates() const +{ + QVector states; + for (auto* cb : _checkboxes) { + states.append(cb->isChecked()); + } + return states; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/General/CheckBoxListDialog.h b/qtfred/src/ui/dialogs/General/CheckBoxListDialog.h new file mode 100644 index 00000000000..57273e4da38 --- /dev/null +++ b/qtfred/src/ui/dialogs/General/CheckBoxListDialog.h @@ -0,0 +1,27 @@ +#pragma once + + +#include +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class CheckBoxListDialog; +} + +class CheckBoxListDialog : public QDialog { + Q_OBJECT + public: + explicit CheckBoxListDialog(QWidget* parent = nullptr); + + void setCaption(const QString& text); + void setOptions(const QVector>& options); + QVector getCheckedStates() const; + + private: + Ui::CheckBoxListDialog* ui; + QVector _checkboxes; +}; + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/General/ImagePickerDialog.cpp b/qtfred/src/ui/dialogs/General/ImagePickerDialog.cpp new file mode 100644 index 00000000000..e4b8ebd17d7 --- /dev/null +++ b/qtfred/src/ui/dialogs/General/ImagePickerDialog.cpp @@ -0,0 +1,175 @@ +#include "ImagePickerDialog.h" + +#include "ui/util/ImageRenderer.h" + +#include +#include +#include +#include + +using fso::fred::util::loadImageToQImage; + +ImagePickerDialog::ImagePickerDialog(QWidget* parent) : QDialog(parent) +{ + setWindowTitle("Choose Image"); + resize(720, 520); + + auto* vbox = new QVBoxLayout(this); + + _filterEdit = new QLineEdit(this); + _filterEdit->setPlaceholderText("Filter by name..."); + vbox->addWidget(_filterEdit); + + _list = new QListWidget(this); + _list->setViewMode(QListView::IconMode); + _list->setIconSize(QSize(112, 112)); + _list->setResizeMode(QListView::Adjust); + _list->setUniformItemSizes(true); + _list->setMovement(QListView::Static); + _list->setSelectionMode(QAbstractItemView::SingleSelection); + _list->setSpacing(8); + vbox->addWidget(_list, 1); + + auto* hbox = new QHBoxLayout(); + hbox->addStretch(1); + _okBtn = new QPushButton("OK", this); + _cancelBtn = new QPushButton("Cancel", this); + hbox->addWidget(_okBtn); + hbox->addWidget(_cancelBtn); + vbox->addLayout(hbox); + + connect(_filterEdit, &QLineEdit::textChanged, this, &ImagePickerDialog::onFilterTextChanged); + connect(_list, &QListWidget::itemActivated, this, &ImagePickerDialog::onItemActivated); + connect(_okBtn, &QPushButton::clicked, this, &ImagePickerDialog::onOk); + connect(_cancelBtn, &QPushButton::clicked, this, &QDialog::reject); +} + +void ImagePickerDialog::setImageFilenames(const QStringList& filenames) +{ + _allFiles = filenames; + rebuildList(); +} + +void ImagePickerDialog::setInitialSelection(const QString& filename) +{ + _selected = filename; + // Apply after list is built + for (int i = 0; i < _list->count(); ++i) { + auto* it = _list->item(i); + if (it->data(Qt::UserRole).toString() == filename) { + _list->setCurrentItem(it); + _list->scrollToItem(it, QAbstractItemView::PositionAtCenter); + break; + } + } +} + +void ImagePickerDialog::onFilterTextChanged(const QString& text) +{ + _filterText = text; + rebuildList(); +} + +void ImagePickerDialog::onItemActivated(QListWidgetItem* item) +{ + if (!item) + return; + _selected = item->data(Qt::UserRole).toString(); + accept(); +} + +void ImagePickerDialog::onOk() +{ + auto* item = _list->currentItem(); + _selected = item ? item->data(Qt::UserRole).toString() : QString(); + accept(); +} + +QIcon ImagePickerDialog::iconFor(const QString& name) +{ + if (_thumbCache.contains(name)) { + return {_thumbCache.value(name)}; + } + + // Decode via bmpman using ImageRenderer + QImage img; + QString err; + if (loadImageToQImage(name.toStdString(), img, &err) && !img.isNull()) { + // Scale to icon size for snappy scrolling + QPixmap pm = QPixmap::fromImage(img.scaled(_list->iconSize(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + _thumbCache.insert(name, pm); + return {pm}; + } + + // Fallback file icon + return style()->standardIcon(QStyle::SP_FileIcon); +} + +static QIcon makeEmptySlotIcon(const QSize& size) +{ + QPixmap pm(size); + pm.fill(Qt::transparent); + + QPainter p(&pm); + p.setRenderHint(QPainter::Antialiasing); + + // Border + QPen pen(QColor(180, 180, 180)); + pen.setWidth(2); + p.setPen(pen); + p.setBrush(Qt::NoBrush); + p.drawRect(pm.rect().adjusted(1, 1, -2, -2)); + + // Subtle X + QPen xPen(QColor(180, 180, 180, 180)); // slightly transparent gray + xPen.setWidth(2); + p.setPen(xPen); + + const int pad = 6; // padding inside the square so X isn't edge-to-edge + QPoint topLeft(pad, pad); + QPoint bottomRight(size.width() - pad, size.height() - pad); + QPoint topRight(bottomRight.x(), topLeft.y()); + QPoint bottomLeft(topLeft.x(), bottomRight.y()); + + p.drawLine(topLeft, bottomRight); + p.drawLine(topRight, bottomLeft); + + p.end(); + + return {pm}; +} + + +void ImagePickerDialog::rebuildList() +{ + _list->clear(); + + // Add unset option first, if enabled + if (_allowUnset) { + auto unsetIcon = makeEmptySlotIcon(_list->iconSize()); + auto* unsetItem = new QListWidgetItem(unsetIcon, ""); + unsetItem->setData(Qt::UserRole, QString()); // empty filename + unsetItem->setToolTip("Remove image / no image selected"); + _list->addItem(unsetItem); + + if (_selected.isEmpty()) { + _list->setCurrentItem(unsetItem); + } + } + + const auto fl = _filterText.trimmed().toLower(); + for (const auto& name : _allFiles) { + if (!fl.isEmpty() && !name.toLower().contains(fl)) + continue; + + auto icon = iconFor(name); + auto* it = new QListWidgetItem(icon, name); + it->setData(Qt::UserRole, name); + it->setToolTip(name); + _list->addItem(it); + + if (!_selected.isEmpty() && name == _selected) { + _list->setCurrentItem(it); + } + } +} \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/General/ImagePickerDialog.h b/qtfred/src/ui/dialogs/General/ImagePickerDialog.h new file mode 100644 index 00000000000..65b86f184ac --- /dev/null +++ b/qtfred/src/ui/dialogs/General/ImagePickerDialog.h @@ -0,0 +1,47 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +class ImagePickerDialog : public QDialog { + Q_OBJECT + public: + explicit ImagePickerDialog(QWidget* parent = nullptr); + + void setImageFilenames(const QStringList& filenames); + void setInitialSelection(const QString& filename); + QString selectedFile() const + { + return _selected; + } + + void allowUnset(bool enable) + { + _allowUnset = enable; + } + + private slots: + void onFilterTextChanged(const QString& text); + void onItemActivated(QListWidgetItem* item); + void onOk(); + + private: // NOLINT(readability-redundant-access-specifiers) + void rebuildList(); + QIcon iconFor(const QString& name); + + QLineEdit* _filterEdit{nullptr}; + QListWidget* _list{nullptr}; + QPushButton* _okBtn{nullptr}; + QPushButton* _cancelBtn{nullptr}; + + QStringList _allFiles; + QString _filterText; + QString _selected; + + QHash _thumbCache; + + bool _allowUnset{false}; +}; \ No newline at end of file diff --git a/qtfred/src/ui/util/ImageRenderer.cpp b/qtfred/src/ui/util/ImageRenderer.cpp new file mode 100644 index 00000000000..15350038e1e --- /dev/null +++ b/qtfred/src/ui/util/ImageRenderer.cpp @@ -0,0 +1,97 @@ +#include "ImageRenderer.h" + +#include // bm_load, bm_get_info, bm_has_alpha_channel +#include + +#include + +namespace fso::fred::util { + +static void setError(QString* outError, const QString& text) +{ + if (outError) + *outError = text; +} + +bool loadHandleToQImage(int bmHandle, QImage& outImage, QString* outError) +{ + outImage = QImage(); // clear + + if (bmHandle < 0) { + setError(outError, QStringLiteral("Invalid bitmap handle.")); + return false; + } + + int w = 0, h = 0; + ushort flags = 0; + int nframes = 0, fps = 0; + + // Use the returned handle (first frame if this is an animation.. TODO: Handle animations. Will be useful for Heads) + int srcHandle = bm_get_info(bmHandle, &w, &h, &flags, &nframes, &fps); + if (srcHandle < 0 || w <= 0 || h <= 0) { + setError(outError, QStringLiteral("Bitmap has invalid info.")); + return false; + } + + if (w <= 0 || h <= 0) { + setError(outError, QStringLiteral("Bitmap has invalid dimensions.")); + return false; + } + + const bool hasAlpha = bm_has_alpha_channel(bmHandle); + const int channels = hasAlpha ? 4 : 3; + const size_t bufSize = static_cast(w) * static_cast(h) * channels; + + // Allocate a temporary buffer and let the renderer copy pixels into it + QByteArray buffer; + buffer.resize(static_cast(bufSize)); + if (buffer.size() != static_cast(bufSize)) { + setError(outError, QStringLiteral("Out of memory allocating pixel buffer.")); + return false; + } + + // Copy RGBA pixels into the buffer + gr_get_bitmap_from_texture(buffer.data(), bmHandle); + + // Build QImage by copying to own memory + if (hasAlpha) { + QImage tmp(reinterpret_cast(buffer.constData()), w, h, QImage::Format_RGBA8888); + outImage = tmp.copy(); + } else { + QImage tmp(reinterpret_cast(buffer.constData()), w, h, QImage::Format_RGB888); + outImage = tmp.copy(); + } + + if (outImage.isNull()) { + setError(outError, QStringLiteral("Failed to construct QImage.")); + return false; + } + + return true; +} + +bool loadImageToQImage(const std::string& filename, QImage& outImage, QString* outError) +{ + outImage = QImage(); + + if (filename.empty()) { + setError(outError, QStringLiteral("Empty filename.")); + return false; + } + + // Let bmpman resolve the file + int handle = bm_load(filename.c_str()); + if (handle < 0) { + setError(outError, QStringLiteral("bm_load failed for \"%1\".").arg(QString::fromStdString(filename))); + return false; + } + + const bool ok = loadHandleToQImage(handle, outImage, outError); + + + // bm_unload(handle); TODO test unloading + + return ok; +} + +} // namespace fso::fred::util diff --git a/qtfred/src/ui/util/ImageRenderer.h b/qtfred/src/ui/util/ImageRenderer.h new file mode 100644 index 00000000000..6800293d6ba --- /dev/null +++ b/qtfred/src/ui/util/ImageRenderer.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace fso::fred::util { + +/** + * @brief Loads an image file (any format FSO supports) into a QImage for UI preview. + * @param filename Path or VFS name relative to FSO search paths. + * @param outImage On success, receives a valid QImage copy. + * @param outError Optional: receives any error as a string. + * @return true on success, false otherwise. + */ +bool loadImageToQImage(const std::string& filename, QImage& outImage, QString* outError = nullptr); + +/** + * @brief Same as above but using an existing bmpman handle. + * Useful if the caller already called bm_load(). + */ +bool loadHandleToQImage(int bmHandle, QImage& outImage, QString* outError = nullptr); + +} // namespace fso::fred::util diff --git a/qtfred/ui/CheckBoxListDialog.ui b/qtfred/ui/CheckBoxListDialog.ui new file mode 100644 index 00000000000..deff328f2d9 --- /dev/null +++ b/qtfred/ui/CheckBoxListDialog.ui @@ -0,0 +1,79 @@ + + + fso::fred::dialogs::CheckBoxListDialog + + + + 0 + 0 + 217 + 294 + + + + Select Options + + + + + + true + + + + + 0 + 0 + 197 + 245 + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + fso::fred::dialogs::CheckBoxListDialog + accept() + + + 248 + 398 + + + 157 + 206 + + + + + buttonBox + rejected() + fso::fred::dialogs::CheckBoxListDialog + reject() + + + 316 + 400 + + + 308 + 210 + + + + + From 9ecc158aee394457181d78c48713f3b163fc438f Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 13 Aug 2025 16:42:19 -0500 Subject: [PATCH 344/466] initialize used_pool --- qtfred/src/mission/Editor.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index ec5900976d5..f08fd76a1e3 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -186,7 +186,6 @@ void Editor::maybeUseAutosave(std::string& filepath) bool Editor::loadMission(const std::string& mission_name, int flags) { char name[512], * old_name; int i, j, k, ob; - int used_pool[MAX_WEAPON_TYPES]; object* objp; // activate the localizer hash table @@ -335,6 +334,10 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { } } + int used_pool[MAX_WEAPON_TYPES] = {}; + for (auto& pool : used_pool) + pool = 0; + for (i = 0; i < Num_teams; i++) { generate_team_weaponry_usage_list(i, _weapon_usage[i]); for (j = 0; j < Team_data[i].num_weapon_choices; j++) { From 0b6150e9616a61d56b3cc137f23df7ca45be31a2 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Thu, 14 Aug 2025 21:14:27 -0500 Subject: [PATCH 345/466] Qt fred misc bugs (#6933) * fix main window title * Fix loadout dialog crashing --- qtfred/src/main.cpp | 2 +- qtfred/src/ui/dialogs/LoadoutDialog.cpp | 51 ++++++++++++++++--------- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/qtfred/src/main.cpp b/qtfred/src/main.cpp index 1942e794a24..4557d3ab008 100644 --- a/qtfred/src/main.cpp +++ b/qtfred/src/main.cpp @@ -102,7 +102,7 @@ int main(int argc, char* argv[]) { // Expect that the platform library is in the same directory QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath()); - QGuiApplication::setApplicationDisplayName(QApplication::tr("qtFRED v%1").arg(FS_VERSION_FULL)); + //QGuiApplication::setApplicationDisplayName(QApplication::tr("qtFRED v%1").arg(FS_VERSION_FULL)); #ifndef NDEBUG QLoggingCategory::defaultCategory()->setEnabled(QtDebugMsg, true); diff --git a/qtfred/src/ui/dialogs/LoadoutDialog.cpp b/qtfred/src/ui/dialogs/LoadoutDialog.cpp index 8a25edc2683..17574a7f02a 100644 --- a/qtfred/src/ui/dialogs/LoadoutDialog.cpp +++ b/qtfred/src/ui/dialogs/LoadoutDialog.cpp @@ -321,7 +321,8 @@ void LoadoutDialog::addShipButtonClicked() SCP_vector list; for (const auto& item : ui->listShipsNotUsed->selectedItems()){ - list.emplace_back(item->text().toStdString()); + SCP_string shipName = item->text().toUtf8().constData(); + list.emplace_back(shipName); } if (_mode == TABLE_MODE) { @@ -338,7 +339,8 @@ void LoadoutDialog::addWeaponButtonClicked() SCP_vector list; for (const auto& item: ui->listWeaponsNotUsed->selectedItems()){ - list.emplace_back(item->text().toStdString()); + SCP_string weaponName = item->text().toUtf8().constData(); + list.emplace_back(weaponName); } if (_mode == TABLE_MODE) { @@ -355,7 +357,8 @@ void LoadoutDialog::removeShipButtonClicked() SCP_vector list; for (const auto& item : ui->usedShipsList->selectedItems()){ - list.emplace_back(item->text().toStdString()); + SCP_string shipName = item->text().toUtf8().constData(); + list.emplace_back(shipName); } if (_mode == TABLE_MODE) { @@ -372,7 +375,8 @@ void LoadoutDialog::removeWeaponButtonClicked() SCP_vector list; for (const auto& item : ui->usedWeaponsList->selectedItems()){ - list.emplace_back(item->text().toStdString()); + SCP_string weaponName = item->text().toUtf8().constData(); + list.emplace_back(weaponName); } if (_mode == TABLE_MODE) { @@ -436,7 +440,7 @@ void LoadoutDialog::onExtraItemsViaVariableCombo() } SCP_vector list = (_lastSelectionChanged == USED_SHIPS) ? getSelectedShips() : getSelectedWeapons(); - SCP_string chosenVariable = ui->extraItemsViaVariableCombo->currentText().toStdString(); + SCP_string chosenVariable = ui->extraItemsViaVariableCombo->currentText().toUtf8().constData(); _model->setExtraAllocatedViaVariable(list, chosenVariable, _lastSelectionChanged == USED_SHIPS, _mode == VARIABLE_MODE); updateUI(); @@ -578,7 +582,8 @@ void LoadoutDialog::updateUI() bool found = false; for (int x = 0; x < ui->usedShipsList->rowCount(); ++x){ - if (ui->usedShipsList->item(x,0) && lcase_equal(ui->usedShipsList->item(x, 0)->text().toStdString(), shipName)) { + SCP_string usedShipName = ui->usedShipsList->item(x, 0)->text().toUtf8().constData(); + if (ui->usedShipsList->item(x,0) && lcase_equal(usedShipName, shipName)) { found = true; // update the quantities here, and make sure it's visible ui->usedShipsList->item(x, 1)->setText(newShip.first.substr(divider + 1).c_str()); @@ -600,7 +605,8 @@ void LoadoutDialog::updateUI() // remove from the unused list for (int x = 0; x < ui->listShipsNotUsed->count(); ++x) { - if (lcase_equal(ui->listShipsNotUsed->item(x)->text().toStdString(), shipName)) { + SCP_string usedShipName = ui->listShipsNotUsed->item(x)->text().toUtf8().constData(); + if (lcase_equal(usedShipName, shipName)) { ui->listShipsNotUsed->setRowHidden(x, true); break; } @@ -610,7 +616,8 @@ void LoadoutDialog::updateUI() bool found = false; for (int x = 0; x < ui->listShipsNotUsed->count(); ++x){ - if (lcase_equal(ui->listShipsNotUsed->item(x)->text().toStdString(), shipName)) { + SCP_string usedShipName = ui->listShipsNotUsed->item(x)->text().toUtf8().constData(); + if (lcase_equal(usedShipName, shipName)) { found = true; ui->listShipsNotUsed->setRowHidden(x, false); break; @@ -623,8 +630,9 @@ void LoadoutDialog::updateUI() // remove from the used list for (int x = 0; x < ui->usedShipsList->rowCount(); ++x) { + SCP_string usedShipName = ui->usedShipsList->item(x, 0)->text().toUtf8().constData(); if (ui->usedShipsList->item(x, 0) && - lcase_equal(ui->usedShipsList->item(x, 0)->text().toStdString(), shipName)) { + lcase_equal(usedShipName, shipName)) { ui->usedShipsList->setRowHidden(x, true); break; } @@ -641,7 +649,8 @@ void LoadoutDialog::updateUI() // Add or update in the used list for (int x = 0; x < ui->usedWeaponsList->rowCount(); ++x) { - if (ui->usedWeaponsList->item(x,0) && lcase_equal(ui->usedWeaponsList->item(x, 0)->text().toStdString(), weaponName)) { + SCP_string usedWepName = ui->usedWeaponsList->item(x, 0)->text().toUtf8().constData(); + if (ui->usedWeaponsList->item(x,0) && lcase_equal(usedWepName, weaponName)) { found = true; // only need to update the quantities here. ui->usedWeaponsList->item(x, 1)->setText(newWeapon.first.substr(divider + 1).c_str()); @@ -664,7 +673,8 @@ void LoadoutDialog::updateUI() // remove from the unused list for (int x = 0; x < ui->listWeaponsNotUsed->count(); ++x) { - if (lcase_equal(ui->listWeaponsNotUsed->item(x)->text().toStdString(), weaponName)) { + SCP_string usedWepName = ui->listWeaponsNotUsed->item(x)->text().toUtf8().constData(); + if (lcase_equal(usedWepName, weaponName)) { ui->listWeaponsNotUsed->setRowHidden(x, true); break; } @@ -674,7 +684,8 @@ void LoadoutDialog::updateUI() bool found = false; for (int x = 0; x < ui->listWeaponsNotUsed->count(); ++x){ - if (ui->listWeaponsNotUsed->item(x) && lcase_equal(ui->listWeaponsNotUsed->item(x)->text().toStdString(), weaponName)) { + SCP_string usedWepName = ui->listWeaponsNotUsed->item(x)->text().toUtf8().constData(); + if (ui->listWeaponsNotUsed->item(x) && lcase_equal(usedWepName, weaponName)) { found = true; ui->listWeaponsNotUsed->setRowHidden(x, false); break; @@ -687,8 +698,9 @@ void LoadoutDialog::updateUI() // remove from the used list for (int x = 0; x < ui->usedWeaponsList->rowCount(); ++x) { + SCP_string usedWepName = ui->usedWeaponsList->item(x, 0)->text().toUtf8().constData(); if (ui->usedWeaponsList->item(x, 0) && - lcase_equal(ui->usedWeaponsList->item(x, 0)->text().toStdString(), weaponName)) { + lcase_equal(usedWepName, weaponName)) { ui->usedWeaponsList->setRowHidden(x, true); break; } @@ -832,8 +844,8 @@ void LoadoutDialog::updateUI() ui->extraItemsViaVariableCombo->setCurrentIndex(0); } else { for (int x = 0; x < ui->extraItemsViaVariableCombo->count(); ++x) { - if (lcase_equal(ui->extraItemsViaVariableCombo->itemText(x).toStdString(), - currentVariable)) { + SCP_string variableName = ui->extraItemsViaVariableCombo->itemText(x).toUtf8().constData(); + if (lcase_equal(variableName, currentVariable)) { ui->extraItemsViaVariableCombo->setCurrentIndex(x); break; } @@ -850,7 +862,8 @@ void LoadoutDialog::updateUI() bool found = false; for (const auto& weapon : requiredWeapons) { - if (ui->usedWeaponsList->item(x, 0) && ui->usedWeaponsList->item(x,2) && lcase_equal(ui->usedWeaponsList->item(x, 0)->text().toStdString(), weapon)) { + SCP_string usedWepName = ui->usedWeaponsList->item(x, 0)->text().toUtf8().constData(); + if (ui->usedWeaponsList->item(x, 0) && ui->usedWeaponsList->item(x,2) && lcase_equal(usedWepName, weapon)) { found = true; ui->usedWeaponsList->item(x, 2)->setText("Yes"); break; @@ -870,7 +883,8 @@ SCP_vector LoadoutDialog::getSelectedShips() for (int x = 0; x < ui->usedShipsList->rowCount(); ++x) { if (ui->usedShipsList->item(x, 0) && ui->usedShipsList->item(x,0)->isSelected()) { - namesOut.emplace_back(ui->usedShipsList->item(x, 0)->text().toStdString()); + SCP_string shipName = ui->usedShipsList->item(x, 0)->text().toUtf8().constData(); + namesOut.emplace_back(shipName); } } @@ -883,7 +897,8 @@ SCP_vector LoadoutDialog::getSelectedWeapons() for (int x = 0; x < ui->usedWeaponsList->rowCount(); ++x) { if (ui->usedWeaponsList->item(x, 0) && ui->usedWeaponsList->item(x, 0)->isSelected()) { - namesOut.emplace_back(ui->usedWeaponsList->item(x, 0)->text().toStdString()); + SCP_string weaponName = ui->usedWeaponsList->item(x, 0)->text().toUtf8().constData(); + namesOut.emplace_back(weaponName); } } From be4d4eb1d57419949de6e5857eebfff42b24832d Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Fri, 15 Aug 2025 19:09:12 -0700 Subject: [PATCH 346/466] add hooks --- code/ship/ship.cpp | 75 ++++++++++++++++++++++++++++++++------------ code/ship/ship.h | 3 ++ code/ship/shipfx.cpp | 33 ++++++++++++------- 3 files changed, 80 insertions(+), 31 deletions(-) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 6c2de5fc551..f4409ce9cef 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -1073,6 +1073,9 @@ void ship_info::clone(const ship_info& other) impact_spew = other.impact_spew; damage_spew = other.damage_spew; + death_roll_exp_particles = other.death_roll_exp_particles; + pre_death_exp_particles = other.pre_death_exp_particles; + propagating_exp_particles = other.propagating_exp_particles; split_particles = other.split_particles; knossos_end_particles = other.knossos_end_particles; regular_end_particles = other.regular_end_particles; @@ -1434,6 +1437,9 @@ void ship_info::move(ship_info&& other) std::swap(impact_spew, other.impact_spew); std::swap(damage_spew, other.damage_spew); + std::swap(death_roll_exp_particles, other.death_roll_exp_particles); + std::swap(pre_death_exp_particles, other.pre_death_exp_particles); + std::swap(propagating_exp_particles, other.propagating_exp_particles); std::swap(split_particles, other.split_particles); std::swap(knossos_end_particles, other.knossos_end_particles); std::swap(regular_end_particles, other.regular_end_particles); @@ -1798,6 +1804,10 @@ ship_info::ship_info() static auto default_damage_spew = default_ship_particle_effect(LegacyShipParticleType::DAMAGE_SPEW, 1, 0, 1.3f, 0.7f, 1.0f, 1.0f, 12.0f, 3.0f, 0.0f, 1.0f, particle::Anim_bitmap_id_smoke, 0.7f); damage_spew = default_damage_spew; + death_roll_exp_particles = particle::ParticleEffectHandle::invalid(); + pre_death_exp_particles = particle::ParticleEffectHandle::invalid(); + propagating_exp_particles = particle::ParticleEffectHandle::invalid(); + static auto default_split_particles = default_ship_particle_effect(LegacyShipParticleType::SPLIT_PARTICLES, 80, 40, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 2.0f, 1.0f, particle::Anim_bitmap_id_smoke2, 1.f); split_particles = default_split_particles; @@ -3820,7 +3830,9 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool sip->explosion_splits_ship = sip->explosion_propagates == 1; } - if(optional_string("$Propagating Expl Radius Multiplier:")){ + if (optional_string("$Propagating Explosion Effect:")) { + sip->propagating_exp_particles = particle::util::parseEffect(sip->name); + } else if (optional_string("$Propagating Expl Radius Multiplier:")) { stuff_float(&sip->prop_exp_rad_mult); if(sip->prop_exp_rad_mult <= 0) { // on invalid value return to default setting @@ -3839,7 +3851,9 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool sip->death_roll_base_time = 2; } - if(optional_string("$Death-Roll Explosion Radius Mult:")){ + if (optional_string("$Death Roll Explosion Effect:")) { + sip->death_roll_exp_particles = particle::util::parseEffect(sip->name); + } else if (optional_string("$Death-Roll Explosion Radius Mult:")) { stuff_float(&sip->death_roll_r_mult); if (sip->death_roll_r_mult < 0) sip->death_roll_r_mult = 0; @@ -3851,7 +3865,9 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool sip->death_roll_time_mult = 1.0f; } - if(optional_string("$Death FX Explosion Radius Mult:")){ + if (optional_string("$Death FX Explosion Effect:")) { + sip->pre_death_exp_particles = particle::util::parseEffect(sip->name); + } else if (optional_string("$Death FX Explosion Radius Mult:")) { stuff_float(&sip->death_fx_r_mult); if (sip->death_fx_r_mult < 0) sip->death_fx_r_mult = 0; @@ -9409,18 +9425,27 @@ static void ship_dying_frame(object *objp, int ship_num) // Get a random point on the surface of a submodel vec3d pnt1 = submodel_get_random_point(pm->id, pm->detail[0]); - model_instance_local_to_global_point(&outpnt, &pnt1, shipp->model_instance_num, pm->detail[0], &objp->orient, &objp->pos ); + if (sip->death_roll_exp_particles.isValid()) { + vec3d center_to_point = objp->pos - pnt1; + vm_vec_normalize(¢er_to_point); + auto source = particle::ParticleManager::get()->createSource(sip->death_roll_exp_particles); + source->setHost(std::make_unique(objp, pnt1)); + source->setNormal(center_to_point); + source->finishCreation(); + } else { + model_instance_local_to_global_point(&outpnt, &pnt1, shipp->model_instance_num, pm->detail[0], &objp->orient, &objp->pos ); - float rad = objp->radius*0.1f; + float rad = objp->radius*0.1f; - if (sip->death_roll_r_mult != 1.0f) - rad *= sip->death_roll_r_mult; + if (sip->death_roll_r_mult != 1.0f) + rad *= sip->death_roll_r_mult; - int fireball_type = fireball_ship_explosion_type(sip); - if(fireball_type < 0) { - fireball_type = FIREBALL_EXPLOSION_LARGE1 + Random::next(FIREBALL_NUM_LARGE_EXPLOSIONS); + int fireball_type = fireball_ship_explosion_type(sip); + if(fireball_type < 0) { + fireball_type = FIREBALL_EXPLOSION_LARGE1 + Random::next(FIREBALL_NUM_LARGE_EXPLOSIONS); + } + fireball_create( &outpnt, fireball_type, FIREBALL_LARGE_EXPLOSION, OBJ_INDEX(objp), rad, false, &objp->phys_info.vel ); } - fireball_create( &outpnt, fireball_type, FIREBALL_LARGE_EXPLOSION, OBJ_INDEX(objp), rad, false, &objp->phys_info.vel ); // start the next fireball up in the next 50 - 200 ms (2-3 per frame) int min_time = 333; int max_time = 500; @@ -9511,24 +9536,34 @@ static void ship_dying_frame(object *objp, int ship_num) } // Find two random vertices on the model, then average them // and make the piece start there. - vec3d tmp, outpnt; + vec3d avgpnt, outpnt; // Gets two random points on the surface of a submodel [KNOSSOS] vec3d pnt1 = submodel_get_random_point(pm->id, pm->detail[0]); vec3d pnt2 = submodel_get_random_point(pm->id, pm->detail[0]); - vm_vec_avg( &tmp, &pnt1, &pnt2 ); - model_instance_local_to_global_point(&outpnt, &tmp, pm, pmi, pm->detail[0], &objp->orient, &objp->pos ); + vm_vec_avg( &avgpnt, &pnt1, &pnt2 ); - float rad = objp->radius*0.40f; + if (sip->pre_death_exp_particles.isValid()) { + vec3d center_to_point = objp->pos - avgpnt; + vm_vec_normalize(¢er_to_point); + auto source = particle::ParticleManager::get()->createSource(sip->pre_death_exp_particles); + source->setHost(std::make_unique(objp, avgpnt)); + source->setNormal(center_to_point); + source->finishCreation(); + } else { + model_instance_local_to_global_point(&outpnt, &avgpnt, pm, pmi, pm->detail[0], &objp->orient, &objp->pos ); - rad *= sip->death_fx_r_mult; + float rad = objp->radius*0.40f; - int fireball_type = fireball_ship_explosion_type(sip); - if(fireball_type < 0) { - fireball_type = FIREBALL_EXPLOSION_MEDIUM; + rad *= sip->death_fx_r_mult; + + int fireball_type = fireball_ship_explosion_type(sip); + if(fireball_type < 0) { + fireball_type = FIREBALL_EXPLOSION_MEDIUM; + } + fireball_create( &outpnt, fireball_type, FIREBALL_MEDIUM_EXPLOSION, OBJ_INDEX(objp), rad, false, &objp->phys_info.vel ); } - fireball_create( &outpnt, fireball_type, FIREBALL_MEDIUM_EXPLOSION, OBJ_INDEX(objp), rad, false, &objp->phys_info.vel ); } } diff --git a/code/ship/ship.h b/code/ship/ship.h index 14f67ddf644..66b1320b10f 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1225,6 +1225,9 @@ class ship_info particle::ParticleEffectHandle impact_spew; particle::ParticleEffectHandle damage_spew; + particle::ParticleEffectHandle death_roll_exp_particles; + particle::ParticleEffectHandle pre_death_exp_particles; + particle::ParticleEffectHandle propagating_exp_particles; particle::ParticleEffectHandle split_particles; particle::ParticleEffectHandle knossos_end_particles; particle::ParticleEffectHandle regular_end_particles; diff --git a/code/ship/shipfx.cpp b/code/ship/shipfx.cpp index 3d030ed0779..a88de3db786 100644 --- a/code/ship/shipfx.cpp +++ b/code/ship/shipfx.cpp @@ -1865,8 +1865,9 @@ static void maybe_fireball_wipe(clip_ship* half_ship, sound_handle* handle_array vm_vec_scale(&temp, 0.1f*frand()); vm_vec_add2(&model_clip_plane_pt, &temp); - float rad = get_model_cross_section_at_z(half_ship->cur_clip_plane_pt, pm); - if (rad < 1) { + float cross_section_rad = get_model_cross_section_at_z(half_ship->cur_clip_plane_pt, pm); + float rad = cross_section_rad; + if (cross_section_rad < 1) { // changed from 0.4 & 0.6 to 0.6 & 0.9 as later 1.5 multiplier was removed rad = half_ship->parent_obj->radius * frand_range(0.6f, 0.9f); } else { @@ -1874,18 +1875,28 @@ static void maybe_fireball_wipe(clip_ship* half_ship, sound_handle* handle_array // changed from 1.4 & 1.6 to 2.1 & 2.4 as later 1.5 multiplier was removed rad *= frand_range(2.1f, 2.4f); } - + rad = MIN(rad, half_ship->parent_obj->radius); + + if (sip->propagating_exp_particles.isValid()) { + auto source = particle::ParticleManager::get()->createSource(sip->propagating_exp_particles); + auto host = std::make_unique(model_clip_plane_pt, half_ship->orient, half_ship->phys_info.vel); + // for particle effects, we'll ignore all the special logic applied to rad and just use the raw radius; the modder can handle it using curves + host->setRadius(cross_section_rad); + source->setHost(std::move(host)); + source->setNormal(half_ship->orient.vec.uvec); + source->finishCreation(); + } else { + //defaults to 1.0 now that multiplier was applied to the static values above + rad *= sip->prop_exp_rad_mult; - //defaults to 1.0 now that multiplier was applied to the static values above - rad *= sip->prop_exp_rad_mult; - - int fireball_type = fireball_ship_explosion_type(sip); - if(fireball_type < 0) { - fireball_type = FIREBALL_EXPLOSION_LARGE1 + Random::next(FIREBALL_NUM_LARGE_EXPLOSIONS); + int fireball_type = fireball_ship_explosion_type(sip); + if(fireball_type < 0) { + fireball_type = FIREBALL_EXPLOSION_LARGE1 + Random::next(FIREBALL_NUM_LARGE_EXPLOSIONS); + } + int low_res_fireballs = Bs_exp_fire_low; + fireball_create(&model_clip_plane_pt, fireball_type, FIREBALL_LARGE_EXPLOSION, OBJ_INDEX(half_ship->parent_obj), rad, false, &half_ship->parent_obj->phys_info.vel, 0.0f, -1, nullptr, low_res_fireballs); } - int low_res_fireballs = Bs_exp_fire_low; - fireball_create(&model_clip_plane_pt, fireball_type, FIREBALL_LARGE_EXPLOSION, OBJ_INDEX(half_ship->parent_obj), rad, false, &half_ship->parent_obj->phys_info.vel, 0.0f, -1, nullptr, low_res_fireballs); // start the next fireball up (3-4 per frame) + 30% int time_low, time_high; From 09b6888bc63c5f7fc6d1a797f97a2c8097049bce Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Fri, 15 Aug 2025 20:45:05 -0700 Subject: [PATCH 347/466] fix normals --- code/ship/ship.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index f4409ce9cef..b4b33628f4e 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -9424,17 +9424,16 @@ static void ship_dying_frame(object *objp, int ship_num) // Get a random point on the surface of a submodel vec3d pnt1 = submodel_get_random_point(pm->id, pm->detail[0]); + model_instance_local_to_global_point(&outpnt, &pnt1, shipp->model_instance_num, pm->detail[0], &objp->orient, &objp->pos ); if (sip->death_roll_exp_particles.isValid()) { - vec3d center_to_point = objp->pos - pnt1; + vec3d center_to_point = outpnt - objp->pos; vm_vec_normalize(¢er_to_point); auto source = particle::ParticleManager::get()->createSource(sip->death_roll_exp_particles); source->setHost(std::make_unique(objp, pnt1)); source->setNormal(center_to_point); source->finishCreation(); } else { - model_instance_local_to_global_point(&outpnt, &pnt1, shipp->model_instance_num, pm->detail[0], &objp->orient, &objp->pos ); - float rad = objp->radius*0.1f; if (sip->death_roll_r_mult != 1.0f) @@ -9543,17 +9542,16 @@ static void ship_dying_frame(object *objp, int ship_num) vec3d pnt2 = submodel_get_random_point(pm->id, pm->detail[0]); vm_vec_avg( &avgpnt, &pnt1, &pnt2 ); + model_instance_local_to_global_point(&outpnt, &avgpnt, pm, pmi, pm->detail[0], &objp->orient, &objp->pos ); if (sip->pre_death_exp_particles.isValid()) { - vec3d center_to_point = objp->pos - avgpnt; + vec3d center_to_point = outpnt - objp->pos; vm_vec_normalize(¢er_to_point); auto source = particle::ParticleManager::get()->createSource(sip->pre_death_exp_particles); source->setHost(std::make_unique(objp, avgpnt)); source->setNormal(center_to_point); source->finishCreation(); } else { - model_instance_local_to_global_point(&outpnt, &avgpnt, pm, pmi, pm->detail[0], &objp->orient, &objp->pos ); - float rad = objp->radius*0.40f; rad *= sip->death_fx_r_mult; From dde16a54c6d7b0a90703517f85fab528c607917f Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 16 Aug 2025 01:14:45 -0400 Subject: [PATCH 348/466] restore inadvertently deleted code This has an interesting history. In the original Volition source release, global damage was set to originate from the shockwave position, which would have avoided the fireball issue fixed by #6882. (Technically, though, this was only done for huge ships which would have still left the problem for smaller ships.) In c2716d4ee879e22671971d6be5a165d21bccef4b, this code was changed to run in non-HTL mode in an attempt to fix a bug. Then in 70529dbd5a2e64c188675fbec086c5e7d5fa05ea, as part of the removal of non-HTL mode, the code was entirely deleted. So, this restores the code. The #6882 fix is still valid, both because it works for non-huge ships, and because shockwaves may not necessarily originate from the subobject being destroyed. The #6882 fix also subsumes the effect of the restored code, but it's still good to restore the code in case future development may depend on it. --- code/ship/shiphit.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index a53510edf17..505912c943b 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -3080,6 +3080,11 @@ void ship_apply_global_damage(object *ship_objp, object *other_obj, const vec3d // shield_quad = quadrant facing the force_center shield_quad = get_quadrant(&local_hitpos, ship_objp); + // world_hitpos use force_center for shockwave + // Goober5000 check for NULL + if (other_obj && (other_obj->type == OBJ_SHOCKWAVE) && (Ship_info[Ships[ship_objp->instance].ship_info_index].is_huge_ship())) + world_hitpos = *force_center; + int wip_index = -1; if(other_obj != nullptr && other_obj->type == OBJ_SHOCKWAVE) wip_index = shockwave_get_weapon_index(other_obj->instance); From c535108708d8c0ae336f27817fa64fcb2fcfd9b2 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Sat, 16 Aug 2025 06:02:46 -0400 Subject: [PATCH 349/466] Allow background option to completely work (VR useful) (#6930) From discussion with VR players, it appears that being able to toggle off the skybox/starfield/planet background is very useful to some players to prevent motion sickness. Currently, FSO has the toggle in the Graphics Options window for `Planets/Backgrounds` but it only turns off planet bitmaps. This PR suggests it should be expanded into an accessibility role for players, and properly turn off all background elements--so planets, plus skybox rendering (especially now since most mods also use skyboxes as backgrounds, whereas in early days this was not primarily the case). Happy to discuss and/or add as a flag if folks think that would be needed. --- code/starfield/starfield.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/starfield/starfield.cpp b/code/starfield/starfield.cpp index 5ecae120852..3eeb33801ee 100644 --- a/code/starfield/starfield.cpp +++ b/code/starfield/starfield.cpp @@ -2305,6 +2305,10 @@ void stars_draw_background() return; } + // detail settings + if (!Detail.planets_suns) + return; + if (Nmodel_num < 0) return; From 7780769ba5a8d14b900dfcc825c2f27e55474ec9 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Sat, 16 Aug 2025 06:03:00 -0400 Subject: [PATCH 350/466] Add 'Nebula Usage Score' curve input (#6926) Background: Many mods, especially Event Horizon, have highlighted how versatile and useful using nebula are for creating a wide variety of immersive effects beyond just nebula, including weather, volcanic ash, specialty debris falling from the sky, and more. In addition volumetric nebula have opened another realm of possibilities for modders to creatively utilize. Furthermore, modders can not leverage the powerful updates to the particle system to create a wide diversity of effects, which can be also utilized within nebula (or any mission) by making weapons with those effects and then spawning those weapons with scripts/sexps. Just one creative example is exploding rock debris from a volcano (like in Event Horizon). The visuals of nebula poof though are of course spawned in density according to the `Nebula Detail Level` graphics slider in the Graphics tab of the Options menu. Thus, what the player sets will affect visuals of the nebula. As a modder creating visuals, this can have the effect where particle effects spawned from weapons used in nebula may look different in less dense fields. Ideally there could be a way for the modder to incorporate this Nebula slider detail into the development of their effects, to ensure that the effect visuals properly reflect and integrate with the nebula density that the player might see. The solution to this problem is luckily not difficult, as PR #6889 shows how it can be dealt with. That PR added a particle curve input `Particle Usage Score` which is incredibly useful to allow particle effects to scale visually according to the `Particle Detail Level` that the player has set in the Graphics tab of the Options menu. As such this utility could also be extended to the Nebula detail level to allow modders to develop weapon spawned effects for nebula missions that properly visually integrate with the nebula detail level. This PR This PR follows and extends the relatively simple approach of #6889, in that it adds a new curve input `Nebula Usage Score` for use within the particle effects table. This new input will allow modders to develop effects that can properly mesh with varying nebula detail levels--all with the aim to further expand the visuals and range of nebula utility for environmental effects. --- code/particle/ParticleEffect.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/code/particle/ParticleEffect.h b/code/particle/ParticleEffect.h index abc60fdced2..23024fe1c68 100644 --- a/code/particle/ParticleEffect.h +++ b/code/particle/ParticleEffect.h @@ -234,6 +234,10 @@ class ParticleEffect { std::pair {"Particle Usage Score", modular_curves_math_input< modular_curves_global_submember_input, modular_curves_global_submember_input, + ModularCurvesMathOperators::division>{}}, + std::pair {"Nebula Usage Score", modular_curves_math_input< + modular_curves_global_submember_input, + modular_curves_global_submember_input, ModularCurvesMathOperators::division>{}}) .derive_modular_curves_input_only_subset( //Effect Number std::pair {"Spawntime Left", modular_curves_functional_full_input<&ParticleSource::getEffectRemainingTime>{}}, From 41dc187456457f563bf33519390b5ad89bf7af3c Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 16 Aug 2025 14:50:31 -0500 Subject: [PATCH 351/466] add flag list widget --- qtfred/source_groups.cmake | 2 + qtfred/src/ui/widgets/FlagList.cpp | 249 +++++++++++++++++++++++++++++ qtfred/src/ui/widgets/FlagList.h | 83 ++++++++++ 3 files changed, 334 insertions(+) create mode 100644 qtfred/src/ui/widgets/FlagList.cpp create mode 100644 qtfred/src/ui/widgets/FlagList.h diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 0b880c884ed..fcf866d1129 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -190,6 +190,8 @@ add_file_folder("Source/UI/Util" add_file_folder("Source/UI/Widgets" src/ui/widgets/ColorComboBox.cpp src/ui/widgets/ColorComboBox.h + src/ui/widgets/FlagList.cpp + src/ui/widgets/FlagList.h src/ui/widgets/renderwidget.cpp src/ui/widgets/renderwidget.h src/ui/widgets/sexp_tree.cpp diff --git a/qtfred/src/ui/widgets/FlagList.cpp b/qtfred/src/ui/widgets/FlagList.cpp new file mode 100644 index 00000000000..152ecd22c39 --- /dev/null +++ b/qtfred/src/ui/widgets/FlagList.cpp @@ -0,0 +1,249 @@ +#include "ui/widgets/FlagList.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fso::fred { + +FlagListWidget::FlagListWidget(QWidget* parent) : QWidget(parent) +{ + buildUi(); + connectSignals(); + + setFilterVisible(true); + setToolbarVisible(true); +} + +FlagListWidget::~FlagListWidget() = default; + +void FlagListWidget::buildUi() +{ + auto* outer = new QVBoxLayout(this); + outer->setContentsMargins(0, 0, 0, 0); + outer->setSpacing(6); + + // filter row + auto* filterRow = new QHBoxLayout(); + filterRow->setContentsMargins(0, 0, 0, 0); + filterRow->setSpacing(6); + + _filter = new QLineEdit(this); + _filter->setPlaceholderText(tr("Filter flags...")); + filterRow->addWidget(_filter, /*stretch*/ 1); + + // toolbar + _btnAll = new QToolButton(this); + _btnAll->setText(tr("All")); + _btnAll->setToolTip(tr("Select all")); + + _btnNone = new QToolButton(this); + _btnNone->setText(tr("None")); + _btnNone->setToolTip(tr("Clear all")); + + filterRow->addWidget(_btnAll); + filterRow->addWidget(_btnNone); + + outer->addLayout(filterRow); + + // list view and setup + _model = new QStandardItemModel(this); + + _proxy = new QSortFilterProxyModel(this); + _proxy->setSourceModel(_model); + _proxy->setFilterCaseSensitivity(Qt::CaseInsensitive); + _proxy->setFilterRole(Qt::DisplayRole); + _proxy->setSortRole(Qt::DisplayRole); + _proxy->setDynamicSortFilter(true); + + _list = new QListView(this); + _list->setModel(_proxy); + _list->setUniformItemSizes(true); + _list->setSelectionMode(QAbstractItemView::NoSelection); + _list->setEditTriggers(QAbstractItemView::NoEditTriggers); + _list->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + + outer->addWidget(_list, /*stretch*/ 1); + + setLayout(outer); +} + +void FlagListWidget::connectSignals() +{ + // React to user toggles + connect(_model, &QStandardItemModel::itemChanged, this, &FlagListWidget::onItemChanged); + // Filter field + connect(_filter, &QLineEdit::textChanged, this, &FlagListWidget::onFilterTextChanged); + // Toolbar actions + connect(_btnAll, &QToolButton::clicked, this, &FlagListWidget::onSelectAll); + connect(_btnNone, &QToolButton::clicked, this, &FlagListWidget::onClearAll); +} + +void FlagListWidget::setFlags(const QVector>& flags) +{ + rebuildModel(flags); +} + +void FlagListWidget::setFlagDescriptions(const QVector>& descriptions) +{ + _descByName.clear(); + _descByName.reserve(descriptions.size()); + for (const auto& p : descriptions) { + _descByName.insert(p.first, p.second); + } + applyTooltipsToItems(); // apply immediately if items already exist +} + +void FlagListWidget::rebuildModel(const QVector>& flags) +{ + _updating = true; + + _model->clear(); + _model->setColumnCount(1); + + _model->setHorizontalHeaderLabels({tr("Flag")}); + + _model->insertRows(0, flags.size()); + for (int i = 0; i < flags.size(); ++i) { + const auto& name = flags[i].first; + const bool checked = flags[i].second; + + auto* item = new QStandardItem(name); + item->setCheckable(true); + item->setCheckState(checked ? Qt::Checked : Qt::Unchecked); + item->setData(name, KeyRole); + + // If we have a description for this flag, set it as tooltip + const auto it = _descByName.constFind(name); + if (it != _descByName.constEnd()) + item->setToolTip(*it); + + _model->setItem(i, 0, item); + } + + // Reapply filter text so a rebuild respects current filter + onFilterTextChanged(_filter->text()); + + _updating = false; + + Q_EMIT flagsChanged(snapshot()); +} + +void FlagListWidget::applyTooltipsToItems() +{ + // Apply descriptions to existing items (used when descriptions are set after setFlags) + for (int r = 0; r < _model->rowCount(); ++r) { + if (auto* it = _model->item(r, 0)) { + const auto key = it->data(KeyRole).toString(); + const auto dIt = _descByName.constFind(key); + it->setToolTip(dIt != _descByName.constEnd() ? *dIt : QString()); + } + } +} + +QVector> FlagListWidget::getFlags() const +{ + return snapshot(); +} + +void FlagListWidget::clear() +{ + _updating = true; + _model->clear(); + _updating = false; + Q_EMIT flagsChanged({}); +} + +void FlagListWidget::setFilterVisible(bool visible) +{ + _filterVisible = visible; + if (_filter) + _filter->setVisible(visible); +} + +bool FlagListWidget::filterVisible() const +{ + return _filterVisible; +} + +void FlagListWidget::setToolbarVisible(bool visible) +{ + _toolbarVisible = visible; + if (_btnAll) + _btnAll->setVisible(visible); + if (_btnNone) + _btnNone->setVisible(visible); +} + +bool FlagListWidget::toolbarVisible() const +{ + return _toolbarVisible; +} + +void FlagListWidget::onItemChanged(QStandardItem* item) +{ + if (_updating || !item) + return; + + const auto name = item->data(KeyRole).toString(); + const bool checked = (item->checkState() == Qt::Checked); + + Q_EMIT flagToggled(name, checked); + Q_EMIT flagsChanged(snapshot()); +} + +void FlagListWidget::onFilterTextChanged(const QString& text) +{ + // Use a simple contains filter + QRegularExpression re(QRegularExpression::escape(text), QRegularExpression::CaseInsensitiveOption); + // Convert to text to emulate substring + const QString pattern = QStringLiteral(".*%1.*").arg(re.pattern()); + _proxy->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); +} + +void FlagListWidget::onSelectAll() +{ + _updating = true; + for (int r = 0; r < _model->rowCount(); ++r) { + if (auto* it = _model->item(r, 0)) { + it->setCheckState(Qt::Checked); + } + } + _updating = false; + Q_EMIT flagsChanged(snapshot()); +} + +void FlagListWidget::onClearAll() +{ + _updating = true; + for (int r = 0; r < _model->rowCount(); ++r) { + if (auto* it = _model->item(r, 0)) { + it->setCheckState(Qt::Unchecked); + } + } + _updating = false; + Q_EMIT flagsChanged(snapshot()); +} + +QVector> FlagListWidget::snapshot() const +{ + QVector> out; + out.reserve(_model->rowCount()); + for (int r = 0; r < _model->rowCount(); ++r) { + if (auto* it = _model->item(r, 0)) { + const auto key = it->data(KeyRole).toString(); + const bool checked = (it->checkState() == Qt::Checked); + out.append({key, checked}); + } + } + return out; +} + +} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/widgets/FlagList.h b/qtfred/src/ui/widgets/FlagList.h new file mode 100644 index 00000000000..9b33c27a72d --- /dev/null +++ b/qtfred/src/ui/widgets/FlagList.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class QLineEdit; +class QToolButton; +class QListView; +class QStandardItemModel; +class QSortFilterProxyModel; +class QStandardItem; + +namespace fso::fred { + +class FlagListWidget final : public QWidget { + Q_OBJECT + Q_PROPERTY(bool filterVisible READ filterVisible WRITE setFilterVisible) + Q_PROPERTY(bool toolbarVisible READ toolbarVisible WRITE setToolbarVisible) + + public: + explicit FlagListWidget(QWidget* parent = nullptr); + ~FlagListWidget() override; + + void setFlags(const QVector>& flags); + + // Optionally set descriptions + void setFlagDescriptions(const QVector>& descriptions); + + // Read back the entire list and their checked states + QVector> getFlags() const; + + // Optional UI controls + void setFilterVisible(bool visible); + bool filterVisible() const; + + void setToolbarVisible(bool visible); + bool toolbarVisible() const; + + // Clear all items + void clear(); + + signals: + // Emitted whenever a checkbox is toggled + void flagToggled(const QString& name, bool checked); + // Emitted after any change that alters the entire set + void flagsChanged(const QVector>& flags); + + private slots: + void onItemChanged(QStandardItem* item); + void onFilterTextChanged(const QString& text); + void onSelectAll(); + void onClearAll(); + + private: + enum Roles : int { + KeyRole = Qt::UserRole + 1 + }; + + void buildUi(); + void connectSignals(); + void rebuildModel(const QVector>& flags); + void applyTooltipsToItems(); + QVector> snapshot() const; + + QLineEdit* _filter = nullptr; + QToolButton* _btnAll = nullptr; + QToolButton* _btnNone = nullptr; + QListView* _list = nullptr; + QStandardItemModel* _model = nullptr; + QSortFilterProxyModel* _proxy = nullptr; + + QHash _descByName; + + bool _updating = false; // guards against emitting signals during programmatic changes + bool _filterVisible = true; + bool _toolbarVisible = true; +}; + +} // namespace fso::fred \ No newline at end of file From dadb91889f297cacd9788d95fd2bd74964d4aa74 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 16 Aug 2025 15:10:54 -0500 Subject: [PATCH 352/466] clang --- qtfred/src/ui/widgets/FlagList.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/ui/widgets/FlagList.h b/qtfred/src/ui/widgets/FlagList.h index 9b33c27a72d..7bcce924937 100644 --- a/qtfred/src/ui/widgets/FlagList.h +++ b/qtfred/src/ui/widgets/FlagList.h @@ -55,7 +55,7 @@ class FlagListWidget final : public QWidget { void onSelectAll(); void onClearAll(); - private: + private: // NOLINT(readability-redundant-access-specifiers) enum Roles : int { KeyRole = Qt::UserRole + 1 }; From 59a362ed1d029e26563cdbee90785081923b9475 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Sat, 16 Aug 2025 22:14:51 -0400 Subject: [PATCH 353/466] fix end-mission sexp in multi (#6943) A previous attempt to fix the end-mission sexp in multi such that it wouldn't trigger the jump out sequence was horribly broken. Notable safety checks, state changes and server/client syncing was skipped over. This caused, as one particular bug, a standalone server to stop communicating with the game host. When this happened stats couldn't be accepted/tossed and you were stuck at the debrief screen, being forced to disconnect from the standalone in order to continue. A proper fix for this is to use multi sexps to tell clients to enter a debrief state before the standard end mission request is handled. This will allow it the bypass the actual warp out animation for players while still doing the normal end of mission handling. NOTE: This is a breaking, but not fatal, multi change and should be safe considering the recent version bump in early June. As such an additional version bump is not required with this for 25.0. --- code/network/multimsgs.cpp | 30 +++++++----------------------- code/network/multimsgs.h | 3 --- code/parse/sexp.cpp | 20 ++++++++++++++------ 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/code/network/multimsgs.cpp b/code/network/multimsgs.cpp index 4725ef2b05a..ef4596c5983 100644 --- a/code/network/multimsgs.cpp +++ b/code/network/multimsgs.cpp @@ -3952,22 +3952,6 @@ void process_ingame_nak(ubyte *data, header *hinfo) } } -// If the end_mission SEXP has been used tell clients to skip straight to the debrief screen -void send_force_end_mission_packet() -{ - ubyte data[MAX_PACKET_SIZE]; - int packet_size; - - packet_size = 0; - BUILD_HEADER(FORCE_MISSION_END); - - if (Net_player->flags & NETINFO_FLAG_AM_MASTER) - { - // tell everyone to leave the game - multi_io_send_to_all_reliable(data, packet_size); - } -} - // process a packet indicating that we should jump straight to the debrief screen void process_force_end_mission_packet(ubyte * /*data*/, header *hinfo) { @@ -3977,13 +3961,13 @@ void process_force_end_mission_packet(ubyte * /*data*/, header *hinfo) PACKET_SET_SIZE(); - ml_string("Receiving force end mission packet"); - - // Since only the server sends out these packets it should never receive one - Assert (!(Net_player->flags & NETINFO_FLAG_AM_MASTER)); - - multi_handle_sudden_mission_end(); - send_debrief_event(); + // TODO: Obsolete packet - Remove on next multi bump + // + // This method of ending a mission was horribly broken and skipped over a + // lot of necessary state changes resulting in broken standalone net traffic + // + // We need to support receiving this packet for compatibility sake, but it + // should be removed on the next multi bump (as noted in #6927) } // send a packet telling players to end the mission diff --git a/code/network/multimsgs.h b/code/network/multimsgs.h index 8ba0641bf1d..91852e9b6a4 100644 --- a/code/network/multimsgs.h +++ b/code/network/multimsgs.h @@ -360,9 +360,6 @@ void send_new_player_packet(int new_player_num,net_player *target); // send a packet telling players to end the mission void send_endgame_packet(net_player *pl = NULL); -// send a skip to debrief item packet -void send_force_end_mission_packet(); - // send a position/orientation update for myself (if I'm an observer) void send_observer_update_packet(); diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 7725f591e2c..503a3c19190 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -17064,12 +17064,16 @@ void sexp_end_mission(int n) send_debrief_event(); } - // Karajorma - callback all the clients here. - if (MULTIPLAYER_MASTER) - { - multi_handle_sudden_mission_end(); - send_force_end_mission_packet(); - } + Current_sexp_network_packet.do_callback(); +} + +void multi_sexp_end_mission() +{ + // This is a bit of hack, but when in a debrief state clients will skip the + // warp out sequence when the endgame packet is processed. + send_debrief_event(); + // Standard way to end mission (equivalent to Alt-J) + multi_handle_end_mission_request(); } // Goober5000 @@ -30678,6 +30682,10 @@ void multi_sexp_eval() multi_sexp_red_alert(); break; + case OP_END_MISSION: + multi_sexp_end_mission(); + break; + // bad sexp in the packet default: // probably just a version error where the host supports a SEXP but a client does not From a0730f601c3c36f69d61f5bb94eba33ce1fb0de7 Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Sun, 17 Aug 2025 15:36:38 -0700 Subject: [PATCH 354/466] add particle hook --- code/asteroid/asteroid.cpp | 56 +++++++++++++++++++++++++++++--------- code/asteroid/asteroid.h | 7 +++-- 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index abb91378d4e..55be6db2000 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -1704,37 +1704,57 @@ static void asteroid_do_area_effect(object *asteroid_objp) */ void asteroid_hit( object * pasteroid_obj, object * other_obj, vec3d * hitpos, float damage, vec3d* force ) { - float explosion_life; - asteroid *asp; - - asp = &Asteroids[pasteroid_obj->instance]; - + asteroid *asp = &Asteroids[pasteroid_obj->instance]; + asteroid_info *asip = &Asteroid_info[Asteroids[pasteroid_obj->instance].asteroid_type]; + if (pasteroid_obj->flags[Object::Object_Flags::Should_be_dead]){ return; } - + if ( MULTIPLAYER_MASTER ){ send_asteroid_hit( pasteroid_obj, other_obj, hitpos, damage, force ); } - + if (hitpos && force && The_mission.ai_profile->flags[AI::Profile_Flags::Whackable_asteroids]) { vec3d rel_hit_pos = *hitpos - pasteroid_obj->pos; physics_calculate_and_apply_whack(force, &rel_hit_pos, &pasteroid_obj->phys_info, &pasteroid_obj->orient, &pasteroid_obj->phys_info.I_body_inv); pasteroid_obj->phys_info.desired_vel = pasteroid_obj->phys_info.vel; } - + pasteroid_obj->hull_strength -= damage; - + if (pasteroid_obj->hull_strength < 0.0f) { if ( !asp->final_death_time.isValid() ) { int play_loud_collision = 0; + + float explosion_life; + int breakup_timestamp; + + Assertion(!asip->end_particles.isValid() || asip->breakup_delay.has_value(), "Asteroid %s has end particles but no breakup delay. Parsing should not have allowed this!", asip->name); + + if (asip->end_particles.isValid()) { + auto source = particle::ParticleManager::get()->createSource(asip->end_particles); + + // Use the position since the asteroid is going to be invalid soon + auto host = std::make_unique(pasteroid_obj->pos, pasteroid_obj->orient, pasteroid_obj->phys_info.vel); + host->setRadius(pasteroid_obj->radius); + source->setHost(std::move(host)); + source->setNormal(pasteroid_obj->orient.vec.uvec); + source->finishCreation(); + } else { + explosion_life = asteroid_create_explosion(pasteroid_obj); + } - explosion_life = asteroid_create_explosion(pasteroid_obj); + if (asip->breakup_delay.has_value()) { + breakup_timestamp = fl2i(*asip->breakup_delay * MILLISECONDS_PER_SECOND); + } else { + breakup_timestamp = fl2i((explosion_life * MILLISECONDS_PER_SECOND) / 5.f); + } asteroid_explode_sound(pasteroid_obj, asp->asteroid_type, play_loud_collision); asteroid_do_area_effect(pasteroid_obj); - asp->final_death_time = _timestamp( fl2i(explosion_life*MILLISECONDS_PER_SECOND)/5 ); // Wait till 30% of vclip time before breaking the asteroid up. + asp->final_death_time = _timestamp( breakup_timestamp ); // Wait till 20% of vclip time before breaking the asteroid up, or use the specified delay if ( hitpos ) { asp->death_hit_pos = *hitpos; } else { @@ -2334,10 +2354,20 @@ static void asteroid_parse_section() if(optional_string("$Explosion Animations:")){ stuff_fireball_index_list(asteroid_p->explosion_bitmap_anims, asteroid_p->name); + + if (optional_string("$Explosion Radius Mult:")) { + stuff_float(&asteroid_p->fireball_radius_multiplier); + } + } else if (optional_string("$Explosion Effect:")) { + asteroid_p->end_particles = particle::util::parseEffect(asteroid_p->name); + } + + if (optional_string("$Breakup Delay:")) { + stuff_float(&asteroid_p->breakup_delay.emplace()); } - if (optional_string("$Explosion Radius Mult:")) { - stuff_float(&asteroid_p->fireball_radius_multiplier); + if (asteroid_p->end_particles.isValid() && !asteroid_p->breakup_delay.has_value()) { + error_display(0, "Asteroid %s has an explosion effect but no breakup delay!", asteroid_p->name); } if (optional_string("$Expl inner rad:")){ diff --git a/code/asteroid/asteroid.h b/code/asteroid/asteroid.h index 36d5bd0961d..828fc8ddd14 100644 --- a/code/asteroid/asteroid.h +++ b/code/asteroid/asteroid.h @@ -16,6 +16,7 @@ #include "globalincs/pstypes.h" #include "object/object_flags.h" #include "io/timer.h" +#include "particle/ParticleEffect.h" class object; class polymodel; @@ -79,7 +80,9 @@ class asteroid_info float initial_asteroid_strength; // starting strength of asteroid SCP_vector< asteroid_split_info > split_info; SCP_vector explosion_bitmap_anims; - float fireball_radius_multiplier; // the model radius is multiplied by this to determine the fireball size + float fireball_radius_multiplier; // the model radius is multiplied by this to determine the fireball size + particle::ParticleEffectHandle end_particles; + std::optional breakup_delay; SCP_string display_name; // only used for hud targeting display and for debris float spawn_weight; // debris only, relative proportion to spawn compared to other types in its asteroid field float gravity_const; // multiplier for mission gravity @@ -90,7 +93,7 @@ class asteroid_info rotational_vel_multiplier(1), damage_type_idx(0), damage_type_idx_sav( -1 ), inner_rad( 0 ), outer_rad( 0 ), damage( 0 ), blast( 0 ), initial_asteroid_strength( 0 ), - fireball_radius_multiplier( -1 ), spawn_weight( 1 ), gravity_const( 0 ) + fireball_radius_multiplier( -1 ), end_particles( particle::ParticleEffectHandle::invalid() ), breakup_delay( std::nullopt ), spawn_weight( 1 ), gravity_const( 0 ) { name[ 0 ] = 0; display_name = ""; From 6539fc9aeb4b91a61b51d6476d7aa96cae9ccf02 Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Sun, 17 Aug 2025 16:06:32 -0700 Subject: [PATCH 355/466] clean up control flow --- code/asteroid/asteroid.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index 55be6db2000..13c14283f6a 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -2351,15 +2351,17 @@ static void asteroid_parse_section() asteroid_p->damage_type_idx_sav = damage_type_add(buf); asteroid_p->damage_type_idx = asteroid_p->damage_type_idx_sav; } - - if(optional_string("$Explosion Animations:")){ - stuff_fireball_index_list(asteroid_p->explosion_bitmap_anims, asteroid_p->name); + if (optional_string("$Explosion Effect:")) { + asteroid_p->end_particles = particle::util::parseEffect(asteroid_p->name); + } else { + if(optional_string("$Explosion Animations:")){ + stuff_fireball_index_list(asteroid_p->explosion_bitmap_anims, asteroid_p->name); + } + if (optional_string("$Explosion Radius Mult:")) { - stuff_float(&asteroid_p->fireball_radius_multiplier); + stuff_float(&asteroid_p->fireball_radius_multiplier); } - } else if (optional_string("$Explosion Effect:")) { - asteroid_p->end_particles = particle::util::parseEffect(asteroid_p->name); } if (optional_string("$Breakup Delay:")) { From 21c9b65c99e07cf778f403803c318a58f64ec2b2 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Tue, 15 Jul 2025 20:50:21 -0400 Subject: [PATCH 356/466] add _WIN32 guard to trackir/freetrack --- code/headtracking/freetrack.cpp | 4 ++++ code/headtracking/freetrack.h | 4 ++++ code/headtracking/trackir.cpp | 4 ++++ code/headtracking/trackir.h | 4 ++++ code/headtracking/trackirpublic.cpp | 4 ++++ code/headtracking/trackirpublic.h | 4 ++++ 6 files changed, 24 insertions(+) diff --git a/code/headtracking/freetrack.cpp b/code/headtracking/freetrack.cpp index e7076d15dd2..5589bb27e55 100644 --- a/code/headtracking/freetrack.cpp +++ b/code/headtracking/freetrack.cpp @@ -1,4 +1,6 @@ +#ifdef _WIN32 + #include "headtracking/freetrack.h" #define WIN32_LEAN_AND_MEAN @@ -183,3 +185,5 @@ namespace headtracking } } } + +#endif // _WIN32 diff --git a/code/headtracking/freetrack.h b/code/headtracking/freetrack.h index 74f95d9ea6d..4c0ae81bd81 100644 --- a/code/headtracking/freetrack.h +++ b/code/headtracking/freetrack.h @@ -2,6 +2,8 @@ #ifndef HEADTRACKING_FREETRACK_H #define HEADTRACKING_FREETRACK_H +#ifdef _WIN32 + #include "headtracking/headtracking.h" #include "headtracking/headtracking_internal.h" @@ -92,4 +94,6 @@ namespace headtracking } } +#endif // _WIN32 + #endif // HEADTRACKING_FREETRACK_H diff --git a/code/headtracking/trackir.cpp b/code/headtracking/trackir.cpp index 89625b2536b..fb4acc96fbd 100644 --- a/code/headtracking/trackir.cpp +++ b/code/headtracking/trackir.cpp @@ -1,4 +1,6 @@ +#ifdef _WIN32 + #include "headtracking/trackir.h" #include "headtracking/trackirpublic.h" @@ -53,3 +55,5 @@ namespace headtracking } } } + +#endif // _WIN32 diff --git a/code/headtracking/trackir.h b/code/headtracking/trackir.h index 353da7341af..de1f430259d 100644 --- a/code/headtracking/trackir.h +++ b/code/headtracking/trackir.h @@ -2,6 +2,8 @@ #ifndef HEADTRACKING_TRACKIR_H #define HEADTRACKING_TRACKIR_H +#ifdef _WIN32 + #include "headtracking/headtracking.h" #include "headtracking/headtracking_internal.h" #include "headtracking/trackirpublic.h" @@ -24,4 +26,6 @@ namespace headtracking } } +#endif // _WIN32 + #endif // HEADTRACKING_TRACKIR_H diff --git a/code/headtracking/trackirpublic.cpp b/code/headtracking/trackirpublic.cpp index 14c621b8adc..499ef582889 100644 --- a/code/headtracking/trackirpublic.cpp +++ b/code/headtracking/trackirpublic.cpp @@ -1,4 +1,6 @@ +#ifdef _WIN32 + #include "headtracking/trackirpublic.h" TrackIRDLL::TrackIRDLL() @@ -126,3 +128,5 @@ float TrackIRDLL::GetYaw() const return m_GetYaw(); return 0.0f; } + +#endif // _WIN32 diff --git a/code/headtracking/trackirpublic.h b/code/headtracking/trackirpublic.h index 78355b7eebd..b44ae81edc3 100644 --- a/code/headtracking/trackirpublic.h +++ b/code/headtracking/trackirpublic.h @@ -1,6 +1,8 @@ #ifndef TRACKIRPUBLIC_H_INCLUDED_ #define TRACKIRPUBLIC_H_INCLUDED_ +#ifdef _WIN32 + #include "external_dll/externalcode.h" #include "globalincs/pstypes.h" #include "osapi/osapi.h" @@ -82,4 +84,6 @@ class TrackIRDLL : public SCP_ExternalCode bool m_enabled; }; +#endif // _WIN32 + #endif /* TRACKIRPUBLIC_H_INCLUDED_ */ From 908f30698596f1da722acf63c681f6dc86ab4d02 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Sat, 19 Jul 2025 19:16:22 -0400 Subject: [PATCH 357/466] fix some abs() type conversion warnings --- code/math/ik_solver.cpp | 2 +- code/model/modelread.cpp | 4 ++-- code/physics/physics.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/code/math/ik_solver.cpp b/code/math/ik_solver.cpp index fa0e62ff4de..a83da4a575a 100644 --- a/code/math/ik_solver.cpp +++ b/code/math/ik_solver.cpp @@ -204,7 +204,7 @@ bool ik_constraint_window::constrain(matrix& localRot, bool /*backwardsPass*/) c //Clamp absolute value of individual angles to window for (float angles::* i : pbh) { - const float absAngle = abs(currentAngles.*i); + const float absAngle = std::abs(currentAngles.*i); if(absAngle > absLimit.*i){ needsClamp = true; currentAngles.*i = copysignf(std::min(absAngle, absLimit.*i), currentAngles.*i); diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 9aaafc09c00..1e264477b64 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -1065,7 +1065,7 @@ float get_submodel_delta_angle(const submodel_instance *smi) float get_submodel_delta_shift(const submodel_instance *smi) { // this is a bit simpler - return abs(smi->cur_offset - smi->prev_offset); + return std::abs(smi->cur_offset - smi->prev_offset); } void do_new_subsystem( int n_subsystems, model_subsystem *slist, int subobj_num, float rad, const vec3d *pnt, char *props, const char *subobj_name, int model_num ) @@ -4388,7 +4388,7 @@ void submodel_look_at(polymodel *pm, polymodel_instance *pmi, int submodel_num) // calculate turn rate // (try to avoid a one-frame dramatic spike in the turn rate if the angle passes 0.0 or PI2) - if (abs(smi->cur_angle - smi->prev_angle) < PI) + if (std::abs(smi->cur_angle - smi->prev_angle) < PI) smi->current_turn_rate = smi->desired_turn_rate = (smi->cur_angle - smi->prev_angle) / flFrametime; // and now set the other submodel fields diff --git a/code/physics/physics.cpp b/code/physics/physics.cpp index 6ef9e8db76a..639313f7ea1 100644 --- a/code/physics/physics.cpp +++ b/code/physics/physics.cpp @@ -1249,7 +1249,7 @@ bool physics_lead_ballistic_trajectory(const vec3d* start, const vec3d* end_pos, time = range / (weapon_speed * cosf(angle)); - if (abs(time - best_guess_time) < 0.01f) + if (std::abs(time - best_guess_time) < 0.01f) break; else best_guess_time = time; From fc7389f95a3626e501d9215bf8775785ddc67292 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 18 Aug 2025 11:59:22 -0400 Subject: [PATCH 358/466] fix various compiler warnings --- code/cfile/cfile.cpp | 2 +- code/cfile/cfilecompression.cpp | 2 +- code/debugconsole/consoleparse.cpp | 4 ++-- code/inetfile/chttpget.cpp | 4 ++-- code/missionui/missionweaponchoice.cpp | 4 ++-- code/network/chat_api.cpp | 2 +- code/network/multiutil.cpp | 6 +++--- code/network/stand_gui-unix.cpp | 12 ++++-------- code/parse/sexp.cpp | 5 +++-- code/windows_stub/stubs.cpp | 4 ++-- 10 files changed, 21 insertions(+), 24 deletions(-) diff --git a/code/cfile/cfile.cpp b/code/cfile/cfile.cpp index 8692cd60f5f..0fd43a137f5 100644 --- a/code/cfile/cfile.cpp +++ b/code/cfile/cfile.cpp @@ -902,7 +902,7 @@ static CFILE *cf_open_fill_cfblock(const char* source, int line, const char* ori cfp->source_file = source; cfp->line_num = line; - int pos = ftell(fp); + auto pos = ftell(fp); if(pos == -1L) pos = 0; cf_init_lowlevel_read_code(cfp,0,filelength(fileno(fp)), 0 ); diff --git a/code/cfile/cfilecompression.cpp b/code/cfile/cfilecompression.cpp index 97b1002f433..10e34e769a3 100644 --- a/code/cfile/cfilecompression.cpp +++ b/code/cfile/cfilecompression.cpp @@ -143,7 +143,7 @@ void lz41_load_offsets(CFILE* cf) int* offsets_ptr = cf->compression_info.offsets; /* Seek to the first offset position, remember to consider the trailing ints */ - fso_fseek(cf, ( ( sizeof(int) * cf->compression_info.num_offsets ) * -1 ) - (sizeof(int)*3 ), SEEK_END); + fso_fseek(cf, static_cast( ( sizeof(int) * cf->compression_info.num_offsets ) * -1 ) - (sizeof(int)*3 ), SEEK_END); for (block = 0; block < cf->compression_info.num_offsets; ++block) { auto bytes_read = fread(offsets_ptr++, sizeof(int), 1, cf->fp); diff --git a/code/debugconsole/consoleparse.cpp b/code/debugconsole/consoleparse.cpp index 8e18e0ce1de..103c9ebf024 100644 --- a/code/debugconsole/consoleparse.cpp +++ b/code/debugconsole/consoleparse.cpp @@ -1083,7 +1083,7 @@ void dc_stuff_int(int *i) value_l = dc_parse_long(token.c_str(), DCT_INT); if ((value_l < INT_MAX) && (value_l > INT_MIN)) { - *i = value_l; + *i = static_cast(value_l); } else { throw errParse(token.c_str(), DCT_INT); @@ -1101,7 +1101,7 @@ void dc_stuff_uint(uint *i) value_l = dc_parse_long(Cp, DCT_INT); if (value_l < UINT_MAX) { - *i = value_l; + *i = static_cast(value_l); } else { throw errParse(token.c_str(), DCT_INT); diff --git a/code/inetfile/chttpget.cpp b/code/inetfile/chttpget.cpp index 3e8605b4473..256cd5ec0e8 100644 --- a/code/inetfile/chttpget.cpp +++ b/code/inetfile/chttpget.cpp @@ -392,7 +392,7 @@ int ChttpGet::ConnectSocket() char *ChttpGet::GetHTTPLine() { - int iBytesRead; + long iBytesRead; char chunk[2]; uint igotcrlf = 0; memset(recv_buffer,0,1000); @@ -463,7 +463,7 @@ char *ChttpGet::GetHTTPLine() uint ChttpGet::ReadDataChannel() { char sDataBuffer[4096]; // Data-storage buffer for the data channel - int nBytesRecv = 0; // Bytes received from the data channel + long nBytesRecv = 0; // Bytes received from the data channel fd_set wfds; diff --git a/code/missionui/missionweaponchoice.cpp b/code/missionui/missionweaponchoice.cpp index 6eb71f17dae..a2d35cb603b 100644 --- a/code/missionui/missionweaponchoice.cpp +++ b/code/missionui/missionweaponchoice.cpp @@ -941,7 +941,7 @@ void draw_3d_overhead_view(int model_num, gr_curve(lineendx, lineendy, 5, curve, resize_mode); if (curve == 0 || curve == 1) { - lineendy = bank_coords[x][1] + lround(bank_y_offset * 1.5); + lineendy = bank_coords[x][1] + static_cast(lround(bank_y_offset * 1.5)); } else { lineendy = bank_coords[x][1] + (bank_y_offset / 2); } @@ -1015,7 +1015,7 @@ void draw_3d_overhead_view(int model_num, if (curve == 1 || curve == 2) { lineendy = bank_coords[x + MAX_SHIP_PRIMARY_BANKS][1] + (bank_y_offset / 2); } else { - lineendy = bank_coords[x + MAX_SHIP_PRIMARY_BANKS][1] + lround(bank_y_offset * 1.5); + lineendy = bank_coords[x + MAX_SHIP_PRIMARY_BANKS][1] + static_cast(lround(bank_y_offset * 1.5)); } gr_line(xc, lineendy, xc, yc, resize_mode); diff --git a/code/network/chat_api.cpp b/code/network/chat_api.cpp index d6859f2ca54..55d96c23602 100644 --- a/code/network/chat_api.cpp +++ b/code/network/chat_api.cpp @@ -473,7 +473,7 @@ char *ChatGetString(void) struct timeval timeout; char ch[2]; char *p; - int bytesread; + long bytesread; static char return_string[MAXCHATBUFFER]; timeout.tv_sec=0; diff --git a/code/network/multiutil.cpp b/code/network/multiutil.cpp index 90b8a2edac0..d9deec24046 100644 --- a/code/network/multiutil.cpp +++ b/code/network/multiutil.cpp @@ -3310,7 +3310,7 @@ void bitbuffer_put( bitbuffer *bitbuf, uint data, int bit_count ) { uint mask; - mask = 1L << ( bit_count - 1 ); + mask = 1U << ( bit_count - 1 ); while ( mask != 0) { if ( mask & data ) { bitbuf->rack |= bitbuf->mask; @@ -3330,7 +3330,7 @@ uint bitbuffer_get_unsigned( bitbuffer *bitbuf, int bit_count ) uint local_mask; uint return_value; - local_mask = 1L << ( bit_count - 1 ); + local_mask = 1U << ( bit_count - 1 ); return_value = 0; while ( local_mask != 0) { @@ -3355,7 +3355,7 @@ int bitbuffer_get_signed( bitbuffer *bitbuf, int bit_count ) uint local_mask; uint return_value; - local_mask = 1L << ( bit_count - 1 ); + local_mask = 1U << ( bit_count - 1 ); return_value = 0; while ( local_mask != 0) { if ( bitbuf->mask == 0x80 ) { diff --git a/code/network/stand_gui-unix.cpp b/code/network/stand_gui-unix.cpp index 4ceddb3f8a6..b6e86768f26 100644 --- a/code/network/stand_gui-unix.cpp +++ b/code/network/stand_gui-unix.cpp @@ -131,18 +131,14 @@ class KickPlayerCommand: public WebapiCommand { } void execute() override { - size_t foundPlayerIndex = MAX_PLAYERS; - for (size_t idx = 0; idx < MAX_PLAYERS; idx++) { + for (int idx = 0; idx < MAX_PLAYERS; idx++) { if (MULTI_CONNECTED(Net_players[idx])) { if (Net_players[idx].player_id == mPlayerId) { - foundPlayerIndex = idx; + multi_kick_player(idx, 0); + break; } } } - - if (foundPlayerIndex < MAX_PLAYERS) { - multi_kick_player(foundPlayerIndex, 0); - } } private: int mPlayerId; @@ -622,7 +618,7 @@ static bool webserverApiRequest(mg_connection *conn, const mg_request_info *ri) std::string basicAuthValue = "Basic "; - basicAuthValue += base64_encode(reinterpret_cast(userNameAndPassword.c_str()), userNameAndPassword.length()); + basicAuthValue += base64_encode(reinterpret_cast(userNameAndPassword.c_str()), static_cast(userNameAndPassword.length())); const char* authValue = mg_get_header(conn, "Authorization"); if (authValue == NULL || strcmp(authValue, basicAuthValue.c_str()) != 0) { diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 503a3c19190..0ff66ee3896 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -27633,8 +27633,9 @@ void maybe_write_to_event_log(int result) { char buffer [256]; - int mask = generate_event_log_flags_mask(result); - sprintf(buffer, "Event: %s at mission time %d seconds (%d milliseconds)", Mission_events[Event_index].name.c_str(), f2i(Missiontime), f2i((longlong)Missiontime * MILLISECONDS_PER_SECOND)); + int mask = generate_event_log_flags_mask(result); + int secs = f2i(Missiontime); + sprintf(buffer, "Event: %s at mission time %d seconds (%d milliseconds)", Mission_events[Event_index].name.c_str(), secs, secs * MILLISECONDS_PER_SECOND); Current_event_log_buffer->push_back(buffer); if (!Snapshot_all_events && (!(mask &= Mission_events[Event_index].mission_log_flags))) { diff --git a/code/windows_stub/stubs.cpp b/code/windows_stub/stubs.cpp index 849a4b8060b..1f45959c7e1 100644 --- a/code/windows_stub/stubs.cpp +++ b/code/windows_stub/stubs.cpp @@ -43,7 +43,7 @@ int filelength(int fd) if (fstat (fd, &buf) == -1) return -1; - return buf.st_size; + return static_cast(buf.st_size); } @@ -199,7 +199,7 @@ void _splitpath (const char *path, char * /*drive*/, char *dir, char *fname, cha lp = ls + strlen(ls); // move to the end } - int dist = lp-ls; + auto dist = lp-ls; if (dist > (_MAX_FNAME-1)) dist = _MAX_FNAME-1; From d9b7710f92eb804b5f0a1d6d662d2f2b254b3a0a Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 18 Aug 2025 12:00:40 -0400 Subject: [PATCH 359/466] properly set some macOS bundle properties --- freespace2/resources.cmake | 2 ++ freespace2/resources/mac/Info.plist.in | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freespace2/resources.cmake b/freespace2/resources.cmake index db38c4dc942..ddf00309dc6 100644 --- a/freespace2/resources.cmake +++ b/freespace2/resources.cmake @@ -55,6 +55,8 @@ elseif(PLATFORM_MAC) set_target_properties(Freespace2 PROPERTIES MACOSX_BUNDLE_LONG_VERSION_STRING "${FSO_FULL_VERSION_STRING}") set_target_properties(Freespace2 PROPERTIES MACOSX_BUNDLE_SHORT_VERSION_STRING "${FSO_PRODUCT_VERSION_STRING}") set_target_properties(Freespace2 PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "FreeSpace Open") + set_target_properties(Freespace2 PROPERTIES MACOSX_DEPLOYMENT_TARGET "${CMAKE_OSX_DEPLOYMENT_TARGET}") + set_target_properties(Freespace2 PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER "us.indiegames.scp.FreeSpaceOpen") # Copy everything from the Resources directory add_custom_command(TARGET Freespace2 POST_BUILD diff --git a/freespace2/resources/mac/Info.plist.in b/freespace2/resources/mac/Info.plist.in index 4fa1efe0694..44d3c501e96 100644 --- a/freespace2/resources/mac/Info.plist.in +++ b/freespace2/resources/mac/Info.plist.in @@ -17,7 +17,7 @@ CFBundleName FreeSpace Open CFBundleIdentifier - us.indiegames.scp.FreeSpaceOpen + ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundlePackageType APPL CFBundleSignature From af833f059cc0cfc925792e019f6c2b754c6d53c1 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Sun, 17 Aug 2025 17:09:41 -0400 Subject: [PATCH 360/466] fix avcodec deprecation warnings --- code/cutscene/ffmpeg/internal.cpp | 9 ++++++--- code/sound/ffmpeg/FFmpegWaveFile.cpp | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/code/cutscene/ffmpeg/internal.cpp b/code/cutscene/ffmpeg/internal.cpp index be0920a5068..4f087d10191 100644 --- a/code/cutscene/ffmpeg/internal.cpp +++ b/code/cutscene/ffmpeg/internal.cpp @@ -12,9 +12,10 @@ DecoderStatus::~DecoderStatus() { videoCodec = nullptr; if (videoCodecCtx != nullptr) { - avcodec_close(videoCodecCtx); #if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255) avcodec_free_context(&videoCodecCtx); +#else + avcodec_close(videoCodecCtx); #endif videoCodecCtx = nullptr; } @@ -24,9 +25,10 @@ DecoderStatus::~DecoderStatus() { audioCodec = nullptr; if (audioCodecCtx != nullptr) { - avcodec_close(audioCodecCtx); #if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255) avcodec_free_context(&audioCodecCtx); +#else + avcodec_close(audioCodecCtx); #endif audioCodecCtx = nullptr; } @@ -36,9 +38,10 @@ DecoderStatus::~DecoderStatus() { subtitleCodec = nullptr; if (subtitleCodecCtx != nullptr) { - avcodec_close(subtitleCodecCtx); #if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255) avcodec_free_context(&subtitleCodecCtx); +#else + avcodec_close(subtitleCodecCtx); #endif subtitleCodecCtx = nullptr; } diff --git a/code/sound/ffmpeg/FFmpegWaveFile.cpp b/code/sound/ffmpeg/FFmpegWaveFile.cpp index ab1e0ddc520..77541d7b826 100644 --- a/code/sound/ffmpeg/FFmpegWaveFile.cpp +++ b/code/sound/ffmpeg/FFmpegWaveFile.cpp @@ -151,9 +151,10 @@ FFmpegWaveFile::~FFmpegWaveFile() av_frame_free(&m_decodeFrame); if (m_audioCodecCtx) { - avcodec_close(m_audioCodecCtx); #if LIBAVCODEC_VERSION_INT > AV_VERSION_INT(57, 24, 255) avcodec_free_context(&m_audioCodecCtx); +#else + avcodec_close(m_audioCodecCtx); #endif m_audioCodecCtx = nullptr; } From 4e75114479144fbcf1d99d8e1a89935847a3390b Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Mon, 18 Aug 2025 13:45:17 -0700 Subject: [PATCH 361/466] Fix parented line sprite particles appearing in the wrong place (#6950) * account for parenting * fix p1 position --- code/particle/particle.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/code/particle/particle.cpp b/code/particle/particle.cpp index 0a91a0e273c..471c937e3eb 100644 --- a/code/particle/particle.cpp +++ b/code/particle/particle.cpp @@ -428,12 +428,15 @@ namespace particle } if (part->length != 0.0f) { - vec3d p0 = part->pos; + vec3d p0 = p_pos; vec3d p1; vm_vec_copy_normalize_safe(&p1, &part->velocity); + if (part->attached_objnum >= 0) { + vm_vec_unrotate(&p1, &p1, &Objects[part->attached_objnum].orient); + } p1 *= part->length; - p1 += part->pos; + p1 += p_pos; batching_add_laser(framenum + cur_frame, &p0, radius, &p1, radius); } From 1142e7d9bc6791d2862eb68729bdb42e4072287d Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 18 Aug 2025 21:48:24 -0400 Subject: [PATCH 362/466] force proper ordering of joystick options (#6952) --- code/io/joy-sdl.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/io/joy-sdl.cpp b/code/io/joy-sdl.cpp index 4d3444eca64..3d653550e70 100644 --- a/code/io/joy-sdl.cpp +++ b/code/io/joy-sdl.cpp @@ -286,7 +286,7 @@ auto JoystickOption = options::OptionBuilder("Input.Joystick", .level(options::ExpertLevel::Beginner) .default_val(nullptr) // initial/default value for this option .flags({options::OptionFlags::ForceMultiValueSelection}) - .importance(3) + .importance(100) .change_listener([](Joystick* joy, bool) { setPlayerJoystick(joy, CID_JOY0); return true; @@ -304,7 +304,7 @@ auto JoystickOption1 = options::OptionBuilder("Input.Joystick1", .level(options::ExpertLevel::Beginner) .default_val(nullptr) .flags({ options::OptionFlags::ForceMultiValueSelection }) - .importance(3) + .importance(90) .change_listener([](Joystick* joy, bool) { setPlayerJoystick(joy, CID_JOY1); return true; @@ -322,7 +322,7 @@ auto JoystickOption2 = options::OptionBuilder("Input.Joystick2", .level(options::ExpertLevel::Beginner) .default_val(nullptr) .flags({ options::OptionFlags::ForceMultiValueSelection }) - .importance(3) + .importance(80) .change_listener([](Joystick* joy, bool) { setPlayerJoystick(joy, CID_JOY2); return true; @@ -340,7 +340,7 @@ auto JoystickOption3 = options::OptionBuilder("Input.Joystick3", .level(options::ExpertLevel::Beginner) .default_val(nullptr) .flags({ options::OptionFlags::ForceMultiValueSelection }) - .importance(3) + .importance(70) .change_listener([](Joystick* joy, bool) { setPlayerJoystick(joy, CID_JOY3); return true; From b16854796a1ba8bb560a02b272ace2df5ed7730a Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 18 Aug 2025 21:48:51 -0400 Subject: [PATCH 363/466] fix custom mouse cursors (#6953) UI elements with custom mouse cursors wouldn't show them due to the cursor being reset at the start of each frame. To fix this we need to be sure that we only reset when the element is no longer active. --- code/ui/button.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/ui/button.cpp b/code/ui/button.cpp index 76d8b1856af..4c164eedab5 100644 --- a/code/ui/button.cpp +++ b/code/ui/button.cpp @@ -445,8 +445,8 @@ void UI_BUTTON::maybe_show_custom_cursor() void UI_BUTTON::restore_previous_cursor() { - if (previous_cursor != NULL) { + if (previous_cursor != nullptr && !is_mouse_on()) { io::mouse::CursorManager::get()->setCurrentCursor(previous_cursor); - previous_cursor = NULL; + previous_cursor = nullptr; } } From dd71838715b95f24580a4b347045290acb823d37 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 18 Aug 2025 21:49:35 -0400 Subject: [PATCH 364/466] fix array index bug in ship_get_subsys_index (#6954) --- code/ship/ship.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 6c2de5fc551..ab841b5f3c1 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -15574,6 +15574,9 @@ int ship_get_subsys_index(const ship_subsys *subsys) if (subsys == nullptr) return -1; + if (subsys->parent_objnum < 0) + return -1; + // might need to refresh the cache auto sp = &Ships[Objects[subsys->parent_objnum].instance]; if (!sp->flags[Ship::Ship_Flags::Subsystem_cache_valid]) From 5ea4a4c01430351608be82dfae83e0737abbccc8 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 18 Aug 2025 21:50:15 -0400 Subject: [PATCH 365/466] remove extra config bind on joystick axes (#6955) Fixes fail sound due to the control set not changing between calls. --- code/controlconfig/controlsconfig.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/code/controlconfig/controlsconfig.cpp b/code/controlconfig/controlsconfig.cpp index 6061c27c383..7635c68f988 100644 --- a/code/controlconfig/controlsconfig.cpp +++ b/code/controlconfig/controlsconfig.cpp @@ -2085,7 +2085,6 @@ int control_config_bind_key_on_frame(int ctrl, selItem item, bool API_Access) if (!done && bind) { if (!Axis_override.empty()) { - control_config_bind(ctrl, Axis_override, item, API_Access); control_config_bind(ctrl, Axis_override, item, API_Access); done = true; strcpy_s(bound_string, Axis_override.textify().c_str()); From 836d7d4f2d206f00845cc90271b399bb3eff3cca Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Mon, 18 Aug 2025 19:02:07 -0700 Subject: [PATCH 366/466] switch include --- code/asteroid/asteroid.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/asteroid/asteroid.h b/code/asteroid/asteroid.h index 828fc8ddd14..4c1789fdd72 100644 --- a/code/asteroid/asteroid.h +++ b/code/asteroid/asteroid.h @@ -16,7 +16,7 @@ #include "globalincs/pstypes.h" #include "object/object_flags.h" #include "io/timer.h" -#include "particle/ParticleEffect.h" +#include "particle/ParticleSource.h" class object; class polymodel; From bd9ea61e9dcbf08b9879e1d606be3b4a25eb5fa0 Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Mon, 18 Aug 2025 19:17:18 -0700 Subject: [PATCH 367/466] appease clang --- code/asteroid/asteroid.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index 13c14283f6a..a33cdd703e9 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -1727,7 +1727,7 @@ void asteroid_hit( object * pasteroid_obj, object * other_obj, vec3d * hitpos, f if ( !asp->final_death_time.isValid() ) { int play_loud_collision = 0; - float explosion_life; + float explosion_life = 1.f; int breakup_timestamp; Assertion(!asip->end_particles.isValid() || asip->breakup_delay.has_value(), "Asteroid %s has end particles but no breakup delay. Parsing should not have allowed this!", asip->name); From 0bf632ceae46cbbe5e08fb6953102ae78bb885f2 Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Mon, 18 Aug 2025 19:36:17 -0700 Subject: [PATCH 368/466] Add apparent size input for particle source curve (#6947) * functional method * add input --- code/particle/ParticleEffect.cpp | 2 +- code/particle/ParticleEffect.h | 9 +++++++-- code/particle/ParticleSource.cpp | 8 ++++++-- code/particle/ParticleSource.h | 4 +++- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/code/particle/ParticleEffect.cpp b/code/particle/ParticleEffect.cpp index 6e7d84a5014..e1d61391def 100644 --- a/code/particle/ParticleEffect.cpp +++ b/code/particle/ParticleEffect.cpp @@ -124,7 +124,7 @@ ParticleEffect::ParticleEffect(SCP_string name, m_particleChance(particleChance), m_distanceCulled(distanceCulled) {} -float ParticleEffect::getApproximateVisualSize(const vec3d& pos) const { +float ParticleEffect::getApproximatePixelSize(const vec3d& pos) const { float distance_to_eye = vm_vec_dist(&Eye_position, &pos); return convert_distance_and_diameter_to_pixel_size( diff --git a/code/particle/ParticleEffect.h b/code/particle/ParticleEffect.h index 23024fe1c68..39b4be8da8a 100644 --- a/code/particle/ParticleEffect.h +++ b/code/particle/ParticleEffect.h @@ -9,6 +9,7 @@ #include "utils/RandomRange.h" #include "utils/id.h" #include "utils/modular_curves.h" +#include "graphics/2d.h" #include @@ -201,7 +202,7 @@ class ParticleEffect { bool isOnetime() const { return m_duration == Duration::ONETIME; } - float getApproximateVisualSize(const vec3d& pos) const; + float getApproximatePixelSize(const vec3d& pos) const; constexpr static auto modular_curves_definition = make_modular_curve_definition( std::array { @@ -243,7 +244,11 @@ class ParticleEffect { std::pair {"Spawntime Left", modular_curves_functional_full_input<&ParticleSource::getEffectRemainingTime>{}}, std::pair {"Time Running", modular_curves_functional_full_input<&ParticleSource::getEffectRunningTime>{}}) .derive_modular_curves_input_only_subset( //Sampled spawn position - std::pair {"Apparent Visual Size At Emitter", modular_curves_functional_full_input<&ParticleSource::getEffectVisualSize>{}} + std::pair {"Pixel Size At Emitter", modular_curves_functional_full_input<&ParticleSource::getEffectPixelSize>{}}, + std::pair {"Apparent Size At Emitter", modular_curves_math_input< + modular_curves_functional_full_input<&ParticleSource::getEffectPixelSize>, + modular_curves_global_submember_input, + ModularCurvesMathOperators::division>{}} ); MODULAR_CURVE_SET(m_modular_curves, modular_curves_definition); diff --git a/code/particle/ParticleSource.cpp b/code/particle/ParticleSource.cpp index ac98171317e..a7d337337a3 100644 --- a/code/particle/ParticleSource.cpp +++ b/code/particle/ParticleSource.cpp @@ -110,7 +110,11 @@ float ParticleSource::getEffectRunningTime(const std::tuple& source) { - return std::get<0>(source).getEffect()[std::get<1>(source)].getApproximateVisualSize(std::get<2>(source)); +float ParticleSource::getEffectPixelSize(const std::tuple& source) { + return std::get<0>(source).getEffect()[std::get<1>(source)].getApproximatePixelSize(std::get<2>(source)); +} + +float ParticleSource::getEffectApparentSize(const std::tuple& source) { + return i2fl(std::get<0>(source).getEffect()[std::get<1>(source)].getApproximatePixelSize(std::get<2>(source))) / i2fl(gr_screen.max_w); } } diff --git a/code/particle/ParticleSource.h b/code/particle/ParticleSource.h index cde17a1dcca..5422950152d 100644 --- a/code/particle/ParticleSource.h +++ b/code/particle/ParticleSource.h @@ -80,7 +80,9 @@ class ParticleSource { static float getEffectRunningTime(const std::tuple& source); - static float getEffectVisualSize(const std::tuple& source); + static float getEffectPixelSize(const std::tuple& source); + + static float getEffectApparentSize(const std::tuple& source); public: ParticleSource(); From e5fccc0b197263d52a07593a22a470623e390e49 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Tue, 19 Aug 2025 12:12:55 -0400 Subject: [PATCH 369/466] fix conversion of Missiontime to milliseconds (#6957) Broken in #6948 but the original didn't work properly either due to conversion to int32_t and the resulting size limit. Big assist from Goober5000 for noticing the bug and helping to sort out the wonky values. --- code/parse/sexp.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 0ff66ee3896..8049687a894 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -27634,8 +27634,7 @@ void maybe_write_to_event_log(int result) char buffer [256]; int mask = generate_event_log_flags_mask(result); - int secs = f2i(Missiontime); - sprintf(buffer, "Event: %s at mission time %d seconds (%d milliseconds)", Mission_events[Event_index].name.c_str(), secs, secs * MILLISECONDS_PER_SECOND); + sprintf(buffer, "Event: %s at mission time %d seconds (%d milliseconds)", Mission_events[Event_index].name.c_str(), f2i(Missiontime), static_cast(f2fl(Missiontime) * MILLISECONDS_PER_SECOND)); Current_event_log_buffer->push_back(buffer); if (!Snapshot_all_events && (!(mask &= Mission_events[Event_index].mission_log_flags))) { From 806bb7729849dd1d3884371afff8f2f48351104a Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Wed, 20 Aug 2025 06:49:00 -0700 Subject: [PATCH 370/466] Particle effect hooks for subsystem debris trails and debris explosions (#6944) * add hooks * parsing * add particle hooks * appease clang * readability --- code/debris/debris.cpp | 55 ++++++++++++++++++++++++++++++++++++------ code/model/model.h | 4 +++ code/ship/ship.cpp | 51 +++++++++++++++++++++++++++++++++++++++ code/ship/ship.h | 5 ++++ code/ship/shipfx.cpp | 6 ++--- code/ship/shipfx.h | 2 +- 6 files changed, 112 insertions(+), 11 deletions(-) diff --git a/code/debris/debris.cpp b/code/debris/debris.cpp index d3c883bb546..f458137b69f 100644 --- a/code/debris/debris.cpp +++ b/code/debris/debris.cpp @@ -66,24 +66,47 @@ debris_electrical_arc *debris_find_or_create_electrical_arc_slot(debris *db, boo */ static void debris_start_death_roll(object *debris_obj, debris *debris_p, vec3d *hitpos = nullptr) { + auto sip = &Ship_info[debris_p->ship_info_index]; if (debris_p->is_hull) { // tell everyone else to blow up the piece of debris if( MULTIPLAYER_MASTER ) send_debris_update_packet(debris_obj,DEBRIS_UPDATE_NUKE); - int fireball_type = fireball_ship_explosion_type(&Ship_info[debris_p->ship_info_index]); - if(fireball_type < 0) { - fireball_type = FIREBALL_EXPLOSION_LARGE1 + Random::next(FIREBALL_NUM_LARGE_EXPLOSIONS); + if (sip->debris_end_particles.isValid()) { + auto source = particle::ParticleManager::get()->createSource(sip->debris_end_particles); + + // Use the position since the object is going to be invalid soon + auto host = std::make_unique(debris_obj->pos, debris_obj->orient, debris_obj->phys_info.vel); + host->setRadius(debris_obj->radius); + source->setHost(std::move(host)); + source->setNormal(debris_obj->orient.vec.uvec); + source->finishCreation(); + } else { + int fireball_type = fireball_ship_explosion_type(sip); + if(fireball_type < 0) { + fireball_type = FIREBALL_EXPLOSION_LARGE1 + Random::next(FIREBALL_NUM_LARGE_EXPLOSIONS); + } + fireball_create( &debris_obj->pos, fireball_type, FIREBALL_LARGE_EXPLOSION, OBJ_INDEX(debris_obj), debris_obj->radius*1.75f); } - fireball_create( &debris_obj->pos, fireball_type, FIREBALL_LARGE_EXPLOSION, OBJ_INDEX(debris_obj), debris_obj->radius*1.75f); // only play debris destroy sound if hull piece and it has been around for at least 2 seconds if ( Missiontime > debris_p->time_started + 2*F1_0 ) { - auto snd_id = Ship_info[debris_p->ship_info_index].debris_explosion_sound; + auto snd_id = sip->debris_explosion_sound; if (snd_id.isValid()) { snd_play_3d( gamesnd_get_game_sound(snd_id), &debris_obj->pos, &View_position, debris_obj->radius ); } } + } else { + if (sip->shrapnel_end_particles.isValid()) { + auto source = particle::ParticleManager::get()->createSource(sip->shrapnel_end_particles); + + // Use the position since the object is going to be invalid soon + auto host = std::make_unique(debris_obj->pos, debris_obj->orient, debris_obj->phys_info.vel); + host->setRadius(debris_obj->radius); + source->setHost(std::move(host)); + source->setNormal(debris_obj->orient.vec.uvec); + source->finishCreation(); + } } if (scripting::hooks::OnDebrisDeath->isActive()) { @@ -427,8 +450,26 @@ object *debris_create(object *source_obj, int model_num, int submodel_num, const debris_create_set_velocity(&Debris[obj->instance], shipp, exp_center, exp_force, source_subsys); debris_create_fire_hook(obj, source_obj); const auto& sip = Ship_info[Ships[source_obj->instance].ship_info_index]; - if (sip.debris_flame_particles.isValid()) { - auto source = particle::ParticleManager::get()->createSource(sip.debris_flame_particles); + particle::ParticleEffectHandle flame_effect; + if (source_subsys != nullptr) { + if (hull_flag) { + if (source_subsys->system_info->debris_flame_particles.isValid()) { + flame_effect = source_subsys->system_info->debris_flame_particles; + } else { + flame_effect = sip.default_subsys_debris_flame_particles; + } + } else { + if (source_subsys->system_info->shrapnel_flame_particles.isValid()) { + flame_effect = source_subsys->system_info->shrapnel_flame_particles; + } else { + flame_effect = sip.default_subsys_shrapnel_flame_particles; + } + } + } else { + flame_effect = hull_flag ? sip.debris_flame_particles : sip.shrapnel_flame_particles; + } + if (flame_effect.isValid()) { + auto source = particle::ParticleManager::get()->createSource(flame_effect); source->setHost(std::make_unique(obj, vmd_zero_vector)); source->setTriggerRadius(source_obj->radius); source->setTriggerVelocity(vm_vec_mag_quick(&source_obj->phys_info.vel)); diff --git a/code/model/model.h b/code/model/model.h index 9ca939a602c..0d528036fdd 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -23,6 +23,7 @@ #include "model/model_flags.h" #include "object/object.h" #include "ship/ship_flags.h" +#include "particle/ParticleEffect.h" class object; class ship_info; @@ -293,6 +294,9 @@ class model_subsystem { /* contains rotation rate info */ float density; + particle::ParticleEffectHandle debris_flame_particles; + particle::ParticleEffectHandle shrapnel_flame_particles; + void reset(); model_subsystem(); diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index ab841b5f3c1..979da1d538a 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -1077,6 +1077,11 @@ void ship_info::clone(const ship_info& other) knossos_end_particles = other.knossos_end_particles; regular_end_particles = other.regular_end_particles; debris_flame_particles = other.debris_flame_particles; + shrapnel_flame_particles = other.shrapnel_flame_particles; + debris_end_particles = other.debris_end_particles; + shrapnel_end_particles = other.shrapnel_end_particles; + default_subsys_debris_flame_particles = other.default_subsys_debris_flame_particles; + default_subsys_shrapnel_flame_particles = other.default_subsys_shrapnel_flame_particles; debris_min_lifetime = other.debris_min_lifetime; debris_max_lifetime = other.debris_max_lifetime; @@ -1438,6 +1443,11 @@ void ship_info::move(ship_info&& other) std::swap(knossos_end_particles, other.knossos_end_particles); std::swap(regular_end_particles, other.regular_end_particles); std::swap(debris_flame_particles, other.debris_flame_particles); + std::swap(shrapnel_flame_particles, other.shrapnel_flame_particles); + std::swap(debris_end_particles, other.debris_end_particles); + std::swap(shrapnel_end_particles, other.shrapnel_end_particles); + std::swap(default_subsys_debris_flame_particles, other.default_subsys_debris_flame_particles); + std::swap(default_subsys_shrapnel_flame_particles, other.default_subsys_shrapnel_flame_particles); debris_min_lifetime = other.debris_min_lifetime; debris_max_lifetime = other.debris_max_lifetime; @@ -1808,6 +1818,11 @@ ship_info::ship_info() regular_end_particles = default_regular_end_particles; debris_flame_particles = particle::ParticleEffectHandle::invalid(); + shrapnel_flame_particles = particle::ParticleEffectHandle::invalid(); + debris_end_particles = particle::ParticleEffectHandle::invalid(); + shrapnel_end_particles = particle::ParticleEffectHandle::invalid(); + default_subsys_debris_flame_particles = particle::ParticleEffectHandle::invalid(); + default_subsys_shrapnel_flame_particles = particle::ParticleEffectHandle::invalid(); debris_min_lifetime = -1.0f; debris_max_lifetime = -1.0f; @@ -3917,6 +3932,21 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool sip->debris_flame_particles = particle::util::parseEffect(sip->name); } + if(optional_string("$Shrapnel Flame Effect:")) + { + sip->shrapnel_flame_particles = particle::util::parseEffect(sip->name); + } + + if(optional_string("$Debris Death Effect:")) + { + sip->debris_end_particles = particle::util::parseEffect(sip->name); + } + + if(optional_string("$Shrapnel Death Effect:")) + { + sip->shrapnel_end_particles = particle::util::parseEffect(sip->name); + } + auto skip_str = "$Skip Death Roll Percent Chance:"; auto vaporize_str = "$Vaporize Percent Chance:"; int which; @@ -5458,6 +5488,16 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool required_string("$end_custom_strings"); } + if(optional_string("$Default Subsystem Debris Flame Effect:")) + { + sip->default_subsys_debris_flame_particles = particle::util::parseEffect(sip->name); + } + + if(optional_string("$Default Subsystem Shrapnel Flame Effect:")) + { + sip->default_subsys_shrapnel_flame_particles = particle::util::parseEffect(sip->name); + } + int n_subsystems = 0; int n_excess_subsystems = 0; int cont_flag = 1; @@ -5555,6 +5595,8 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool sp->turret_max_bomb_ownage = -1; sp->turret_max_target_ownage = -1; sp->density = 1.0f; + sp->debris_flame_particles = particle::ParticleEffectHandle::invalid(); + sp->shrapnel_flame_particles = particle::ParticleEffectHandle::invalid(); } sfo_return = stuff_float_optional(&percentage_of_hits); if(sfo_return==2) @@ -5745,6 +5787,15 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool stuff_float(&sp->density); } + if(optional_string("$Debris Flame Effect:")) + { + sp->debris_flame_particles = particle::util::parseEffect(sip->name); + } + if(optional_string("$Shrapnel Debris Flame Effect:")) + { + sp->shrapnel_flame_particles = particle::util::parseEffect(sip->name); + } + if (optional_string("$Flags:")) { SCP_vector errors; flagset tmp_flags; diff --git a/code/ship/ship.h b/code/ship/ship.h index 14f67ddf644..d3bfa0b62e3 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1229,6 +1229,11 @@ class ship_info particle::ParticleEffectHandle knossos_end_particles; particle::ParticleEffectHandle regular_end_particles; particle::ParticleEffectHandle debris_flame_particles; + particle::ParticleEffectHandle shrapnel_flame_particles; + particle::ParticleEffectHandle debris_end_particles; + particle::ParticleEffectHandle shrapnel_end_particles; + particle::ParticleEffectHandle default_subsys_debris_flame_particles; + particle::ParticleEffectHandle default_subsys_shrapnel_flame_particles; //Debris stuff float debris_min_lifetime; diff --git a/code/ship/shipfx.cpp b/code/ship/shipfx.cpp index 3d030ed0779..33359200f73 100644 --- a/code/ship/shipfx.cpp +++ b/code/ship/shipfx.cpp @@ -282,7 +282,7 @@ void shipfx_blow_off_subsystem(object *ship_objp, ship *ship_p, const ship_subsy // create debris shards if (!(subsys->flags[Ship::Subsystem_Flags::Vanished]) && !no_explosion) { - shipfx_blow_up_model(ship_objp, psub->subobj_num, 50, &subobj_pos ); + shipfx_blow_up_model(ship_objp, psub->subobj_num, 50, &subobj_pos, subsys ); // create live debris objects, if any // TODO: some MULTIPLAYER implcations here!! @@ -343,7 +343,7 @@ static void shipfx_blow_up_hull(object *obj, const polymodel *pm, const polymode /** * Creates "ndebris" pieces of debris on random verts of the the "submodel" in the ship's model. */ -void shipfx_blow_up_model(object *obj, int submodel, int ndebris, const vec3d *exp_center) +void shipfx_blow_up_model(object *obj, int submodel, int ndebris, const vec3d *exp_center, const ship_subsys *subsys) { int i; @@ -374,7 +374,7 @@ void shipfx_blow_up_model(object *obj, int submodel, int ndebris, const vec3d *e vm_vec_avg( &tmp, &pnt1, &pnt2 ); model_instance_local_to_global_point(&outpnt, &tmp, pm, pmi, submodel, &obj->orient, &obj->pos ); - debris_create( obj, use_ship_debris ? Ship_info[Ships[obj->instance].ship_info_index].generic_debris_model_num : -1, -1, &outpnt, exp_center, 0, 1.0f ); + debris_create( obj, use_ship_debris ? Ship_info[Ships[obj->instance].ship_info_index].generic_debris_model_num : -1, -1, &outpnt, exp_center, false, 1.0f, subsys ); } } diff --git a/code/ship/shipfx.h b/code/ship/shipfx.h index 25aadea6d7e..905afde0260 100644 --- a/code/ship/shipfx.h +++ b/code/ship/shipfx.h @@ -36,7 +36,7 @@ extern void shipfx_blow_off_subsystem(object *ship_obj, ship *ship_p, const ship // Creates "ndebris" pieces of debris on random verts of the "submodel" in the // ship's model. -extern void shipfx_blow_up_model(object *obj, int submodel, int ndebris, const vec3d *exp_center); +extern void shipfx_blow_up_model(object *obj, int submodel, int ndebris, const vec3d *exp_center, const ship_subsys *subsys = nullptr); // ================================================= From 6cadc350b1641654fa26645e25bebae16848d70e Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 14:40:10 -0400 Subject: [PATCH 371/466] Start fixing myriad clang issues --- .../mission/dialogs/VariableDialogModel.cpp | 18 ++++++--------- .../src/mission/dialogs/VariableDialogModel.h | 23 ++++++++----------- 2 files changed, 16 insertions(+), 25 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 4d269d07ab2..d211e1e69dc 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -3,9 +3,7 @@ #include #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { static int _textMode = 0; VariableDialogModel::VariableDialogModel(QObject* parent, EditorViewport* viewport) @@ -311,7 +309,7 @@ bool VariableDialogModel::apply() for (const auto& container : _containerItems){ newContainers.push_back(createContainer(container)); - if (container.originalName != "" && container.name != container.originalName){ + if (!container.originalName.empty() && container.name != container.originalName){ renamedContainers[container.originalName] = container.name; } } @@ -783,7 +781,7 @@ bool VariableDialogModel::safeToAlterVariable(int index) } // FIXME! until we can actually count references (via a SEXP backend), this is the best way to go. - if (variable->originalName != ""){ + if (!variable->originalName.empty()){ return true; } @@ -1963,7 +1961,7 @@ bool VariableDialogModel::safeToAlterContainer(int index) } // FIXME! Until there's a sexp backend, we can only check if we just created the container. - if (container->originalName != ""){ + if (!container->originalName.empty()){ return true; } @@ -1972,8 +1970,8 @@ bool VariableDialogModel::safeToAlterContainer(int index) bool VariableDialogModel::safeToAlterContainer(const containerInfo& containerItem) { - // again, FIXME! Needs actally reference count. - return containerItem.originalName == ""; + // again, FIXME! Needs actual reference count. + return containerItem.originalName.empty(); } SCP_string VariableDialogModel::changeMapItemNumberValue(int index, int itemIndex, int newValue) @@ -2402,6 +2400,4 @@ SCP_string VariableDialogModel::trimIntegerString(SCP_string source) } -} // dialogs -} // fred -} // fso +} diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 9dac7b37b17..b8a8ce86d82 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -6,18 +6,16 @@ #include "parse/sexp_container.h" #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { struct variableInfo { SCP_string name = ""; - SCP_string originalName = ""; + SCP_string originalName; bool deleted = false; bool string = true; int flags = 0; int numberValue = 0; - SCP_string stringValue = ""; + SCP_string stringValue; }; @@ -163,10 +161,10 @@ class VariableDialogModel : public AbstractDialogModel { return nullptr; } - variableInfo* lookupVariableByName(SCP_string name){ - for (int x = 0; x < static_cast(_variableItems.size()); ++x) { - if (_variableItems[x].name == name) { - return &_variableItems[x]; + variableInfo* lookupVariableByName(const SCP_string& name){ + for (auto& variableItem : _variableItems) { + if (variableItem.name == name) { + return variableItem; } } @@ -181,7 +179,7 @@ class VariableDialogModel : public AbstractDialogModel { return nullptr; } - containerInfo* lookupContainerByName(SCP_string name){ + containerInfo* lookupContainerByName(const SCP_string& name){ for (int x = 0; x < static_cast(_containerItems.size()); ++x) { if (_containerItems[x].name == name) { return &_containerItems[x]; @@ -235,7 +233,7 @@ class VariableDialogModel : public AbstractDialogModel { // many of the controls in this editor can lead to drastic actions, so this will be very useful. - bool confirmAction(SCP_string question, SCP_string informativeText) + bool confirmAction(const SCP_string& question, const SCP_string& informativeText) { QMessageBox msgBox; msgBox.setText(question.c_str()); @@ -261,6 +259,3 @@ class VariableDialogModel : public AbstractDialogModel { }; } // namespace dialogs -} // namespace fred -} // namespace fso - From c85876d0d3f2e0d716fba4a7c013a5c6ad48baea Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 14:46:33 -0400 Subject: [PATCH 372/466] don't forget headers --- .../mission/dialogs/VariableDialogModel.cpp | 16 ++++++++-------- .../src/mission/dialogs/VariableDialogModel.h | 18 +++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index d211e1e69dc..65faec84758 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -592,7 +592,7 @@ bool VariableDialogModel::setVariableEternalFlag(int index, bool eternal) return eternal; } -SCP_string VariableDialogModel::setVariableStringValue(int index, SCP_string value) +SCP_string VariableDialogModel::setVariableStringValue(int index, const SCP_string& value) { auto variable = lookupVariable(index); @@ -664,7 +664,7 @@ SCP_string VariableDialogModel::addNewVariable(SCP_string nameIn) return _variableItems.back().name; } -SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName) +SCP_string VariableDialogModel::changeVariableName(int index, const SCP_string& newName) { auto variable = lookupVariable(index); @@ -1303,7 +1303,7 @@ SCP_string VariableDialogModel::copyContainer(int index) return _containerItems.back().name; } -SCP_string VariableDialogModel::changeContainerName(int index, SCP_string newName) +SCP_string VariableDialogModel::changeContainerName(int index, const SCP_string& newName) { auto container = lookupContainer(index); @@ -1370,7 +1370,7 @@ SCP_string VariableDialogModel::addListItem(int index) } } -SCP_string VariableDialogModel::addListItem(int index, SCP_string item) +SCP_string VariableDialogModel::addListItem(int index, const SCP_string& item) { auto container = lookupContainer(index); @@ -1454,7 +1454,7 @@ std::pair VariableDialogModel::addMapItem(int index) } // Overload for specified key and/or Value -std::pair VariableDialogModel::addMapItem(int index, SCP_string key, SCP_string value) +std::pair VariableDialogModel::addMapItem(int index, const SCP_string& key, const SCP_string& value) { auto container = lookupContainer(index); @@ -1543,7 +1543,7 @@ SCP_string VariableDialogModel::copyListItem(int containerIndex, int index) } -SCP_string VariableDialogModel::changeListItem(int containerIndex, int index, SCP_string newString) +SCP_string VariableDialogModel::changeListItem(int containerIndex, int index, const SCP_string& newString) { auto container = lookupContainer(containerIndex); @@ -1821,7 +1821,7 @@ bool VariableDialogModel::removeMapItem(int index, int itemIndex) return true; } -SCP_string VariableDialogModel::changeMapItemKey(int index, int keyRow, SCP_string newKey) +SCP_string VariableDialogModel::changeMapItemKey(int index, int keyRow, const SCP_string& newKey) { auto container = lookupContainer(index); @@ -1839,7 +1839,7 @@ SCP_string VariableDialogModel::changeMapItemKey(int index, int keyRow, SCP_stri return container->keys[keyRow]; } -SCP_string VariableDialogModel::changeMapItemStringValue(int index, int itemIndex, SCP_string newValue) +SCP_string VariableDialogModel::changeMapItemStringValue(int index, int itemIndex, const SCP_string& newValue) { auto item = lookupContainerStringItem(index, itemIndex); diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index b8a8ce86d82..aadcc55560f 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -64,12 +64,12 @@ class VariableDialogModel : public AbstractDialogModel { int setVariableOnMissionCloseOrCompleteFlag(int index, int flags); bool setVariableEternalFlag(int index, bool eternal); - SCP_string setVariableStringValue(int index, SCP_string value); + SCP_string setVariableStringValue(int index, const SCP_string& value); int setVariableNumberValue(int index, int value); SCP_string addNewVariable(); SCP_string addNewVariable(SCP_string nameIn); - SCP_string changeVariableName(int index, SCP_string newName); + SCP_string changeVariableName(int index, const SCP_string& newName); SCP_string copyVariable(int index); // returns whether it succeeded bool removeVariable(int index, bool toDelete); @@ -99,25 +99,25 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string addContainer(); SCP_string addContainer(SCP_string nameIn); SCP_string copyContainer(int index); - SCP_string changeContainerName(int index, SCP_string newName); + SCP_string changeContainerName(int index, const SCP_string& newName); bool removeContainer(int index, bool toDelete); SCP_string addListItem(int index); - SCP_string addListItem(int index, SCP_string item); + SCP_string addListItem(int index, const SCP_string& item); SCP_string copyListItem(int containerIndex, int index); bool removeListItem(int containerindex, int index); std::pair addMapItem(int index); - std::pair addMapItem(int index, SCP_string key, SCP_string value); + std::pair addMapItem(int index, const SCP_string& key, const SCP_string& value); std::pair copyMapItem(int index, int itemIndex); - SCP_string changeListItem(int containerIndex, int index, SCP_string newString); + SCP_string changeListItem(int containerIndex, int index, const SCP_string& newString); bool removeMapItem(int index, int rowIndex); void shiftListItemUp(int containerIndex, int itemIndex); void shiftListItemDown(int containerIndex, int itemIndex); - SCP_string changeMapItemKey(int index, int keyIndex, SCP_string newKey); - SCP_string changeMapItemStringValue(int index, int itemIndex, SCP_string newValue); + SCP_string changeMapItemKey(int index, int keyIndex, const SCP_string& newKey); + SCP_string changeMapItemStringValue(int index, int itemIndex, const SCP_string& newValue); SCP_string changeMapItemNumberValue(int index, int itemIndex, int newValue); const SCP_vector& getMapKeys(int index); @@ -199,7 +199,7 @@ class VariableDialogModel : public AbstractDialogModel { return nullptr; } - SCP_string* lookupContainerKeyByName(int containerIndex, SCP_string keyIn){ + SCP_string* lookupContainerKeyByName(int containerIndex, const SCP_string& keyIn){ if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ for (auto key = _containerItems[containerIndex].keys.begin(); key != _containerItems[containerIndex].keys.end(); ++key) { if (*key == keyIn){ From 7713e1b09b3a1321b3355a4d27b9f3d933070be4 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 17:17:33 -0400 Subject: [PATCH 373/466] Don't overzealously mark this --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 2 +- qtfred/src/mission/dialogs/VariableDialogModel.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 65faec84758..d5095689eac 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -664,7 +664,7 @@ SCP_string VariableDialogModel::addNewVariable(SCP_string nameIn) return _variableItems.back().name; } -SCP_string VariableDialogModel::changeVariableName(int index, const SCP_string& newName) +SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName) { auto variable = lookupVariable(index); diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index aadcc55560f..685a9603571 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -69,7 +69,7 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string addNewVariable(); SCP_string addNewVariable(SCP_string nameIn); - SCP_string changeVariableName(int index, const SCP_string& newName); + SCP_string changeVariableName(int index, SCP_string newName); SCP_string copyVariable(int index); // returns whether it succeeded bool removeVariable(int index, bool toDelete); From 1a60eff319e42cfef89ee3e24ec2ba5fe02d84d7 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 17:31:20 -0400 Subject: [PATCH 374/466] And try this --- qtfred/src/mission/dialogs/VariableDialogModel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 685a9603571..ec6d2ca4955 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -162,7 +162,7 @@ class VariableDialogModel : public AbstractDialogModel { } variableInfo* lookupVariableByName(const SCP_string& name){ - for (auto& variableItem : _variableItems) { + for (auto* variableItem : _variableItems) { if (variableItem.name == name) { return variableItem; } From cbd491e277582afb626f459a6fc372483004c3ec Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 20 Aug 2025 17:08:52 -0500 Subject: [PATCH 375/466] QtFRED Jump Node Editor Dialog (#6940) * qtfred jumpnode editor dialog * unused function * clang * and and use SCP_trim --- code/globalincs/vmallocator.cpp | 15 + code/globalincs/vmallocator.h | 1 + qtfred/source_groups.cmake | 5 + qtfred/src/mission/FredRenderer.cpp | 3 +- .../dialogs/JumpNodeEditorDialogModel.cpp | 376 ++++++++++++++++++ .../dialogs/JumpNodeEditorDialogModel.h | 66 +++ qtfred/src/ui/FredView.cpp | 14 +- qtfred/src/ui/FredView.h | 1 + .../src/ui/dialogs/JumpNodeEditorDialog.cpp | 131 ++++++ qtfred/src/ui/dialogs/JumpNodeEditorDialog.h | 41 ++ qtfred/ui/FredView.ui | 11 +- qtfred/ui/JumpNodeEditorDialog.ui | 230 +++++++++++ 12 files changed, 891 insertions(+), 3 deletions(-) create mode 100644 qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h create mode 100644 qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp create mode 100644 qtfred/src/ui/dialogs/JumpNodeEditorDialog.h create mode 100644 qtfred/ui/JumpNodeEditorDialog.ui diff --git a/code/globalincs/vmallocator.cpp b/code/globalincs/vmallocator.cpp index c464a57e908..d5939471a34 100644 --- a/code/globalincs/vmallocator.cpp +++ b/code/globalincs/vmallocator.cpp @@ -80,6 +80,21 @@ bool SCP_truncate(SCP_string &str, size_t len) return false; } +bool SCP_trim(SCP_string& str) +{ + auto start = str.find_first_not_of(" \t\r\n"); + auto end = str.find_last_not_of(" \t\r\n"); + if (start == SCP_string::npos) { + str.clear(); + return true; + } + if (start > 0 || end < str.length() - 1) { + str = str.substr(start, end - start + 1); + return true; + } + return false; +} + bool lcase_equal(const SCP_string& _Left, const SCP_string& _Right) { if (_Left.size() != _Right.size()) diff --git a/code/globalincs/vmallocator.h b/code/globalincs/vmallocator.h index 9483fc43893..191e0771007 100644 --- a/code/globalincs/vmallocator.h +++ b/code/globalincs/vmallocator.h @@ -91,6 +91,7 @@ extern void SCP_toupper(SCP_string &str); extern void SCP_totitle(SCP_string &str); extern bool SCP_truncate(SCP_string &str, size_t len); +extern bool SCP_trim(SCP_string& str); extern bool lcase_equal(const SCP_string& _Left, const SCP_string& _Right); extern bool lcase_lessthan(const SCP_string& _Left, const SCP_string& _Right); diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index fcf866d1129..261989bf6c4 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -52,6 +52,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/FictionViewerDialogModel.h src/mission/dialogs/FormWingDialogModel.cpp src/mission/dialogs/FormWingDialogModel.h + src/mission/dialogs/JumpNodeEditorDialogModel.cpp + src/mission/dialogs/JumpNodeEditorDialogModel.h src/mission/dialogs/LoadoutEditorDialogModel.cpp src/mission/dialogs/LoadoutEditorDialogModel.h src/mission/dialogs/MissionGoalsDialogModel.cpp @@ -124,6 +126,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/FictionViewerDialog.h src/ui/dialogs/FormWingDialog.cpp src/ui/dialogs/FormWingDialog.h + src/ui/dialogs/JumpNodeEditorDialog.cpp + src/ui/dialogs/JumpNodeEditorDialog.h src/ui/dialogs/LoadoutDialog.cpp src/ui/dialogs/LoadoutDialog.h src/ui/dialogs/MissionGoalsDialog.cpp @@ -217,6 +221,7 @@ add_file_folder("UI" ui/FictionViewerDialog.ui ui/FormWingDialog.ui ui/FredView.ui + ui/JumpNodeEditorDialog.ui ui/LoadoutDialog.ui ui/MissionGoalsDialog.ui ui/MissionSpecDialog.ui diff --git a/qtfred/src/mission/FredRenderer.cpp b/qtfred/src/mission/FredRenderer.cpp index b48c134868a..b92ccc10371 100644 --- a/qtfred/src/mission/FredRenderer.cpp +++ b/qtfred/src/mission/FredRenderer.cpp @@ -506,7 +506,8 @@ void FredRenderer::display_ship_info(int cur_object_index) { else strcpy_s(buf, "Briefing icon"); } else if (objp->type == OBJ_JUMP_NODE) { - strcpy_s(buf, "Jump Node"); + CJumpNode* jnp = jumpnode_get_by_objnum(OBJ_INDEX(objp)); + sprintf(buf, "%s\n%s", jnp->GetName(), jnp->GetDisplayName()); } else Assert(0); } diff --git a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp new file mode 100644 index 00000000000..93d9c9e37fe --- /dev/null +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp @@ -0,0 +1,376 @@ +#include "JumpNodeEditorDialogModel.h" + +#include "globalincs/linklist.h" +#include +#include +#include +#include +#include + +namespace fso::fred::dialogs { + +JumpNodeEditorDialogModel::JumpNodeEditorDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + connect(viewport->editor, &Editor::currentObjectChanged, this, &JumpNodeEditorDialogModel::onSelectedObjectChanged); + connect(viewport->editor, &Editor::objectMarkingChanged, this, &JumpNodeEditorDialogModel::onSelectedObjectMarkingChanged); + connect(viewport->editor, &Editor::missionChanged, this, &JumpNodeEditorDialogModel::onMissionChanged); + + initializeData(); +} + +bool JumpNodeEditorDialogModel::apply() +{ + if (_currentlySelectedNodeIndex < 0) { + // Nothing to apply + return true; + } + + // Validate + if (!validateData()) { + return false; + } + + // Commit + auto* jnp = jumpnode_get_by_objnum(getSelectedJumpNodeObjnum(_currentlySelectedNodeIndex)); + Assertion(jnp != nullptr, "Jump node not found during apply!"); + + char old_name_buf[NAME_LENGTH]; + std::strncpy(old_name_buf, jnp->GetName(), NAME_LENGTH - 1); + old_name_buf[NAME_LENGTH - 1] = '\0'; + + jnp->SetName(_name.c_str()); + jnp->SetDisplayName(lcase_equal(_display, "") ? _name.c_str() : _display.c_str()); + + // Only set a non default model + if (!lcase_equal(_modelFilename, JN_DEFAULT_MODEL)) { + jnp->SetModel(_modelFilename.c_str()); + } + + jnp->SetAlphaColor(_red, _green, _blue, _alpha); + jnp->SetVisibility(!_hidden); + + // Update sexp references when name changes + if (strcmp(old_name_buf, _name.c_str()) != 0) { + update_sexp_references(old_name_buf, _name.c_str()); + } + + _editor->missionChanged(); + return true; +} + +void JumpNodeEditorDialogModel::reject() +{ + // do nothing +} + +void JumpNodeEditorDialogModel::initializeData() +{ + buildNodeList(); + + // Find the currently selected object if it's a jump node + int objnum = -1; + if (query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_JUMP_NODE) { + objnum = _editor->currentObject; + } + + if (objnum >= 0) { + auto* jnp = jumpnode_get_by_objnum(objnum); + Assertion(jnp != nullptr, "Jump node not found for current object!"); + + _name = jnp->GetName(); + _display = jnp->HasDisplayName() ? jnp->GetDisplayName() : ""; + + const int model_num = jnp->GetModelNumber(); + if (auto* pm = model_get(model_num)) { + _modelFilename = pm->filename; + } else { + _modelFilename.clear(); + } + + const auto& c = jnp->GetColor(); + _red = c.red; + _green = c.green; + _blue = c.blue; + _alpha = c.alpha; + + _hidden = jnp->IsHidden(); + + // Find the index of the jump node in the local list + for (const auto& node : _nodes) { + if (!stricmp(node.first.c_str(), _name.c_str())) { + _currentlySelectedNodeIndex = node.second; + break; + } + } + } else { + _name.clear(); + _display.clear(); + _modelFilename.clear(); + _red = _green = _blue = _alpha = 0; + _hidden = false; + + _currentlySelectedNodeIndex = -1; + } + + Q_EMIT jumpNodeMarkingChanged(); +} + +void JumpNodeEditorDialogModel::buildNodeList() +{ + _nodes.clear(); + int idx = 0; + for (auto& node : Jump_nodes) { + _nodes.emplace_back(node.GetName(), idx++); + } +} + +bool JumpNodeEditorDialogModel::validateData() +{ + _bypass_errors = false; + + SCP_trim(_name); + + const SCP_string name = _name; + if (name.empty()) { + showErrorDialogNoCancel("A jump node name cannot be empty."); + return false; + } + + // Disallow leading '<' + if (!name.empty() && name[0] == '<') { + showErrorDialogNoCancel("Jump node names are not allowed to begin with '<'."); + return false; + } + + // Wing name collision + for (auto& wing : Wings) { + if (!stricmp(wing.name, name.c_str())) { + showErrorDialogNoCancel("This jump node name is already being used by a wing."); + return false; + } + } + + // Ship/start name collision + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_SHIP || ptr->type == OBJ_START) { + if (!stricmp(name.c_str(), Ships[ptr->instance].ship_name)) { + showErrorDialogNoCancel("This jump node name is already being used by a ship."); + return false; + } + } + } + + // AI target priority group collision + for (auto& ai : Ai_tp_list) { + if (!stricmp(name.c_str(), ai.name)) { + showErrorDialogNoCancel("This jump node name is already being used by a target priority group."); + return false; + } + } + + // Waypoint path collision + if (find_matching_waypoint_list(name.c_str()) != nullptr) { + showErrorDialogNoCancel("This jump node name is already being used by a waypoint path."); + return false; + } + + // Another jump node with the same name (but not this one) + auto* jnp = jumpnode_get_by_objnum(getSelectedJumpNodeObjnum(_currentlySelectedNodeIndex)); + auto* found = jumpnode_get_by_name(name.c_str()); + if (found != nullptr && found != jnp) { + showErrorDialogNoCancel("This jump node name is already being used by another jump node."); + return false; + } + + if (!cf_exists_full(_modelFilename.c_str(), CF_TYPE_MODELS)) { + showErrorDialogNoCancel("This jump node model file does not exist."); + return false; + } + + return true; +} + +void JumpNodeEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) +{ + if (_bypass_errors) { + return; + } + + _bypass_errors = true; + _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Error", message, {DialogButton::Ok}); +} + +int JumpNodeEditorDialogModel::getSelectedJumpNodeObjnum(int idx) const +{ + // Find the jump node and then mark it + for (const auto& node : Jump_nodes) { + if (!stricmp(node.GetName(), _nodes[idx].first.c_str())) { + return node.GetSCPObjectNumber(); + } + } + + return -1; +} + +void JumpNodeEditorDialogModel::onSelectedObjectChanged(int) +{ + initializeData(); +} + +void JumpNodeEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) +{ + initializeData(); +} + +void JumpNodeEditorDialogModel::onMissionChanged() +{ + initializeData(); +} + +const SCP_vector>& JumpNodeEditorDialogModel::getJumpNodeList() const +{ + return _nodes; +} + +void JumpNodeEditorDialogModel::selectJumpNodeByListIndex(int idx) +{ + if (_currentlySelectedNodeIndex == idx) { + // No change + return; + } + + if (!SCP_vector_inbounds(_nodes, idx)) + return; + + if (apply()) { + _editor->unmark_all(); + + int objnum = getSelectedJumpNodeObjnum(idx); + + if (objnum < 0) { + _currentlySelectedNodeIndex = -1; + return; + } + + _editor->markObject(objnum); + _currentlySelectedNodeIndex = idx; + } +} + +int JumpNodeEditorDialogModel::getCurrentJumpNodeIndex() const +{ + return _currentlySelectedNodeIndex; +} +bool JumpNodeEditorDialogModel::hasValidSelection() const +{ + return _currentlySelectedNodeIndex >= 0; +} + +void JumpNodeEditorDialogModel::setName(const SCP_string& v) +{ + SCP_trim(_name); + + SCP_string current = _name; + + _name = v; + if (apply()) { + set_modified(); + } else { + _name = current; // restore the old name + } +} + +const SCP_string& JumpNodeEditorDialogModel::getName() const +{ + return _name; +} + +void JumpNodeEditorDialogModel::setDisplayName(const SCP_string& v) +{ + modify(_display, v); + apply(); // Apply changes immediately to update the display name +} + +const SCP_string& JumpNodeEditorDialogModel::getDisplayName() const +{ + return _display; +} + +void JumpNodeEditorDialogModel::setModelFilename(const SCP_string& v) +{ + SCP_string current = _modelFilename; + + _modelFilename = v; + if (apply()) { + set_modified(); + } else { + _modelFilename = current; // restore the old name + } +} + +const SCP_string& JumpNodeEditorDialogModel::getModelFilename() const +{ + return _modelFilename; +} + +void JumpNodeEditorDialogModel::setColorR(int v) +{ + CLAMP(v, 0, 255); + modify(_red, v); + apply(); // Apply changes immediately to update the model color +} + +int JumpNodeEditorDialogModel::getColorR() const +{ + return _red; +} + +void JumpNodeEditorDialogModel::setColorG(int v) +{ + CLAMP(v, 0, 255); + modify(_green, v); + apply(); // Apply changes immediately to update the model color +} + +int JumpNodeEditorDialogModel::getColorG() const +{ + return _green; +} + +void JumpNodeEditorDialogModel::setColorB(int v) +{ + CLAMP(v, 0, 255); + modify(_blue, v); + apply(); // Apply changes immediately to update the model color +} + +int JumpNodeEditorDialogModel::getColorB() const +{ + return _blue; +} + +void JumpNodeEditorDialogModel::setColorA(int v) +{ + CLAMP(v, 0, 255); + modify(_alpha, v); + apply(); // Apply changes immediately to update the model color +} + +int JumpNodeEditorDialogModel::getColorA() const +{ + return _alpha; +} + +void JumpNodeEditorDialogModel::setHidden(bool v) +{ + modify(_hidden, v); + apply(); // Apply changes immediately to update the visibility +} + +bool JumpNodeEditorDialogModel::getHidden() const +{ + return _hidden; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h new file mode 100644 index 00000000000..8c0c56a34d5 --- /dev/null +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.h @@ -0,0 +1,66 @@ +#pragma once +#include "AbstractDialogModel.h" + +namespace fso::fred::dialogs { + +class JumpNodeEditorDialogModel : public AbstractDialogModel { + Q_OBJECT + public: + explicit JumpNodeEditorDialogModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; + + const SCP_vector>& getJumpNodeList() const; + void selectJumpNodeByListIndex(int idx); + int getCurrentJumpNodeIndex() const; + bool hasValidSelection() const; + + void setName(const SCP_string& v); + const SCP_string& getName() const; + void setDisplayName(const SCP_string& v); // "" means use Name + const SCP_string& getDisplayName() const; + void setModelFilename(const SCP_string& v); + const SCP_string& getModelFilename() const; + + void setColorR(int v); + int getColorR() const; + void setColorG(int v); + int getColorG() const; + void setColorB(int v); + int getColorB() const; + void setColorA(int v); + int getColorA() const; + + void setHidden(bool v); + bool getHidden() const; + + signals: + void jumpNodeMarkingChanged(); + + private slots: + void onSelectedObjectChanged(int); + void onSelectedObjectMarkingChanged(int, bool); + void onMissionChanged(); + + private: // NOLINT(readability-redundant-access-specifiers) + void initializeData(); + void buildNodeList(); + bool validateData(); + void showErrorDialogNoCancel(const SCP_string& message); + int getSelectedJumpNodeObjnum(int idx) const; + + int _currentlySelectedNodeIndex = -1; + + SCP_string _name; + SCP_string _display; + SCP_string _modelFilename; + int _red = 0, _green = 0, _blue = 0, _alpha = 0; + bool _hidden = false; + + SCP_vector> _nodes; + + bool _bypass_errors = false; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 17dff870125..f9b8550c78b 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -741,6 +742,12 @@ void FredView::on_actionWaypoint_Paths_triggered(bool) { editorDialog->setAttribute(Qt::WA_DeleteOnClose); editorDialog->show(); } +void FredView::on_actionJump_Nodes_triggered(bool) +{ + auto editorDialog = new dialogs::JumpNodeEditorDialog(this, _viewport); + editorDialog->setAttribute(Qt::WA_DeleteOnClose); + editorDialog->show(); +} void FredView::on_actionShips_triggered(bool) { if (!_shipEditorDialog) { @@ -860,7 +867,12 @@ void FredView::handleObjectEditor(int objNum) { fred->selectObject(objNum); // Use the existing slot for this to avoid duplicating code - on_actionWaypoint_Paths_triggered(false); + if (Objects[objNum].type == OBJ_JUMP_NODE) { + on_actionJump_Nodes_triggered(false); + } else if (Objects[objNum].type == OBJ_WAYPOINT) { + // If this is a waypoint, we need to show the waypoint editor + on_actionWaypoint_Paths_triggered(false); + } } else if (Objects[objNum].type == OBJ_POINT) { return; } else { diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 463f188cb66..2961758b138 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -92,6 +92,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionBriefing_triggered(bool); void on_actionMission_Specs_triggered(bool); void on_actionWaypoint_Paths_triggered(bool); + void on_actionJump_Nodes_triggered(bool); void on_actionObjects_triggered(bool); void on_actionShips_triggered(bool); void on_actionCampaign_triggered(bool); diff --git a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp new file mode 100644 index 00000000000..24739cf4f96 --- /dev/null +++ b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp @@ -0,0 +1,131 @@ +#include "ui/dialogs/JumpNodeEditorDialog.h" +#include "ui/util/SignalBlockers.h" +#include "ui_JumpNodeEditorDialog.h" + +#include + +namespace fso::fred::dialogs { + +JumpNodeEditorDialog::JumpNodeEditorDialog(FredView* parent, EditorViewport* viewport) + : QDialog(parent), _viewport(viewport), ui(new Ui::JumpNodeEditorDialog()), + _model(new JumpNodeEditorDialogModel(this, viewport)) +{ + this->setFocus(); + ui->setupUi(this); + + initializeUi(); + updateUi(); + + connect(_model.get(), &JumpNodeEditorDialogModel::jumpNodeMarkingChanged, this, [this] { + initializeUi(); + updateUi(); + }); + + // Resize the dialog to the minimum size + resize(QDialog::sizeHint()); +} + +JumpNodeEditorDialog::~JumpNodeEditorDialog() = default; + +void JumpNodeEditorDialog::initializeUi() +{ + util::SignalBlockers blockers(this); + + updateJumpNodeListComboBox(); + enableOrDisableControls(); +} + +void JumpNodeEditorDialog::updateJumpNodeListComboBox() +{ + ui->selectJumpNodeComboBox->clear(); + + for (auto& wp : _model->getJumpNodeList()) { + ui->selectJumpNodeComboBox->addItem(QString::fromStdString(wp.first), wp.second); + } + + ui->selectJumpNodeComboBox->setEnabled(!_model->getJumpNodeList().empty()); + + ui->selectJumpNodeComboBox->setCurrentIndex(_model->getCurrentJumpNodeIndex()); +} + +void JumpNodeEditorDialog::updateUi() +{ + util::SignalBlockers blockers(this); + + ui->nameLineEdit->setText(QString::fromStdString(_model->getName())); + ui->displayNameLineEdit->setText(QString::fromStdString(_model->getDisplayName())); + ui->modelFileLineEdit->setText(QString::fromStdString(_model->getModelFilename())); + + ui->redSpinBox->setValue(_model->getColorR()); + ui->greenSpinBox->setValue(_model->getColorG()); + ui->blueSpinBox->setValue(_model->getColorB()); + ui->alphaSpinBox->setValue(_model->getColorA()); + + ui->hiddenByDefaultCheckBox->setChecked(_model->getHidden()); +} + +void JumpNodeEditorDialog::enableOrDisableControls() +{ + const bool enable = _model->hasValidSelection(); + + ui->nameLineEdit->setEnabled(enable); + ui->displayNameLineEdit->setEnabled(enable); + ui->modelFileLineEdit->setEnabled(enable); + ui->redSpinBox->setEnabled(enable); + ui->greenSpinBox->setEnabled(enable); + ui->blueSpinBox->setEnabled(enable); + ui->alphaSpinBox->setEnabled(enable); + ui->hiddenByDefaultCheckBox->setEnabled(enable); +} + +void JumpNodeEditorDialog::on_selectJumpNodeComboBox_currentIndexChanged(int index) +{ + auto itemId = ui->selectJumpNodeComboBox->itemData(index).value(); + _model->selectJumpNodeByListIndex(itemId); +} + +void JumpNodeEditorDialog::on_nameLineEdit_editingFinished() +{ + _model->setName(ui->nameLineEdit->text().toUtf8().constData()); + updateUi(); // Update immediately in case the name change is rejected +} + +void JumpNodeEditorDialog::on_displayNameLineEdit_editingFinished() +{ + _model->setDisplayName(ui->displayNameLineEdit->text().toUtf8().constData()); +} + +void JumpNodeEditorDialog::on_modelFileLineEdit_editingFinished() +{ + _model->setModelFilename(ui->modelFileLineEdit->text().toUtf8().constData()); + updateUi(); // Update immediately in case the name change is rejected +} + +void JumpNodeEditorDialog::on_redSpinBox_valueChanged(int value) +{ + _model->setColorR(value); +} + +void JumpNodeEditorDialog::on_greenSpinBox_valueChanged(int value) +{ + _model->setColorG(value); +} + +void JumpNodeEditorDialog::on_blueSpinBox_valueChanged(int value) +{ + _model->setColorB(value); +} + +void JumpNodeEditorDialog::on_alphaSpinBox_valueChanged(int value) +{ + _model->setColorA(value); +} + +void JumpNodeEditorDialog::on_hiddenByDefaultCheckBox_toggled(bool checked) +{ + _model->setHidden(checked); +} + +void temp() {}; + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h new file mode 100644 index 00000000000..bdff41ae1f2 --- /dev/null +++ b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include + +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class JumpNodeEditorDialog; +} + +class JumpNodeEditorDialog : public QDialog { + Q_OBJECT + public: + JumpNodeEditorDialog(FredView* parent, EditorViewport* viewport); + ~JumpNodeEditorDialog() override; + + private slots: + void on_selectJumpNodeComboBox_currentIndexChanged(int index); + void on_nameLineEdit_editingFinished(); + void on_displayNameLineEdit_editingFinished(); + void on_modelFileLineEdit_editingFinished(); + void on_redSpinBox_valueChanged(int value); + void on_greenSpinBox_valueChanged(int value); + void on_blueSpinBox_valueChanged(int value); + void on_alphaSpinBox_valueChanged(int value); + void on_hiddenByDefaultCheckBox_toggled(bool checked); + + private: // NOLINT(readability-redundant-access-specifiers) + EditorViewport* _viewport; + std::unique_ptr ui; + std::unique_ptr _model; + + void initializeUi(); + void updateJumpNodeListComboBox(); + void updateUi(); + void enableOrDisableControls(); +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index 66690cfb25c..9b38b073b13 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -20,7 +20,7 @@ 0 0 926 - 21 + 22 @@ -168,6 +168,7 @@ + @@ -1510,6 +1511,14 @@ Shift+G + + + Jump &Nodes + + + Shift+J + + diff --git a/qtfred/ui/JumpNodeEditorDialog.ui b/qtfred/ui/JumpNodeEditorDialog.ui new file mode 100644 index 00000000000..a69815c6fd6 --- /dev/null +++ b/qtfred/ui/JumpNodeEditorDialog.ui @@ -0,0 +1,230 @@ + + + fso::fred::dialogs::JumpNodeEditorDialog + + + true + + + + 0 + 0 + 275 + 208 + + + + + 275 + 208 + + + + Jump Node Editor + + + + + + + + + + Select Jump Node + + + + + + + + + + + + Qt::Horizontal + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Name + + + + + + + + + + Display Name + + + + + + + + + + Model File + + + + + + + + + + + + Node Color + + + + + + R + + + + + + + + 41 + 0 + + + + 255 + + + 0 + + + + + + + G + + + + + + + + 41 + 0 + + + + 255 + + + 0 + + + + + + + B + + + + + + + + 41 + 0 + + + + 255 + + + 0 + + + + + + + A + + + + + + + + 41 + 0 + + + + 255 + + + 0 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Hidden By Default + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + From 39f80289c2702ab8753f70136dd703dd620f82e9 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 20 Aug 2025 17:51:53 -0500 Subject: [PATCH 376/466] QtFRED Mission Cutscenes Dialog (#6956) * Mission cutscenes dialog * fixes * getter for now private member * missed some cleanup * assertion --- code/mission/missionparse.h | 16 ++ qtfred/source_groups.cmake | 5 + .../dialogs/MissionCutscenesDialogModel.cpp | 167 +++++++++++++ .../dialogs/MissionCutscenesDialogModel.h | 60 +++++ qtfred/src/ui/FredView.cpp | 7 + qtfred/src/ui/FredView.h | 1 + .../src/ui/dialogs/MissionCutscenesDialog.cpp | 225 ++++++++++++++++++ .../src/ui/dialogs/MissionCutscenesDialog.h | 58 +++++ qtfred/ui/FredView.ui | 9 + qtfred/ui/MissionCutscenesDialog.ui | 205 ++++++++++++++++ 10 files changed, 753 insertions(+) create mode 100644 qtfred/src/mission/dialogs/MissionCutscenesDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/MissionCutscenesDialogModel.h create mode 100644 qtfred/src/ui/dialogs/MissionCutscenesDialog.cpp create mode 100644 qtfred/src/ui/dialogs/MissionCutscenesDialog.h create mode 100644 qtfred/ui/MissionCutscenesDialog.ui diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index 4b56a3c97ce..ae05c7ab121 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -118,6 +118,22 @@ enum : int { Num_movie_types }; +struct cutscene_type_data { + int value; // enum + SCP_string label; // shown in combo boxes + SCP_string desc; // short explanation for the description box +}; + +static const cutscene_type_data CutsceneMenuData[] = { + {MOVIE_PRE_FICTION, "Fiction Viewer", "Plays just before the fiction viewer game state"}, + {MOVIE_PRE_CMD_BRIEF, "Command Briefing", "Plays just before the command briefing game state"}, + {MOVIE_PRE_BRIEF, "Briefing", "Plays just before the briefing game state"}, + {MOVIE_PRE_GAME, "Pre-game", "Plays just before the mission starts after Accept has been pressed"}, + {MOVIE_PRE_DEBRIEF, "Debriefing", "Plays just before the debriefing game state"}, + {MOVIE_POST_DEBRIEF, "Post-debriefing", "Plays when the debriefing has been accepted but before exiting the mission"}, + {MOVIE_END_CAMPAIGN, "End Campaign", "Plays when the campaign has been completed"} +}; + // defines a mission cutscene. typedef struct mission_cutscene { int type; diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 261989bf6c4..9214da1b029 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -56,6 +56,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/JumpNodeEditorDialogModel.h src/mission/dialogs/LoadoutEditorDialogModel.cpp src/mission/dialogs/LoadoutEditorDialogModel.h + src/mission/dialogs/MissionCutscenesDialogModel.cpp + src/mission/dialogs/MissionCutscenesDialogModel.h src/mission/dialogs/MissionGoalsDialogModel.cpp src/mission/dialogs/MissionGoalsDialogModel.h src/mission/dialogs/MissionSpecDialogModel.cpp @@ -130,6 +132,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/JumpNodeEditorDialog.h src/ui/dialogs/LoadoutDialog.cpp src/ui/dialogs/LoadoutDialog.h + src/ui/dialogs/MissionCutscenesDialog.cpp + src/ui/dialogs/MissionCutscenesDialog.h src/ui/dialogs/MissionGoalsDialog.cpp src/ui/dialogs/MissionGoalsDialog.h src/ui/dialogs/MissionSpecDialog.cpp @@ -223,6 +227,7 @@ add_file_folder("UI" ui/FredView.ui ui/JumpNodeEditorDialog.ui ui/LoadoutDialog.ui + ui/MissionCutscenesDialog.ui ui/MissionGoalsDialog.ui ui/MissionSpecDialog.ui ui/ObjectOrientationDialog.ui diff --git a/qtfred/src/mission/dialogs/MissionCutscenesDialogModel.cpp b/qtfred/src/mission/dialogs/MissionCutscenesDialogModel.cpp new file mode 100644 index 00000000000..5815292dbaa --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionCutscenesDialogModel.cpp @@ -0,0 +1,167 @@ +#include "MissionCutscenesDialogModel.h" + + +namespace fso::fred::dialogs { + +MissionCutscenesDialogModel::MissionCutscenesDialogModel(QObject* parent, fso::fred::EditorViewport* viewport) : + AbstractDialogModel(parent, viewport) { +} +bool MissionCutscenesDialogModel::apply() +{ + SCP_vector> names; + + auto changes_detected = query_modified(); + + for (auto& cs : The_mission.cutscenes) { + free_sexp2(cs.formula); + } + + The_mission.cutscenes.clear(); + The_mission.cutscenes.reserve(m_cutscenes.size()); + for (const auto& item : m_cutscenes) { + The_mission.cutscenes.push_back(item); + The_mission.cutscenes.back().formula = _sexp_tree->save_tree(item.formula); + } + + // Only fire the signal after the changes have been applied to make sure the other parts of the code see the updated state + if (changes_detected) { + _editor->missionChanged(); + } + + return true; +} +void MissionCutscenesDialogModel::reject() +{ + // Nothing to do here +} +mission_cutscene& MissionCutscenesDialogModel::getCurrentCutscene() +{ + Assertion(SCP_vector_inbounds(m_cutscenes, cur_cutscene), "Current cutscene index is not valid!"); + return m_cutscenes[cur_cutscene]; +} +bool MissionCutscenesDialogModel::isCurrentCutsceneValid() const +{ + return SCP_vector_inbounds(m_cutscenes, cur_cutscene); +} +void MissionCutscenesDialogModel::initializeData() +{ + m_cutscenes.clear(); + m_sig.clear(); + for (int i = 0; i < static_cast(The_mission.cutscenes.size()); i++) { + m_cutscenes.push_back(The_mission.cutscenes[i]); + m_sig.push_back(i); + + if (m_cutscenes[i].filename[0] == '\0') + strcpy_s(m_cutscenes[i].filename, ""); + } + + cur_cutscene = -1; + modelChanged(); +} +SCP_vector& MissionCutscenesDialogModel::getCutscenes() +{ + return m_cutscenes; +} +void MissionCutscenesDialogModel::setCurrentCutscene(int index) +{ + cur_cutscene = index; + + modelChanged(); +} + +int MissionCutscenesDialogModel::getSelectedCutsceneType() const +{ + return m_display_cutscene_types; +} + +bool MissionCutscenesDialogModel::isCutsceneVisible(const mission_cutscene& cutscene) const +{ + return (cutscene.type == m_display_cutscene_types); +} +void MissionCutscenesDialogModel::setCutsceneType(int type) +{ + modify(m_display_cutscene_types, type); +} +int MissionCutscenesDialogModel::getCutsceneType() const +{ + return m_display_cutscene_types; +} +bool MissionCutscenesDialogModel::query_modified() +{ + if (modified) + return true; + + if (The_mission.cutscenes.size() != m_cutscenes.size()) + return true; + + for (size_t i = 0; i < The_mission.cutscenes.size(); i++) { + if (!lcase_equal(The_mission.cutscenes[i].filename, m_cutscenes[i].filename)) + return true; + if (The_mission.cutscenes[i].type != m_cutscenes[i].type) + return true; + } + + return false; +} +void MissionCutscenesDialogModel::setTreeControl(sexp_tree* tree) +{ + _sexp_tree = tree; +} +void MissionCutscenesDialogModel::deleteCutscene(int node) +{ + size_t i; + for (i = 0; i < m_cutscenes.size(); i++) { + if (m_cutscenes[i].formula == node) { + break; + } + } + + Assertion(i < m_cutscenes.size(), "Invalid cutscene index!"); + m_cutscenes.erase(m_cutscenes.begin() + i); + m_sig.erase(m_sig.begin() + i); + + set_modified(); + modelChanged(); +} +void MissionCutscenesDialogModel::changeFormula(int old_form, int new_form) +{ + size_t i; + for (i=0; i + +#include "ui/widgets/sexp_tree.h" + +namespace fso::fred::dialogs { + +class MissionCutscenesDialogModel: public AbstractDialogModel { + public: + MissionCutscenesDialogModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; + + mission_cutscene& getCurrentCutscene(); + + bool isCurrentCutsceneValid() const; + + void setCurrentCutscene(int index); + int getSelectedCutsceneType() const; + + void initializeData(); + + SCP_vector& getCutscenes(); + + bool isCutsceneVisible(const mission_cutscene& goal) const; + + void setCutsceneType(int type); + + int getCutsceneType() const; + + void deleteCutscene(int formula); + + void changeFormula(int old_form, int new_form); + + mission_cutscene& createNewCutscene(); + + bool query_modified(); + + void setCurrentCutsceneType(int type); + void setCurrentCutsceneFilename(const char* filename); + + // TODO HACK: This does not belong here since it is a UI specific control. Once the model based SEXP tree is implemented + // this should be replaced + void setTreeControl(sexp_tree* tree); + private: + int cur_cutscene = -1; + SCP_vector m_sig; + SCP_vector m_cutscenes; + bool modified = false; + + int m_display_cutscene_types = 0; + + sexp_tree* _sexp_tree = nullptr; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index f9b8550c78b..cad47078826 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -710,6 +711,12 @@ void FredView::on_actionMission_Events_triggered(bool) { eventEditor->setAttribute(Qt::WA_DeleteOnClose); eventEditor->show(); } +void FredView::on_actionMission_Cutscenes_triggered(bool) +{ + auto cutsceneEditor = new dialogs::MissionCutscenesDialog(this, _viewport); + cutsceneEditor->setAttribute(Qt::WA_DeleteOnClose); + cutsceneEditor->show(); +} void FredView::on_actionSelectionLock_triggered(bool enabled) { _viewport->Selection_lock = enabled; } diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 2961758b138..ac349e99832 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -88,6 +88,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionCurrent_Ship_triggered(bool enabled); void on_actionMission_Events_triggered(bool); + void on_actionMission_Cutscenes_triggered(bool); void on_actionAsteroid_Field_triggered(bool); void on_actionBriefing_triggered(bool); void on_actionMission_Specs_triggered(bool); diff --git a/qtfred/src/ui/dialogs/MissionCutscenesDialog.cpp b/qtfred/src/ui/dialogs/MissionCutscenesDialog.cpp new file mode 100644 index 00000000000..0055905b692 --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionCutscenesDialog.cpp @@ -0,0 +1,225 @@ +#include +#include "MissionCutscenesDialog.h" + +#include "ui/util/SignalBlockers.h" +#include "mission/util.h" +#include "ui_MissionCutscenesDialog.h" + +namespace fso::fred::dialogs { + +MissionCutscenesDialog::MissionCutscenesDialog(QWidget* parent, EditorViewport* viewport) + : QDialog(parent), SexpTreeEditorInterface({TreeFlags::LabeledRoot, TreeFlags::RootDeletable}), + ui(new Ui::MissionCutscenesDialog()), _model(new MissionCutscenesDialogModel(this, viewport)), _viewport(viewport) +{ + ui->setupUi(this); + + populateCutsceneCombos(); + + ui->cutsceneEventTree->initializeEditor(viewport->editor, this); + _model->setTreeControl(ui->cutsceneEventTree); + + ui->cutsceneFilename->setMaxLength(NAME_LENGTH - 1); + + ui->helpTextBox->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + + connect(_model.get(), &MissionCutscenesDialogModel::modelChanged, this, &MissionCutscenesDialog::updateUi); + + _model->initializeData(); + + load_tree(); + + recreate_tree(); +} + +MissionCutscenesDialog::~MissionCutscenesDialog() = default; + +void MissionCutscenesDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void MissionCutscenesDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close +} + +void MissionCutscenesDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window +} + +void MissionCutscenesDialog::updateUi() +{ + // Avoid infinite recursion by blocking signal calls caused by our changes here + util::SignalBlockers blocker(this); + + if (!_model->isCurrentCutsceneValid()) { + ui->cutsceneFilename->setText(QString()); + ui->cutsceneTypeCombo->setCurrentIndex(-1); + + ui->cutsceneTypeCombo->setEnabled(false); + ui->cutsceneFilename->setEnabled(false); + + return; + } + + auto& cutscene = _model->getCurrentCutscene(); + + ui->cutsceneFilename->setText(QString::fromUtf8(cutscene.filename)); + ui->cutsceneTypeCombo->setCurrentIndex(cutscene.type); + + ui->cutsceneTypeCombo->setEnabled(true); + ui->cutsceneFilename->setEnabled(true); + + setCutsceneTypeDescription(); +} +void MissionCutscenesDialog::load_tree() +{ + ui->cutsceneEventTree->clear_tree(); + auto& cutscenes = _model->getCutscenes(); + for (auto& scene : cutscenes) { + scene.formula = ui->cutsceneEventTree->load_sub_tree(scene.formula, true, "true"); + } + ui->cutsceneEventTree->post_load(); +} +void MissionCutscenesDialog::recreate_tree() +{ + ui->cutsceneEventTree->clear(); + const auto& cutscenes = _model->getCutscenes(); + for (const auto& scene : cutscenes) { + if (!_model->isCutsceneVisible(scene)) { + continue; + } + + auto h = ui->cutsceneEventTree->insert(scene.filename); + h->setData(0, sexp_tree::FormulaDataRole, scene.formula); + ui->cutsceneEventTree->add_sub_tree(scene.formula, h); + } + + _model->setCurrentCutscene(-1); +} +void MissionCutscenesDialog::createNewCutscene() +{ + auto& scene = _model->createNewCutscene(); + + auto h = ui->cutsceneEventTree->insert(scene.filename); + + ui->cutsceneEventTree->setCurrentItemIndex(-1); + ui->cutsceneEventTree->add_operator("true", h); + auto index = scene.formula = ui->cutsceneEventTree->getCurrentItemIndex(); + h->setData(0, sexp_tree::FormulaDataRole, index); + + ui->cutsceneEventTree->setCurrentItem(h); +} +void MissionCutscenesDialog::changeCutsceneCategory(int type) +{ + if (_model->isCurrentCutsceneValid()) { + _model->setCurrentCutsceneType(type); + recreate_tree(); + } +} + +void MissionCutscenesDialog::populateCutsceneCombos() +{ + ui->displayTypeCombo->clear(); + ui->cutsceneTypeCombo->clear(); + + for (auto& item : CutsceneMenuData) { + ui->displayTypeCombo->addItem(QString::fromStdString(item.label), item.value); + ui->cutsceneTypeCombo->addItem(QString::fromStdString(item.label), item.value); + } + + ui->displayTypeCombo->setCurrentIndex(_model->getSelectedCutsceneType()); + setCutsceneTypeDescription(); +} + +void MissionCutscenesDialog::setCutsceneTypeDescription() +{ + auto index = _model->getCutsceneType(); + if (index < 0 || index >= Num_movie_types) { + ui->cutsceneTypeDescription->setText(QString()); + return; + } + + ui->cutsceneTypeDescription->setText(QString::fromStdString(CutsceneMenuData[index].desc)); +} + +void MissionCutscenesDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void MissionCutscenesDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void MissionCutscenesDialog::on_displayTypeCombo_currentIndexChanged(int index) +{ + _model->setCutsceneType(index); + setCutsceneTypeDescription(); + recreate_tree(); +} + +void MissionCutscenesDialog::on_cutsceneTypeCombo_currentIndexChanged(int index) +{ + changeCutsceneCategory(index); +} + +void MissionCutscenesDialog::on_cutsceneFilename_textChanged(const QString& text) +{ + if (_model->isCurrentCutsceneValid()) { + _model->setCurrentCutsceneFilename(text.toUtf8().constData()); + + auto item = ui->cutsceneEventTree->currentItem(); + while (item->parent() != nullptr) { + item = item->parent(); + } + + item->setText(0, text); + } +} + +void MissionCutscenesDialog::on_newCutsceneBtn_clicked() +{ + createNewCutscene(); +} + +void MissionCutscenesDialog::on_cutsceneEventTree_selectedRootChanged(int formula) +{ + auto& cutscenes = _model->getCutscenes(); + for (size_t i = 0; i < cutscenes.size(); ++i) { + if (cutscenes[i].formula == formula) { + _model->setCurrentCutscene(static_cast(i)); + break; + } + } +} + +void MissionCutscenesDialog::on_cutsceneEventTree_rootNodeDeleted(int node) +{ + _model->deleteCutscene(node); +} + +void MissionCutscenesDialog::on_cutsceneEventTree_rootNodeFormulaChanged(int old, int node) +{ + _model->changeFormula(old, node); +} + +void MissionCutscenesDialog::on_cutsceneEventTree_helpChanged(const QString& help) +{ + ui->helpTextBox->setPlainText(help); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionCutscenesDialog.h b/qtfred/src/ui/dialogs/MissionCutscenesDialog.h new file mode 100644 index 00000000000..385a6936058 --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionCutscenesDialog.h @@ -0,0 +1,58 @@ +#pragma once + +#include + +#include "mission/EditorViewport.h" +#include "mission/dialogs/MissionCutscenesDialogModel.h" + +#include "ui/widgets/sexp_tree.h" + +namespace fso::fred::dialogs { + +namespace Ui { +class MissionCutscenesDialog; +} + +class MissionCutscenesDialog : public QDialog, public SexpTreeEditorInterface { + Q_OBJECT + +public: + explicit MissionCutscenesDialog(QWidget* parent, EditorViewport* viewport); + ~MissionCutscenesDialog() override; + + void accept() override; + void reject() override; + + protected: + void closeEvent(QCloseEvent* event) override; + +private slots: + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + void on_displayTypeCombo_currentIndexChanged(int index); + void on_cutsceneTypeCombo_currentIndexChanged(int index); + void on_cutsceneFilename_textChanged(const QString& text); + void on_newCutsceneBtn_clicked(); + + void on_cutsceneEventTree_selectedRootChanged(int formula); + void on_cutsceneEventTree_rootNodeDeleted(int node); + void on_cutsceneEventTree_rootNodeFormulaChanged(int old, int node); + void on_cutsceneEventTree_helpChanged(const QString& help); + + private: // NOLINT(readability-redundant-access-specifiers) + void updateUi(); + void createNewCutscene(); + void changeCutsceneCategory(int type); + void populateCutsceneCombos(); + void setCutsceneTypeDescription(); + + std::unique_ptr ui; + std::unique_ptr _model; + + EditorViewport* _viewport = nullptr; + void load_tree(); + void recreate_tree(); +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index 9b38b073b13..7be8e874def 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -173,6 +173,7 @@ + @@ -1519,6 +1520,14 @@ Shift+J + + + Mission Cutscenes + + + Shift+X + + diff --git a/qtfred/ui/MissionCutscenesDialog.ui b/qtfred/ui/MissionCutscenesDialog.ui new file mode 100644 index 00000000000..77ff3db0375 --- /dev/null +++ b/qtfred/ui/MissionCutscenesDialog.ui @@ -0,0 +1,205 @@ + + + fso::fred::dialogs::MissionCutscenesDialog + + + Qt::WindowModal + + + + 0 + 0 + 655 + 430 + + + + Mission Objectives + + + + + + Qt::Vertical + + + false + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + true + + + + QLayout::SetFixedSize + + + + + Display Cutscene + + + displayTypeCombo + + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Sunken + + + + + + Description + + + true + + + + + + + + + + Current Cutscene + + + + QLayout::SetMinAndMaxSize + + + QFormLayout::AllNonFixedFieldsGrow + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + Qt::AlignJustify|Qt::AlignTop + + + + + T&ype + + + cutsceneTypeCombo + + + + + + + + 0 + 0 + + + + + + + + Filename + + + cutsceneFilename + + + + + + + + + + + + + 40 + + + + + + 1 + 0 + + + + New Cutscene + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + + true + + + + + + + + + fso::fred::sexp_tree + QTreeView +
ui/widgets/sexp_tree.h
+
+
+ + +
From 83dee2eab44b92779a4681b36163e4dc739d2db8 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Wed, 20 Aug 2025 18:56:16 -0400 Subject: [PATCH 377/466] Spare future modders (#6960) --- code/model/modelread.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/model/modelread.cpp b/code/model/modelread.cpp index 1e264477b64..11f671cb817 100644 --- a/code/model/modelread.cpp +++ b/code/model/modelread.cpp @@ -490,7 +490,7 @@ void get_user_prop_value(char *buf, char *value) char *p, *p1, c; p = buf; - while ( isspace(*p) || (*p == '=') ) // skip white space and equal sign + while ( isspace(*p) || (*p == '=') || (*p == ':') ) // skip white space, equal sign, and colon p++; p1 = p; while ( !iscntrl(*p1) ) // copy until we get to a control character From a4014ea98579aa50b244b05b1316737ca1ba4f2f Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 22:02:40 -0400 Subject: [PATCH 378/466] According to the internet this should work --- qtfred/src/mission/dialogs/VariableDialogModel.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index ec6d2ca4955..5fcb15f47c5 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -162,9 +162,9 @@ class VariableDialogModel : public AbstractDialogModel { } variableInfo* lookupVariableByName(const SCP_string& name){ - for (auto* variableItem : _variableItems) { + for (auto& variableItem : _variableItems) { if (variableItem.name == name) { - return variableItem; + return &variableItem; } } From 4efcd82940f4e1502d082e6f53ec1cbeccdd1fb6 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 22:05:52 -0400 Subject: [PATCH 379/466] Make sure to use empty --- .../src/mission/dialogs/VariableDialogModel.cpp | 16 +++++++--------- qtfred/src/ui/dialogs/VariableDialog.cpp | 4 ++-- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index d5095689eac..ddcf402227c 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -500,7 +500,7 @@ bool VariableDialogModel::setVariableType(int index, bool string) // this variable is currently a string if (variable->string) { // no risk change, because no string was specified. - if (variable->stringValue == "") { + if (variable->stringValue.empty()) { variable->string = string; return variable->string; } else { @@ -526,7 +526,7 @@ bool VariableDialogModel::setVariableType(int index, bool string) return variable->string; } else { // if there was no previous string value - if (variable->stringValue == ""){ + if (variable->stringValue.empty()){ sprintf(variable->stringValue, "%i", variable->numberValue); } @@ -791,7 +791,7 @@ bool VariableDialogModel::safeToAlterVariable(int index) bool VariableDialogModel::safeToAlterVariable(const variableInfo& variableItem) { // again, FIXME! Needs actally reference count. - return variableItem.originalName == ""; + return variableItem.originalName.empty(); } @@ -1183,7 +1183,6 @@ bool VariableDialogModel::setContainerListOrMap(int index, bool list) return container->list; } - return !list; } bool VariableDialogModel::setContainerNetworkStatus(int index, bool network) @@ -1719,7 +1718,6 @@ std::pair VariableDialogModel::copyMapItem(int index, in return std::make_pair(newKey, temp); } - return std::make_pair("", ""); } // requires a model reload anyway, so no return value. @@ -1887,7 +1885,7 @@ void VariableDialogModel::swapKeyAndValues(int index) // not as easy part 2 for (int x = 0; x < static_cast(keysCopy.size()); ++x) { - if (keysCopy[x] == ""){ + if (keysCopy[x].empty()){ container->numberValues[x] = 0; } else { try { @@ -2061,9 +2059,9 @@ const SCP_vector> VariableDialogModel::getVariableValu notes = "Referenced"; } else if (item.deleted){ notes = "To Be Deleted"; - } else if (item.originalName == ""){ + } else if (item.originalName.empty()){ notes = "New"; - } else if ((item.string && item.stringValue == "") || (!item.string && item.numberValue == 0)){ + } else if ((item.string && item.stringValue.empty()) || (!item.string && item.numberValue == 0)){ notes = "Default Value"; } else if (item.name != item.originalName){ notes = "Renamed"; @@ -2218,7 +2216,7 @@ const SCP_vector> VariableDialogModel::getContainerNam notes = "Referenced"; } else if (item.deleted) { notes = "To Be Deleted"; - } else if (item.originalName == "") { + } else if (item.originalName.empty()) { notes = "New"; } else if (item.name != item.originalName){ notes = "Renamed"; diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index ef73dbd6682..150fb957fe0 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -329,7 +329,7 @@ void VariableDialog::onVariablesTableUpdated() auto ret = _model->changeVariableName(item->row(), itemText); // we put something in the cell, but the model couldn't process it. - if (strlen(item->text().toStdString().c_str()) && ret == ""){ + if (strlen(item->text().toStdString().c_str()) && ret.empty()){ // update of variable name failed, resync UI apply = true; @@ -353,7 +353,7 @@ void VariableDialog::onVariablesTableUpdated() temp = temp.substr(0, NAME_LENGTH - 1); SCP_string ret = _model->setVariableStringValue(item->row(), temp); - if (ret == ""){ + if (ret.empty()){ apply = true; } else { item->setText(ret.c_str()); From 81d08cd6f312851aca4a7c840634575523edd833 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 22:13:16 -0400 Subject: [PATCH 380/466] More Clang --- .../mission/dialogs/VariableDialogModel.cpp | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index ddcf402227c..9ebb8bd4150 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -2053,7 +2053,7 @@ const SCP_vector> VariableDialogModel::getVariableValu SCP_vector> outStrings; for (const auto& item : _variableItems){ - SCP_string notes = ""; + SCP_string notes; if (!safeToAlterVariable(item)){ notes = "Referenced"; @@ -2178,8 +2178,8 @@ const SCP_vector> VariableDialogModel::getContainerNam SCP_vector> outStrings; for (const auto& item : _containerItems) { - SCP_string type = ""; - SCP_string notes = ""; + SCP_string type; + SCP_string notes; if (item.string) { type = "String"; @@ -2309,11 +2309,7 @@ bool VariableDialogModel::atMaxVariables() } } - if (count < MAX_SEXP_VARIABLES){ - return false; - } else { - return true; - } + return count < MAX_SEXP_VARIABLES; } // This function is for cleaning up input strings that should be numbers. We could use std::stoi, @@ -2351,11 +2347,7 @@ SCP_string VariableDialogModel::trimIntegerString(SCP_string source) break; // only copy the '-' char if it is the first thing to be copied. case '-': - if (ret.empty()){ - return true; - } else { - return false; - } + return ret.empty(); default: return false; break; From c93c91013cf40478983771938eb2588c77c621e5 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 22:17:36 -0400 Subject: [PATCH 381/466] Do what clang says or else --- qtfred/src/ui/dialogs/VariableDialog.cpp | 50 ++++++++++++------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 150fb957fe0..0fda381a4b2 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1189,7 +1189,7 @@ void VariableDialog::applyModel() ui->variablesTable->item(x, 0)->setText(variables[x][0].c_str()); ui->variablesTable->item(x, 0)->setFlags(ui->variablesTable->item(x, 0)->flags() | Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem(variables[x][0].c_str()); + auto item = new QTableWidgetItem(variables[x][0].c_str()); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->variablesTable->setItem(x, 0, item); } @@ -1208,7 +1208,7 @@ void VariableDialog::applyModel() ui->variablesTable->item(x, 1)->setText(variables[x][1].c_str()); ui->variablesTable->item(x, 1)->setFlags(ui->variablesTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem(variables[x][1].c_str()); + auto item = new QTableWidgetItem(variables[x][1].c_str()); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->variablesTable->setItem(x, 1, item); } @@ -1217,7 +1217,7 @@ void VariableDialog::applyModel() ui->variablesTable->item(x, 2)->setText(variables[x][2].c_str()); ui->variablesTable->item(x, 2)->setFlags(ui->variablesTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem(variables[x][2].c_str()); + auto item = new QTableWidgetItem(variables[x][2].c_str()); ui->variablesTable->setItem(x, 2, item); ui->variablesTable->item(x, 2)->setFlags(item->flags() & ~Qt::ItemIsEditable); } @@ -1227,7 +1227,7 @@ void VariableDialog::applyModel() if (ui->variablesTable->item(x, 0)){ ui->variablesTable->item(x, 0)->setText("Add Variable ..."); } else { - QTableWidgetItem* item = new QTableWidgetItem("Add Variable ..."); + auto item = new QTableWidgetItem("Add Variable ..."); ui->variablesTable->setItem(x, 0, item); } @@ -1235,7 +1235,7 @@ void VariableDialog::applyModel() ui->variablesTable->item(x, 1)->setFlags(ui->variablesTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); ui->variablesTable->item(x, 1)->setText(""); } else { - QTableWidgetItem* item = new QTableWidgetItem(""); + auto item = new QTableWidgetItem(""); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->variablesTable->setItem(x, 1, item); } @@ -1244,7 +1244,7 @@ void VariableDialog::applyModel() ui->variablesTable->item(x, 2)->setFlags(ui->variablesTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); ui->variablesTable->item(x, 2)->setText(""); } else { - QTableWidgetItem* item = new QTableWidgetItem(""); + auto item = new QTableWidgetItem(""); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->variablesTable->setItem(x, 2, item); } @@ -1270,7 +1270,7 @@ void VariableDialog::applyModel() ui->containersTable->item(x, 0)->setText(containers[x][0].c_str()); ui->containersTable->item(x, 0)->setFlags(ui->containersTable->item(x, 0)->flags() | Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem(containers[x][0].c_str()); + auto item = new QTableWidgetItem(containers[x][0].c_str()); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containersTable->setItem(x, 0, item); } @@ -1283,14 +1283,14 @@ void VariableDialog::applyModel() if (ui->containersTable->item(x, 1)){ ui->containersTable->item(x, 1)->setText(containers[x][1].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(containers[x][1].c_str()); + auto item = new QTableWidgetItem(containers[x][1].c_str()); ui->containersTable->setItem(x, 1, item); } if (ui->containersTable->item(x, 2)){ ui->containersTable->item(x, 2)->setText(containers[x][2].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(containers[x][2].c_str()); + auto item = new QTableWidgetItem(containers[x][2].c_str()); ui->containersTable->setItem(x, 2, item); } } @@ -1314,7 +1314,7 @@ void VariableDialog::applyModel() if (ui->containersTable->item(x, 0)){ ui->containersTable->item(x, 0)->setText("Add Container ..."); } else { - QTableWidgetItem* item = new QTableWidgetItem("Add Container ..."); + auto item = new QTableWidgetItem("Add Container ..."); ui->containersTable->setItem(x, 0, item); } @@ -1322,7 +1322,7 @@ void VariableDialog::applyModel() ui->containersTable->item(x, 1)->setFlags(ui->containersTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); ui->containersTable->item(x, 1)->setText(""); } else { - QTableWidgetItem* item = new QTableWidgetItem(""); + auto item = new QTableWidgetItem(""); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->containersTable->setItem(x, 1, item); } @@ -1331,7 +1331,7 @@ void VariableDialog::applyModel() ui->containersTable->item(x, 2)->setFlags(ui->containersTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); ui->containersTable->item(x, 2)->setText(""); } else { - QTableWidgetItem* item = new QTableWidgetItem(""); + auto item = new QTableWidgetItem(""); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->containersTable->setItem(x, 2, item); } @@ -1618,7 +1618,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText(strings[x].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(strings[x].c_str()); + auto item = new QTableWidgetItem(strings[x].c_str()); ui->containerContentsTable->setItem(x, 0, item); } @@ -1642,7 +1642,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x, 1)->setText(""); ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem(""); + auto item = new QTableWidgetItem(""); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } @@ -1658,7 +1658,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText(std::to_string(numbers[x]).c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); + auto item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); ui->containerContentsTable->setItem(x, 0, item); } @@ -1693,7 +1693,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x, 1)->setText(""); ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem(""); + auto item = new QTableWidgetItem(""); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } @@ -1704,7 +1704,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x, 0)->setText("Add item ..."); ui->containerContentsTable->item(x, 0)->setFlags(ui->containerContentsTable->item(x, 0)->flags() | Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + auto item = new QTableWidgetItem("Add item ..."); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 0, item); } @@ -1713,7 +1713,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x, 1)->setText(""); ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem(""); + auto item = new QTableWidgetItem(""); item->setFlags(item->flags() & ~Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } @@ -1748,7 +1748,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText(keys[x].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(keys[x].c_str()); + auto item = new QTableWidgetItem(keys[x].c_str()); ui->containerContentsTable->setItem(x, 0, item); } @@ -1757,7 +1757,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x, 1)->setText(strings[x].c_str()); ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem(strings[x].c_str()); + auto item = new QTableWidgetItem(strings[x].c_str()); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } @@ -1773,7 +1773,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) if (ui->containerContentsTable->item(x, 0)){ ui->containerContentsTable->item(x, 0)->setText(keys[x].c_str()); } else { - QTableWidgetItem* item = new QTableWidgetItem(keys[x].c_str()); + auto item = new QTableWidgetItem(keys[x].c_str()); ui->containerContentsTable->setItem(x, 0, item); } @@ -1782,7 +1782,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x, 1)->setText(std::to_string(numbers[x]).c_str()); ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); + auto item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } @@ -1791,7 +1791,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x, 1)->setText(""); ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem(""); + auto item = new QTableWidgetItem(""); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } @@ -1803,7 +1803,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x, 0)->setText("Add item ..."); ui->containerContentsTable->item(x, 0)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + auto item = new QTableWidgetItem("Add item ..."); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 0, item); } @@ -1812,7 +1812,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x, 1)->setText("Add item ..."); ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); } else { - QTableWidgetItem* item = new QTableWidgetItem("Add item ..."); + auto item = new QTableWidgetItem("Add item ..."); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->containerContentsTable->setItem(x, 1, item); } From 0241c9f4b932908b93abb0494912c34fdf35a797 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 22:25:34 -0400 Subject: [PATCH 382/466] Scare off DeMorgan --- qtfred/src/ui/dialogs/VariableDialog.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 0fda381a4b2..93d638ca4da 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1628,11 +1628,11 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x,0)->setSelected(true); // more than one item and not already at the top of the list. - if (!(x > 0 && x < static_cast(strings.size()))){ + if (x <= 0 || x >= static_cast(strings.size())){ ui->shiftItemUpButton->setEnabled(false); } - if (!(x > -1 && x < static_cast(strings.size()) - 1)){ + if (x <= -1 || x >= static_cast(strings.size()) - 1)){ ui->shiftItemDownButton->setEnabled(false); } } @@ -1678,11 +1678,11 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->containerContentsTable->item(x,0)->setSelected(true); // more than one item and not already at the top of the list. - if (!(x > 0 && x < static_cast(numbers.size()))){ + if (x <= 0 || x >= static_cast(numbers.size())){ ui->shiftItemUpButton->setEnabled(false); } - if (!(x > -1 && x < static_cast(numbers.size()) - 1)){ + if (x <= -1 || x >= static_cast(numbers.size()) - 1){ ui->shiftItemDownButton->setEnabled(false); } } From 552817b8e12333ff775692f38b9aaa6d9acc7dc0 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 22:29:30 -0400 Subject: [PATCH 383/466] Missing Parentheses --- qtfred/src/ui/dialogs/VariableDialog.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 93d638ca4da..88448d75792 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -1632,7 +1632,7 @@ void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) ui->shiftItemUpButton->setEnabled(false); } - if (x <= -1 || x >= static_cast(strings.size()) - 1)){ + if (x <= -1 || x >= static_cast(strings.size()) - 1){ ui->shiftItemDownButton->setEnabled(false); } } From 81656e8dffa6d6747d24217c541592dcd27bd3d4 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 22:37:04 -0400 Subject: [PATCH 384/466] Add back missing closing markup --- qtfred/ui/FredView.ui | 1 + 1 file changed, 1 insertion(+) diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index 954b00c769b..a26347dc005 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -1522,6 +1522,7 @@ Shift+V + Jump &Nodes From 3a7269275e01f905c7499b9eabb58a2400b46c6e Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 22:58:10 -0400 Subject: [PATCH 385/466] blarg --- qtfred/ui/FredView.ui | 1 + 1 file changed, 1 insertion(+) diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index a26347dc005..d927e9acdfd 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -1522,6 +1522,7 @@ Shift+V + From f15a6bc3e80ff88596bbef0f06fd81b89b4492aa Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 23:37:11 -0400 Subject: [PATCH 386/466] Handle more clang warnings --- .../mission/dialogs/VariableDialogModel.cpp | 20 ++++++++----------- .../src/mission/dialogs/VariableDialogModel.h | 8 ++++---- qtfred/src/ui/dialogs/VariableDialog.cpp | 9 +++------ qtfred/src/ui/dialogs/VariableDialog.h | 10 +++++----- 4 files changed, 20 insertions(+), 27 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 9ebb8bd4150..11499370847 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -399,11 +399,7 @@ void VariableDialogModel::initializeData() } } } else { - if (any(container.type & ContainerType::STRING_KEYS)){ - newContainer.stringKeys = true; - } else { - newContainer.stringKeys = false; - } + newContainer.stringKeys = any(container.type & ContainerType::STRING_KEYS); for (const auto& item : container.map_data){ newContainer.keys.push_back(item.first); @@ -412,7 +408,7 @@ void VariableDialogModel::initializeData() newContainer.stringValues.push_back(item.second); newContainer.numberValues.push_back(0); } else { - newContainer.stringValues.push_back(""); + newContainer.stringValues.emplace_back(); try{ newContainer.numberValues.push_back(std::stoi(item.second)); @@ -753,7 +749,7 @@ bool VariableDialogModel::removeVariable(int index, bool toDelete) if (_deleteWarningCount < 2){ SCP_string question = "Are you sure you want to delete this variable? Any references to it will have to be changed."; - SCP_string info = ""; + SCP_string info; if (!confirmAction(question, info)){ --_deleteWarningCount; @@ -1264,7 +1260,7 @@ SCP_string VariableDialogModel::addContainer() return _containerItems.back().name; } -SCP_string VariableDialogModel::addContainer(SCP_string nameIn) +SCP_string VariableDialogModel::addContainer(const SCP_string& nameIn) { _containerItems.emplace_back(); _containerItems.back().name = nameIn.substr(0, TOKEN_LENGTH - 1); @@ -1333,7 +1329,7 @@ bool VariableDialogModel::removeContainer(int index, bool toDelete) if (_deleteWarningCount < 3){ SCP_string question = "Are you sure you want to delete this container? Any references to it will have to be changed."; - SCP_string info = ""; + SCP_string info; if (!confirmAction(question, info)){ return container->deleted; @@ -1589,7 +1585,7 @@ bool VariableDialogModel::removeListItem(int containerIndex, int index) if (_deleteWarningCount < 3){ SCP_string question = "Are you sure you want to delete this list item? This can't be undone."; - SCP_string info = ""; + SCP_string info; if (!confirmAction(question, info)){ --_deleteWarningCount; @@ -1792,7 +1788,7 @@ bool VariableDialogModel::removeMapItem(int index, int itemIndex) // double check that we want to delete if (_deleteWarningCount < 3){ SCP_string question = "Are you sure you want to delete this map item? This can't be undone."; - SCP_string info = ""; + SCP_string info; if (!confirmAction(question, info)){ --_deleteWarningCount; @@ -2234,7 +2230,7 @@ const SCP_vector> VariableDialogModel::getContainerNam return outStrings; } -void VariableDialogModel::setTextMode(int modeIn) { _textMode = modeIn;} +static void VariableDialogModel::setTextMode(int modeIn) { _textMode = modeIn;} void VariableDialogModel::sortMap(int index) { diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 5fcb15f47c5..bb07414933b 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -28,7 +28,7 @@ struct containerInfo { int flags = 0; // this will allow us to look up the original values used in the mission previously. - SCP_string originalName = ""; + SCP_string originalName; // I found out that keys could be strictly typed as numbers *after* finishing the majority of the model.... // So I am just going to store numerical keys as strings and use a bool to differentiate. @@ -97,7 +97,7 @@ class VariableDialogModel : public AbstractDialogModel { bool setContainerEternalFlag(int index, bool eternal); SCP_string addContainer(); - SCP_string addContainer(SCP_string nameIn); + SCP_string addContainer(const SCP_string& nameIn); SCP_string copyContainer(int index); SCP_string changeContainerName(int index, const SCP_string& newName); bool removeContainer(int index, bool toDelete); @@ -131,7 +131,7 @@ class VariableDialogModel : public AbstractDialogModel { const SCP_vector> getVariableValues(); const SCP_vector> getContainerNames(); - void setTextMode(int modeIn); + static void setTextMode(int modeIn); bool checkValidModel(); @@ -233,7 +233,7 @@ class VariableDialogModel : public AbstractDialogModel { // many of the controls in this editor can lead to drastic actions, so this will be very useful. - bool confirmAction(const SCP_string& question, const SCP_string& informativeText) + static bool confirmAction(const SCP_string& question, const SCP_string& informativeText) { QMessageBox msgBox; msgBox.setText(question.c_str()); diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 88448d75792..6e855917899 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -9,9 +9,7 @@ #include //#include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) : QDialog(parent), ui(new Ui::VariableEditorDialog()), _model(new VariableDialogModel(this, viewport)), _viewport(viewport) @@ -334,7 +332,7 @@ void VariableDialog::onVariablesTableUpdated() apply = true; // we had a successful rename. So update the variable we reference. - } else if (ret != "") { + } else if (!ret.empty()) { item->setText(ret.c_str()); _currentVariable = ret; } @@ -1869,5 +1867,4 @@ void VariableDialog::checkValidModel() } } // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file + diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 6d37610faee..16667b91aba 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -78,11 +78,11 @@ class VariableDialog : public QDialog { int getCurrentContainerItemRow(); bool _applyingModel = false; - SCP_string _currentVariable = ""; - SCP_string _currentVariableData = ""; - SCP_string _currentContainer = ""; - SCP_string _currentContainerItemCol1 = ""; - SCP_string _currentContainerItemCol2 = ""; + SCP_string _currentVariable; + SCP_string _currentVariableData; + SCP_string _currentContainer; + SCP_string _currentContainerItemCol1; + SCP_string _currentContainerItemCol2; void reject() override { From 7b9b237a66061670aec2f48a7d7a30db702eeb21 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Wed, 20 Aug 2025 23:45:42 -0400 Subject: [PATCH 387/466] No clever commit message this time How long must I attempt to satisfy the endless hunger of clang --- .../mission/dialogs/VariableDialogModel.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 11499370847..f4611dd461d 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1417,8 +1417,8 @@ std::pair VariableDialogModel::addMapItem(int index) sprintf(newKey, "%i", count); } - for (int x = 0; x < static_cast(container->keys.size()); ++x) { - if (container->keys[x] == newKey){ + for (const auto& key : container->keys) { + if (key == newKey){ conflict = true; break; } @@ -1474,8 +1474,8 @@ std::pair VariableDialogModel::addMapItem(int index, con sprintf(newKey, "%i", count); } - for (int x = 0; x < static_cast(container->keys.size()); ++x) { - if (container->keys[x] == newKey){ + for (const auto& key : container->keys) { + if (key == newKey){ conflict = true; break; } @@ -1640,8 +1640,8 @@ std::pair VariableDialogModel::copyMapItem(int index, in do { found = false; - for (int y = 0; y < static_cast(container->keys.size()); ++y){ - if (container->keys[y] == newKey) { + for (const auto& key : container->keys){ + if (key == newKey) { found = true; break; } @@ -1683,8 +1683,8 @@ std::pair VariableDialogModel::copyMapItem(int index, in do { found = false; - for (int y = 0; y < static_cast(container->keys.size()); ++y){ - if (container->keys[y] == newKey) { + for (const auto& key : container->keys){ + if (key == newKey) { found = true; break; } @@ -2184,7 +2184,7 @@ const SCP_vector> VariableDialogModel::getContainerNam } if (item.list){ - type = listPrefix + type + listPostscript; + type += listPrefix + type + listPostscript; } else { From c21cbb3067baf7ba6885f032fe32954cef804389 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 21 Aug 2025 11:57:06 -0400 Subject: [PATCH 388/466] Handle shadowing --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index f4611dd461d..4d4f652b2ab 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -1474,8 +1474,8 @@ std::pair VariableDialogModel::addMapItem(int index, con sprintf(newKey, "%i", count); } - for (const auto& key : container->keys) { - if (key == newKey){ + for (const auto& current_key : container->keys) { + if (current_key == newKey){ conflict = true; break; } @@ -1640,8 +1640,8 @@ std::pair VariableDialogModel::copyMapItem(int index, in do { found = false; - for (const auto& key : container->keys){ - if (key == newKey) { + for (const auto& current_key : container->keys){ + if (current_key == newKey) { found = true; break; } @@ -1683,8 +1683,8 @@ std::pair VariableDialogModel::copyMapItem(int index, in do { found = false; - for (const auto& key : container->keys){ - if (key == newKey) { + for (const auto& current_key : container->keys){ + if (current_key == newKey) { found = true; break; } @@ -2230,7 +2230,7 @@ const SCP_vector> VariableDialogModel::getContainerNam return outStrings; } -static void VariableDialogModel::setTextMode(int modeIn) { _textMode = modeIn;} +void VariableDialogModel::setTextMode(int modeIn) { _textMode = modeIn;} void VariableDialogModel::sortMap(int index) { From 40b99248926ca208f4b097df75d3cd3e1b05a4f9 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 21 Aug 2025 23:39:40 -0400 Subject: [PATCH 389/466] More clangy --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 8 ++++---- qtfred/src/mission/dialogs/VariableDialogModel.h | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 4d4f652b2ab..c8ab1025927 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -254,7 +254,7 @@ bool VariableDialogModel::apply() // set of instructions for updating variables if (!variable.originalName.empty()) { - for (int i = 0; i < MAX_SEXP_VARIABLES; ++i) { + for (int i = 0; i < MAX_SEXP_VARIABLES; ++i) { // NOLINT(modernize-loop-convert) if (!stricmp(Sexp_variables[i].variable_name, variable.originalName.c_str())){ if (variable.deleted) { sexp_variable_delete(i); @@ -656,7 +656,7 @@ SCP_string VariableDialogModel::addNewVariable(SCP_string nameIn) } _variableItems.emplace_back(); - _variableItems.back().name.substr(0, TOKEN_LENGTH - 1) = nameIn; + _variableItems.back().name.substr(0, TOKEN_LENGTH - 1) = std::move(nameIn); return _variableItems.back().name; } @@ -1440,7 +1440,7 @@ std::pair VariableDialogModel::addMapItem(int index) ret.second = "0"; } - container->stringValues.push_back(""); + container->stringValues.emplace_back(); container->numberValues.push_back(0); sortMap(index); @@ -1705,7 +1705,7 @@ std::pair VariableDialogModel::copyMapItem(int index, in container->keys.push_back(newKey); container->numberValues.push_back(copyValue); - container->stringValues.push_back(""); + container->stringValues.emplace_back(); SCP_string temp; sprintf(temp, "%i", copyValue); diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index bb07414933b..3e2cea49c35 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -180,9 +180,9 @@ class VariableDialogModel : public AbstractDialogModel { } containerInfo* lookupContainerByName(const SCP_string& name){ - for (int x = 0; x < static_cast(_containerItems.size()); ++x) { - if (_containerItems[x].name == name) { - return &_containerItems[x]; + for (auto& container : _containerItems) { + if (container.name == name) { + return &container; } } @@ -201,9 +201,9 @@ class VariableDialogModel : public AbstractDialogModel { SCP_string* lookupContainerKeyByName(int containerIndex, const SCP_string& keyIn){ if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ - for (auto key = _containerItems[containerIndex].keys.begin(); key != _containerItems[containerIndex].keys.end(); ++key) { - if (*key == keyIn){ - return &(*key); + for (auto& key : _containerItems[containerIndex].keys) { + if (key == keyIn){ + return &key; } } } From 55efbe73b3edf440c5eccb16a709aaf22258b049 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 21 Aug 2025 23:41:48 -0400 Subject: [PATCH 390/466] Forgot that VS doesn't have auto save --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 5 +---- qtfred/src/mission/dialogs/VariableDialogModel.h | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index c8ab1025927..a8b6ea238e9 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -2325,10 +2325,7 @@ SCP_string VariableDialogModel::trimIntegerString(SCP_string source) switch (c) { // ignore leading zeros. If all digits are zero, this will be handled elsewhere case '0': - if (foundNonZero) - return true; - else - return false; + return foundNonZero; case '1': case '2': case '3': diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 3e2cea49c35..0839fe2e779 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -127,10 +127,10 @@ class VariableDialogModel : public AbstractDialogModel { void swapKeyAndValues(int index); bool safeToAlterContainer(int index); - bool safeToAlterContainer(const containerInfo& containerItem); + static bool safeToAlterContainer(const containerInfo& containerItem); - const SCP_vector> getVariableValues(); - const SCP_vector> getContainerNames(); + SCP_vector> getVariableValues(); + SCP_vector> getContainerNames(); static void setTextMode(int modeIn); bool checkValidModel(); From 3725f27a5c40c894e5db96fee2a7c4df01d12e12 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Thu, 21 Aug 2025 23:54:18 -0400 Subject: [PATCH 391/466] Match header --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index a8b6ea238e9..8794dbf0fd1 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -2044,7 +2044,7 @@ const SCP_vector& VariableDialogModel::getNumberValues(int index) return container->numberValues; } -const SCP_vector> VariableDialogModel::getVariableValues() +SCP_vector> VariableDialogModel::getVariableValues() { SCP_vector> outStrings; @@ -2073,7 +2073,7 @@ const SCP_vector> VariableDialogModel::getVariableValu return outStrings; } -const SCP_vector> VariableDialogModel::getContainerNames() +SCP_vector> VariableDialogModel::getContainerNames() { // This logic makes the string which we use to display the type of the container, based on the specific mode we're using. SCP_string listPrefix; From b1247042ac7d6236dc9120aa22adaa16c599d0ab Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:18:30 +0100 Subject: [PATCH 392/466] QTFred: Alt Ships Class dialogs (#6959) * Add Files and UI * Initial Setup of dialog * Finish Dialog * Obey Rule of 3(Possibly) Why Cant the compiler do this? add const * Fix use of default keyword * bit more * Remove unused Vars * Missed one * Clang work * Remove future proofing as the compiler doesnt like it * Make static/ Apply de morgans * Lets try this one * Revert to inital and supress warning Also switch to Assertions --- code/mission/missionparse.h | 26 +- qtfred/source_groups.cmake | 5 + .../ShipEditor/ShipAltShipClassModel.cpp | 120 +++++++ .../ShipEditor/ShipAltShipClassModel.h | 38 +++ .../dialogs/ShipEditor/ShipAltShipClass.cpp | 323 ++++++++++++++++++ .../ui/dialogs/ShipEditor/ShipAltShipClass.h | 77 +++++ .../dialogs/ShipEditor/ShipEditorDialog.cpp | 4 +- .../ui/dialogs/ShipEditor/ShipEditorDialog.h | 1 + qtfred/ui/ShipAltShipClass.ui | 162 +++++++++ 9 files changed, 753 insertions(+), 3 deletions(-) create mode 100644 qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp create mode 100644 qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.h create mode 100644 qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp create mode 100644 qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.h create mode 100644 qtfred/ui/ShipAltShipClass.ui diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index ae05c7ab121..07ecbb85ed9 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -352,11 +352,33 @@ extern SCP_vector Fred_texture_replacements; // which ships have had the "immobile" flag migrated to "don't-change-position" and "don't-change-orientation" extern SCP_unordered_set Fred_migrated_immobile_ships; -typedef struct alt_class { +struct alt_class { int ship_class; int variable_index; // if set allows the class to be set by a variable bool default_to_this_class; -}alt_class; + alt_class() + { + ship_class = -1; + variable_index = -1; + default_to_this_class = false; + } + alt_class(const alt_class& a) { + ship_class = a.ship_class; + variable_index = a.variable_index; + default_to_this_class = a.default_to_this_class; + } + bool operator==(const alt_class& a) const + { + return (ship_class == a.ship_class && variable_index == a.variable_index && default_to_this_class == a.default_to_this_class); + } + alt_class& operator=(const alt_class& a) + { + ship_class = a.ship_class; + variable_index = a.variable_index; + default_to_this_class = a.default_to_this_class; + return *this; + } +}; // a parse object // information from a $OBJECT: definition is read into this struct to diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 9214da1b029..f8b07e610a2 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -98,6 +98,8 @@ add_file_folder("Source/Mission/Dialogs/ShipEditor" src/mission/dialogs/ShipEditor/ShipPathsDialogModel.h src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp + src/mission/dialogs/ShipEditor/ShipAltShipClassModel.h + src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp ) add_file_folder("Source/UI" @@ -176,6 +178,8 @@ add_file_folder("Source/UI/Dialogs/ShipEditor" src/ui/dialogs/ShipEditor/ShipPathsDialog.cpp src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp + src/ui/dialogs/ShipEditor/ShipAltShipClass.h + src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h ) @@ -246,6 +250,7 @@ add_file_folder("UI" ui/ShipTBLViewer.ui ui/ShipPathsDialog.ui ui/ShipCustomWarpDialog.ui + ui/ShipAltShipClass.ui ui/ShipWeaponsDialog.ui ) diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp new file mode 100644 index 00000000000..14d9353ccca --- /dev/null +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp @@ -0,0 +1,120 @@ +#include "ShipAltShipClassModel.h" + +#include "ship/ship.h" +namespace fso::fred::dialogs { +ShipAltShipClassModel::ShipAltShipClassModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + initializeData(); +} + +bool ShipAltShipClassModel::apply() +{ + // TODO: Add extra validation here + for (auto& pool_class : alt_class_pool) { + if (pool_class.ship_class == -1 && pool_class.variable_index == -1) { + _viewport->dialogProvider->showButtonDialog(DialogType::Warning, + "Warning", + "Class Can\'t be set by both ship class and by variable simultaneously.", + {DialogButton::Ok}); + return false; + } + } + for (int i = 0; i < _num_selected_ships; i++) { + Ships[_m_selected_ships[i]].s_alt_classes = alt_class_pool; + } + return true; +} + +void ShipAltShipClassModel::reject() {} + +SCP_vector ShipAltShipClassModel::get_pool() const +{ + return alt_class_pool; +} + +SCP_vector> ShipAltShipClassModel::get_classes() +{ + // Fill the ship classes combo box + SCP_vector> _m_set_from_ship_class; + std::pair classData; + // Add the default entry if we need one followed by all the ship classes + classData.first = "Set From Variable"; + classData.second = -1; + _m_set_from_ship_class.push_back(classData); + for (auto it = Ship_info.cbegin(); it != Ship_info.cend(); ++it) { + if (!(it->flags[Ship::Info_Flags::Player_ship])) { + continue; + } + classData.first = it->name; + classData.second = std::distance(Ship_info.cbegin(), it); + _m_set_from_ship_class.push_back(classData); + } + + return _m_set_from_ship_class; +} + +SCP_vector> ShipAltShipClassModel::get_variables() +{ + // Fill the variable combo box + SCP_vector> _m_set_from_variables; + std::pair variableData; + variableData.first = "Set From Ship Class"; + variableData.second = -1; + _m_set_from_variables.push_back(variableData); + for (int i = 0; i < MAX_SEXP_VARIABLES; i++) { + if (Sexp_variables[i].type & SEXP_VARIABLE_STRING) { + std::ostringstream oss; + SCP_string buff = Sexp_variables[i].variable_name; + oss << buff << "[" << Sexp_variables[i].text << "]"; + buff = oss.str(); + variableData.first = buff; + variableData.second = i; + _m_set_from_variables.push_back(variableData); + //_string_variables.push_back(variable); + // _string_variables[0].get().type = 1234; + } + } + return _m_set_from_variables; +} +void ShipAltShipClassModel::sync_data(const SCP_vector& new_pool) { + if (new_pool == alt_class_pool) { + return; + } else { + alt_class_pool = new_pool; + set_modified(); + } +} +void ShipAltShipClassModel::initializeData() +{ + _num_selected_ships = 0; + _m_selected_ships.clear(); + // have we got multiple selected ships? + object* objp = GET_FIRST(&obj_used_list); + while (objp != END_OF_LIST(&obj_used_list)) { + if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { + if (objp->flags[Object::Object_Flags::Marked]) { + _m_selected_ships.push_back(objp->instance); + _num_selected_ships++; + } + } + objp = GET_NEXT(objp); + } + + Assertion(_num_selected_ships > 0, "No Ships Selected"); + // Assert(Objects[cur_object_index].flags[Object::Object_Flags::Marked]); + + alt_class_pool.clear(); + objp = GET_FIRST(&obj_used_list); + while (objp != END_OF_LIST(&obj_used_list)) { + if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { + if (objp->flags[Object::Object_Flags::Marked]) { + alt_class_pool = Ships[objp->instance].s_alt_classes; + break; + } + } + objp = GET_NEXT(objp); + } +} + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.h new file mode 100644 index 00000000000..b9ac7f1c4fb --- /dev/null +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipAltShipClassModel.h @@ -0,0 +1,38 @@ +#pragma once +#include "../AbstractDialogModel.h" +namespace fso::fred::dialogs { +/** + * @brief Model for QtFRED's Alt Ship Class dialog + */ +class ShipAltShipClassModel : public AbstractDialogModel { + private: + /** + * @brief Initialises data for the model + */ + void initializeData(); + + SCP_vector alt_class_pool; + + int _num_selected_ships = 0; + + SCP_vector _m_selected_ships; + + SCP_vector _m_alt_class_list; + + public: + /** + * @brief Constructor + * @param [in] parent The parent dialog. + * @param [in] viewport The viewport this dialog is attacted to. + */ + ShipAltShipClassModel(QObject* parent, EditorViewport* viewport); + bool apply() override; + void reject() override; + + SCP_vector get_pool() const; + + static SCP_vector> get_classes(); + static SCP_vector> get_variables(); + void sync_data(const SCP_vector&); +}; +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp new file mode 100644 index 00000000000..2aad1084859 --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.cpp @@ -0,0 +1,323 @@ +#include "ShipAltShipClass.h" + +#include "ui_ShipAltShipClass.h" + +#include +#include + +#include + +namespace fso::fred::dialogs { +ShipAltShipClass::ShipAltShipClass(QDialog* parent, EditorViewport* viewport) + : QDialog(parent), ui(new Ui::ShipAltShipClass()), + _model(new ShipAltShipClassModel(this, viewport)), _viewport(viewport) +{ + this->setFocus(); + ui->setupUi(this); + initUI(); +} + +ShipAltShipClass::~ShipAltShipClass() = default; + +void ShipAltShipClass::accept() +{ // If apply() returns true, close the dialog + sync_data(); + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void ShipAltShipClass::reject() +{ // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + sync_data(); + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close +} + +void ShipAltShipClass::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window +} + +void ShipAltShipClass::on_buttonBox_accepted() +{ + accept(); +} +void ShipAltShipClass::on_buttonBox_rejected() +{ + reject(); +} + +void ShipAltShipClass::on_addButton_clicked() +{ + auto item = generate_item(ui->shipCombo->currentData(Qt::UserRole).toInt(), + ui->variableCombo->currentData(Qt::UserRole).toInt(), + ui->defaultCheckbox->isChecked()); + if (item != nullptr) { + dynamic_cast(ui->classList->model())->appendRow(item); + } +} + +void ShipAltShipClass::on_insertButton_clicked() +{ + auto current = ui->classList->currentIndex(); + if (current.isValid()) { + auto item = generate_item(ui->shipCombo->currentData(Qt::UserRole).toInt(), + ui->variableCombo->currentData(Qt::UserRole).toInt(), + ui->defaultCheckbox->isChecked()); + if (item != nullptr) { + dynamic_cast(ui->classList->model())->insertRow(current.row(), item); + ui->classList->selectionModel()->clearSelection(); + ui->classList->setCurrentIndex(QModelIndex()); + } + } else { + on_addButton_clicked(); + } +} + +void ShipAltShipClass::on_deleteButton_clicked() +{ + auto current = ui->classList->currentIndex(); + if (current.isValid()) { + dynamic_cast(ui->classList->model())->removeRow(current.row()); + } +} + +void ShipAltShipClass::on_upButton_clicked() +{ + auto current = ui->classList->currentIndex(); + if (current.isValid()) { + int row = current.row(); + if (row != 0) { + auto oldrow = dynamic_cast(ui->classList->model())->takeRow(row); + dynamic_cast(ui->classList->model())->insertRow(row - 1, oldrow.first()); + ui->classList->selectionModel()->clearSelection(); + ui->classList->setCurrentIndex(QModelIndex()); + } + } +} + +void ShipAltShipClass::on_downButton_clicked() +{ + auto current = ui->classList->currentIndex(); + if (current.isValid()) { + int row = current.row(); + if (row != ui->classList->model()->rowCount() - 1) { + auto oldrow = dynamic_cast(ui->classList->model())->takeRow(row); + dynamic_cast(ui->classList->model())->insertRow(row + 1, oldrow.first()); + ui->classList->selectionModel()->clearSelection(); + ui->classList->setCurrentIndex(QModelIndex()); + } + } +} + +void ShipAltShipClass::on_shipCombo_currentIndexChanged(int index) +{ + auto current = ui->classList->currentIndex(); + if (current.isValid()) { + if (ui->shipCombo->itemData(index, Qt::UserRole).toInt() == -1 && ui->variableCombo->model()->rowCount() > 1) { + if (ui->variableCombo->currentData(Qt::UserRole) != -1) { + on_variableCombo_currentIndexChanged(ui->variableCombo->currentIndex()); + } else { + on_variableCombo_currentIndexChanged(1); + } + } else { + QString classname = generate_name(ui->shipCombo->itemData(index, Qt::UserRole).toInt(), -1); + ui->classList->model()->setData(current, classname, Qt::DisplayRole); + ui->classList->model()->setData(current, ui->shipCombo->itemData(index, Qt::UserRole), Qt::UserRole + 1); + ui->classList->model()->setData(current, -1, Qt::UserRole + 2); + } + } + ui->classList->selectionModel()->clearSelection(); + ui->classList->setCurrentIndex(QModelIndex()); +} + +void ShipAltShipClass::on_variableCombo_currentIndexChanged(int index) +{ + auto current = ui->classList->currentIndex(); + if (current.isValid()) { + if (ui->variableCombo->itemData(index, Qt::UserRole).toInt() == -1) { + if (ui->shipCombo->currentData(Qt::UserRole) != -1) { + on_shipCombo_currentIndexChanged(ui->shipCombo->currentIndex()); + } else { + on_shipCombo_currentIndexChanged(1); + } + } else { + int ship_class = + ship_info_lookup(Sexp_variables[ui->variableCombo->itemData(index, Qt::UserRole).toInt()].text); + QString classname = generate_name(ship_class, ui->variableCombo->itemData(index, Qt::UserRole).toInt()); + ui->classList->model()->setData(current, classname, Qt::DisplayRole); + ui->classList->model()->setData(current, ship_class, Qt::UserRole + 1); + ui->classList->model()->setData(current, + ui->variableCombo->itemData(index, Qt::UserRole), + Qt::UserRole + 2); + } + } + ui->classList->selectionModel()->clearSelection(); + ui->classList->setCurrentIndex(QModelIndex()); +} + +void ShipAltShipClass::on_defaultCheckbox_toggled(bool toggled) +{ + auto current = ui->classList->currentIndex(); + if (current.isValid()) { + ui->classList->model()->setData(current, toggled, Qt::UserRole); + } +} +void ShipAltShipClass::initUI() +{ + alt_pool = new QStandardItemModel(); + for (auto& alt_class : _model->get_pool()) { + auto item = generate_item(alt_class.ship_class, alt_class.variable_index, alt_class.default_to_this_class); + if (item != nullptr) { + alt_pool->appendRow(item); + } + } + + ui->classList->setModel(alt_pool); + connect(ui->classList->selectionModel(), + &QItemSelectionModel::currentChanged, + this, + &ShipAltShipClass::classListChanged); + + auto ship_pool = new QStandardItemModel(); + for (auto& ship : _model->get_classes()) { + QString classname = ship.first.c_str(); + auto item = new QStandardItem(classname); + item->setData(ship.second, Qt::UserRole); + ship_pool->appendRow(item); + } + auto shipproxyModel = new InverseSortFilterProxyModel(this); + shipproxyModel->setSourceModel(ship_pool); + ui->shipCombo->setModel(shipproxyModel); + auto variable_pool = new QStandardItemModel(); + for (auto& variable : _model->get_variables()) { + QString classname = variable.first.c_str(); + auto item = new QStandardItem(classname); + item->setData(variable.second, Qt::UserRole); + variable_pool->appendRow(item); + } + ui->variableCombo->setModel(variable_pool); + updateUI(); +} + +void ShipAltShipClass::updateUI() +{ + util::SignalBlockers blockers(this); // block signals while we set up the UI + QModelIndexList* list; + auto current = ui->classList->currentIndex(); + auto ship_class = -1; + auto variable = -1; + auto default_ship = false; + if (current.isValid()) { + ship_class = current.data(Qt::UserRole + 1).toInt(); + variable = current.data(Qt::UserRole + 2).toInt(); + default_ship = current.data(Qt::UserRole).toBool(); + } + if (ui->variableCombo->model()->rowCount() <= 1) { + dynamic_cast(ui->shipCombo->model())->setFilterFixedString("Set From Variable"); + } + list = new QModelIndexList( + ui->shipCombo->model()->match(ui->shipCombo->model()->index(0, 0), Qt::UserRole, ship_class)); + if (!list->empty()) { + ui->shipCombo->setCurrentIndex(list->first().row()); + } else { + if (ui->classList->model()->rowCount() != 0 && ship_class != -1) { + _viewport->dialogProvider->showButtonDialog(DialogType::Error, + "Error", + "Illegal ship class.\n Resetting to -1", + {DialogButton::Ok}); + } + ui->shipCombo->setCurrentIndex(0); + } + + auto varlist = new QModelIndexList( + ui->variableCombo->model()->match(ui->variableCombo->model()->index(0, 0), Qt::UserRole, variable)); + if (!varlist->empty()) { + ui->variableCombo->setCurrentIndex(varlist->first().row()); + } else { + if (ui->classList->model()->rowCount() != 0) { + _viewport->dialogProvider->showButtonDialog(DialogType::Error, + "Error", + "Illegal variable index.\n Resetting to -1", + {DialogButton::Ok}); + } + ui->variableCombo->setCurrentIndex(0); + } + + if (ui->variableCombo->model()->rowCount() <= 1) { + ui->variableCombo->setEnabled(false); + } + ui->defaultCheckbox->setChecked(default_ship); +} +void ShipAltShipClass::classListChanged(const QModelIndex& current) +{ + SCP_UNUSED(current); + updateUI(); +} +QStandardItem* ShipAltShipClass::generate_item(const int classid, const int variable, const bool default_ship) +{ + QString classname = generate_name(classid, variable); + if (!classname.isEmpty()) { + auto item = new QStandardItem(classname); + item->setData(default_ship, Qt::UserRole); + item->setData(classid, Qt::UserRole + 1); + item->setData(variable, Qt::UserRole + 2); + return item; + } else { + Warning(LOCATION, + "Unable to generate item name.\n [%i] was the class id and [%i] the variable index.", + classid, + variable); + return nullptr; + } +} +QString ShipAltShipClass::generate_name(const int classid, const int variable) +{ + QString classname; + if (variable != -1) { + // NOLINTBEGIN(readability-simplify-boolean-expr) + Assertion(variable > -1 && variable < MAX_SEXP_VARIABLES, + "Variable index out of bounds!"); + Assertion(Sexp_variables[variable].type & SEXP_VARIABLE_STRING, "Variable type is not a string."); + // NOLINTEND(readability-simplify-boolean-expr) + classname = Sexp_variables[variable].variable_name; + classname = classname + '[' + Sexp_variables[variable].text + ']'; + } else { + if (classid >= 0 && classid < MAX_SHIP_CLASSES) { + classname = Ship_info[classid].name; + } else { + classname = "Invalid Ship Class"; + Warning(LOCATION, "Invalid Ship Class Index [%i]", classid); + } + } + return classname; +} +void ShipAltShipClass::sync_data() { + SCP_vector new_pool; + int n = ui->classList->model()->rowCount(); + for (int i = 0; i < n; i++) { + alt_class new_list_item; + new_list_item.default_to_this_class = + dynamic_cast(ui->classList->model())->index(i,0).data(Qt::UserRole).toInt(); + new_list_item.ship_class = + dynamic_cast(ui->classList->model())->index(i, 0).data(Qt::UserRole + 1).toInt(); + new_list_item.variable_index = + dynamic_cast(ui->classList->model())->index(i, 0).data(Qt::UserRole + 2).toInt(); + new_pool.push_back(new_list_item); + } + _model->sync_data(new_pool); +} +InverseSortFilterProxyModel::InverseSortFilterProxyModel(QObject* parent) : QSortFilterProxyModel(parent) {} +bool InverseSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const +{ + bool accept = QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent); + return !accept; +} +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.h b/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.h new file mode 100644 index 00000000000..e0ace44de64 --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipAltShipClass.h @@ -0,0 +1,77 @@ +#pragma once +#include + +#include +#include +#include +namespace fso::fred::dialogs { +namespace Ui { +class ShipAltShipClass; +} +/** + * @brief QtFRED's Alternate Ship Class Editor + */ + +class InverseSortFilterProxyModel : public QSortFilterProxyModel { + Q_OBJECT + public: + InverseSortFilterProxyModel(QObject* parent = nullptr); + + protected: + bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override; + +}; +class ShipAltShipClass : public QDialog { + Q_OBJECT + public: + /** + * @brief Constructor + * @param [in] parent The parent dialog. + * @param [in] viewport The viewport this dialog is attacted to. + */ + explicit ShipAltShipClass(QDialog* parent, EditorViewport* viewport); + ~ShipAltShipClass() override; + + void accept() override; + void reject() override; + + protected: + /** + * @brief Overides the Dialogs Close event to add a confermation dialog + * @param [in] *e The event. + */ + void closeEvent(QCloseEvent*) override; + + private: + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + void initUI(); + + /** + * @brief Populates the UI + */ + void updateUI(); + + QStandardItemModel* alt_pool; + + void classListChanged(const QModelIndex& current); + static QStandardItem* generate_item(const int classid, const int variable, const bool default_ship); + static QString generate_name(const int classid, const int variable); + + void sync_data(); + + private slots: // NOLINT(readability-redundant-access-specifiers) + void on_buttonBox_accepted(); + void on_buttonBox_rejected(); + void on_addButton_clicked(); + void on_insertButton_clicked(); + void on_deleteButton_clicked(); + void on_upButton_clicked(); + void on_downButton_clicked(); + void on_shipCombo_currentIndexChanged(int); + void on_variableCombo_currentIndexChanged(int); + void on_defaultCheckbox_toggled(bool); +}; +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index f3fccd36992..592dbf431f6 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -826,7 +826,9 @@ void ShipEditorDialog::on_playerShipButton_clicked() } void ShipEditorDialog::on_altShipClassButton_clicked() { - // TODO: altshipclassui + auto dialog = new dialogs::ShipAltShipClass(this, _viewport); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); } void ShipEditorDialog::on_prevButton_clicked() { diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h index 18592c81e38..96ceb66d27a 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h @@ -14,6 +14,7 @@ #include "ShipWeaponsDialog.h" #include "ShipPathsDialog.h" #include "ShipCustomWarpDialog.h" +#include "ShipAltShipClass.h" #include diff --git a/qtfred/ui/ShipAltShipClass.ui b/qtfred/ui/ShipAltShipClass.ui new file mode 100644 index 00000000000..67043811bf8 --- /dev/null +++ b/qtfred/ui/ShipAltShipClass.ui @@ -0,0 +1,162 @@ + + + fso::fred::dialogs::ShipAltShipClass + + + + 0 + 0 + 640 + 480 + + + + Alternate Ship Class Editor + + + true + + + + + + + + + + QAbstractItemView::NoEditTriggers + + + Qt::IgnoreAction + + + + + + + + + + 0 + 0 + + + + Up + + + + + + + + 0 + 0 + + + + Down + + + + + + + + + + + + + Add + + + + + + + Insert + + + + + + + Delete + + + + + + + + + + + + + + 0 + 0 + + + + Set From Ship + + + + + + + + + + + 0 + 0 + + + + Set From Variable + + + + + + + + + + Default To This Class + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + + From a12c1de012d6d27a1f5a2484dd0977c14cd126e5 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 22 Aug 2025 07:18:38 -0400 Subject: [PATCH 393/466] fix memory access error with active games list (#6964) During maintenance of the active games list it's possible for the list to be cleared before a stale item is removed leading to a access violation. So we need to confirm the list isn't empty when attemping to erase the stale entry. This also fixes a bug where the list entry index is incremented even when an entry is removed which can cause an improper selection index being used. --- code/network/multiui.cpp | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/code/network/multiui.cpp b/code/network/multiui.cpp index dc7280e8b64..4e63cd7f7cf 100644 --- a/code/network/multiui.cpp +++ b/code/network/multiui.cpp @@ -809,6 +809,10 @@ DCF(mj_remove, "Removes a multijoin game (multiplayer") // handle any gui details related to deleting this item multi_join_handle_item_cull(idx); + if (Active_games.empty()) { + return; + } + // delete the item SCP_list::iterator game = Active_games.begin(); std::advance(game, idx); @@ -1793,19 +1797,27 @@ void multi_join_list_page_down() void multi_join_cull_timeouts() { - // traverse through the entire list if any items exist - if(!Active_games.empty()){ - int i = 0; - for (auto game = Active_games.begin(); game != Active_games.end(); ++game) { - if (game->heard_from_timer.isValid() && (ui_timestamp_elapsed(game->heard_from_timer))) { + if (Active_games.empty()) { + return; + } - // handle any gui details related to deleting this item - multi_join_handle_item_cull(i); - - // delete the item - Active_games.erase(game); + // traverse through the entire list if any items exist + int i = 0; + for (auto game = Active_games.begin(); game != Active_games.end(); ++game) { + if (game->heard_from_timer.isValid() && (ui_timestamp_elapsed(game->heard_from_timer))) { + + // handle any gui details related to deleting this item + multi_join_handle_item_cull(i); + + // this list may have been cleared so check for it + if (Active_games.empty()) { + break; } - i++; + + // delete the item + Active_games.erase(game); + } else { + ++i; } } } @@ -1815,11 +1827,7 @@ void multi_join_handle_item_cull(int item_index) { Assertion((item_index >= 0) && (item_index < static_cast(Active_games.size())), "Tried to cull a multiplayer game that doesn't exist! Please report!"); - - //Get the item - SCP_list::iterator game = Active_games.begin(); - std::advance(game, item_index); - + // if this is the only item on the list, unset everything if(Active_games.size() == 1){ Multi_join_list_selected = -1; From 43e85dd8fd09cc524ce4160faf3790ce391e5629 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Fri, 22 Aug 2025 07:18:48 -0400 Subject: [PATCH 394/466] update libpcp to lastest main (#6963) This should fix build issues on ARM64 Windows. Note that the project name changed from libpcp to libpcpnatpmp and is in active development again after a multi-year period of inactivity. There are currently no versioned releases. This is hash b5fe5e2. --- code/CMakeLists.txt | 2 +- code/network/multi_portfwd.cpp | 2 +- lib/CMakeLists.txt | 2 +- lib/libpcp/CMakeLists.txt | 79 - lib/libpcp/src/net/pcp_socket.c | 345 ----- lib/libpcp/src/net/sock_ntop.c | 353 ----- lib/libpcp/src/pcp_api.c | 777 ---------- lib/libpcp/src/pcp_event_handler.c | 1363 ---------------- lib/libpcp/src/pcp_logger.h | 108 -- lib/libpcp/src/pcp_msg.c | 714 --------- lib/libpcp/src/pcp_utils.h | 250 --- lib/libpcp/src/windows/stdint.h | 249 --- lib/libpcpnatpmp/CMakeLists.txt | 72 + lib/libpcpnatpmp/COPYING | 22 + lib/{libpcp => libpcpnatpmp}/Makefile.am | 14 +- lib/libpcpnatpmp/README.md | 23 + .../src => libpcpnatpmp}/default_config.h | 2 +- .../include/pcpnatpmp.h} | 141 +- .../libpcpnatpmp.pc.in} | 8 +- .../src/net/findsaddr-udp.c | 69 +- .../src/net/findsaddr.h | 6 +- .../src/net/gateway.c | 529 +++---- .../src/net/gateway.h | 0 lib/libpcpnatpmp/src/net/pcp_socket.c | 567 +++++++ .../src/net/pcp_socket.h | 36 +- lib/libpcpnatpmp/src/net/sock_ntop.c | 346 +++++ lib/{libpcp => libpcpnatpmp}/src/net/unp.h | 29 +- lib/libpcpnatpmp/src/pcp_api.c | 769 +++++++++ .../src/pcp_client_db.c | 265 ++-- .../src/pcp_client_db.h | 58 +- lib/libpcpnatpmp/src/pcp_event_handler.c | 1369 +++++++++++++++++ .../src/pcp_event_handler.h | 49 +- lib/{libpcp => libpcpnatpmp}/src/pcp_logger.c | 105 +- lib/libpcpnatpmp/src/pcp_logger.h | 126 ++ lib/libpcpnatpmp/src/pcp_msg.c | 696 +++++++++ lib/{libpcp => libpcpnatpmp}/src/pcp_msg.h | 9 +- .../src/pcp_msg_structs.h | 98 +- .../src/pcp_server_discovery.c | 165 +- .../src/pcp_server_discovery.h | 2 +- lib/libpcpnatpmp/src/pcp_utils.h | 252 +++ .../src/windows/pcp_gettimeofday.c | 39 +- .../src/windows/pcp_gettimeofday.h | 0 .../src/windows/pcp_win_defines.h | 34 +- 43 files changed, 4991 insertions(+), 5153 deletions(-) delete mode 100644 lib/libpcp/CMakeLists.txt delete mode 100644 lib/libpcp/src/net/pcp_socket.c delete mode 100644 lib/libpcp/src/net/sock_ntop.c delete mode 100644 lib/libpcp/src/pcp_api.c delete mode 100644 lib/libpcp/src/pcp_event_handler.c delete mode 100644 lib/libpcp/src/pcp_logger.h delete mode 100644 lib/libpcp/src/pcp_msg.c delete mode 100644 lib/libpcp/src/pcp_utils.h delete mode 100644 lib/libpcp/src/windows/stdint.h create mode 100644 lib/libpcpnatpmp/CMakeLists.txt create mode 100644 lib/libpcpnatpmp/COPYING rename lib/{libpcp => libpcpnatpmp}/Makefile.am (77%) create mode 100644 lib/libpcpnatpmp/README.md rename lib/{libpcp/src => libpcpnatpmp}/default_config.h (98%) rename lib/{libpcp/include/pcp.h => libpcpnatpmp/include/pcpnatpmp.h} (77%) rename lib/{libpcp/libpcp-client.pc.in => libpcpnatpmp/libpcpnatpmp.pc.in} (50%) rename lib/{libpcp => libpcpnatpmp}/src/net/findsaddr-udp.c (72%) rename lib/{libpcp => libpcpnatpmp}/src/net/findsaddr.h (90%) rename lib/{libpcp => libpcpnatpmp}/src/net/gateway.c (67%) rename lib/{libpcp => libpcpnatpmp}/src/net/gateway.h (100%) create mode 100644 lib/libpcpnatpmp/src/net/pcp_socket.c rename lib/{libpcp => libpcpnatpmp}/src/net/pcp_socket.h (76%) create mode 100644 lib/libpcpnatpmp/src/net/sock_ntop.c rename lib/{libpcp => libpcpnatpmp}/src/net/unp.h (81%) create mode 100644 lib/libpcpnatpmp/src/pcp_api.c rename lib/{libpcp => libpcpnatpmp}/src/pcp_client_db.c (57%) rename lib/{libpcp => libpcpnatpmp}/src/pcp_client_db.h (88%) create mode 100644 lib/libpcpnatpmp/src/pcp_event_handler.c rename lib/{libpcp => libpcpnatpmp}/src/pcp_event_handler.h (63%) rename lib/{libpcp => libpcpnatpmp}/src/pcp_logger.c (62%) create mode 100644 lib/libpcpnatpmp/src/pcp_logger.h create mode 100644 lib/libpcpnatpmp/src/pcp_msg.c rename lib/{libpcp => libpcpnatpmp}/src/pcp_msg.h (97%) rename lib/{libpcp => libpcpnatpmp}/src/pcp_msg_structs.h (81%) rename lib/{libpcp => libpcpnatpmp}/src/pcp_server_discovery.c (55%) rename lib/{libpcp => libpcpnatpmp}/src/pcp_server_discovery.h (97%) create mode 100644 lib/libpcpnatpmp/src/pcp_utils.h rename lib/{libpcp => libpcpnatpmp}/src/windows/pcp_gettimeofday.c (74%) rename lib/{libpcp => libpcpnatpmp}/src/windows/pcp_gettimeofday.h (100%) rename lib/{libpcp => libpcpnatpmp}/src/windows/pcp_win_defines.h (80%) diff --git a/code/CMakeLists.txt b/code/CMakeLists.txt index 223d074bda9..b622564e7ed 100644 --- a/code/CMakeLists.txt +++ b/code/CMakeLists.txt @@ -59,7 +59,7 @@ endif() target_link_libraries(code PUBLIC libRocket) -target_link_libraries(code PUBLIC pcp) +target_link_libraries(code PUBLIC pcpnatpmp) target_link_libraries(code PUBLIC parsers) diff --git a/code/network/multi_portfwd.cpp b/code/network/multi_portfwd.cpp index fffb99d1616..d1184947d60 100644 --- a/code/network/multi_portfwd.cpp +++ b/code/network/multi_portfwd.cpp @@ -15,7 +15,7 @@ #include #endif -#include "pcp.h" +#include "pcpnatpmp.h" #include "cmdline/cmdline.h" #include "io/timer.h" diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 5bae4f0a1da..60824b7ea1d 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -39,7 +39,7 @@ endif() include(libRocket.cmake) -add_subdirectory(libpcp) +add_subdirectory(libpcpnatpmp) include(antlr4.cmake) diff --git a/lib/libpcp/CMakeLists.txt b/lib/libpcp/CMakeLists.txt deleted file mode 100644 index 44ff913fddc..00000000000 --- a/lib/libpcp/CMakeLists.txt +++ /dev/null @@ -1,79 +0,0 @@ -include(CheckStructHasMember) - -check_struct_has_member("struct sockaddr" sa_len - "sys/types.h sys/socket.h" - HAVE_SOCKADDR_SA_LEN) - -set(LIBPCP_INCLUDE_DIRECTORIES - include - src - src/net -) - -if(WIN32) - set(LIBPCP_INCLUDE_DIRECTORIES - ${LIBPCP_INCLUDE_DIRECTORIES} - include/windows - src/windows - win_utils - ) -endif(WIN32) - -# include directories with source and header files -include_directories(${LIBPCP_INCLUDE_DIRECTORIES} ) - - -set(LIBPCP_SOURCES - include/pcp.h - src/net/gateway.c - src/net/gateway.h - src/net/findsaddr-udp.c - src/pcp_api.c - src/pcp_client_db.c - src/pcp_client_db.h - src/pcp_event_handler.c - src/pcp_event_handler.h - src/pcp_logger.c - src/pcp_logger.h - src/pcp_msg.c - src/pcp_msg.h - src/pcp_server_discovery.c - src/pcp_server_discovery.h - src/net/sock_ntop.c - src/net/pcp_socket.c - src/net/pcp_socket.h - src/net/findsaddr.h - src/net/unp.h -) - -if(WIN32) - set(LIBPCP_SOURCES - ${LIBPCP_SOURCES} - src/windows/pcp_gettimeofday.c - src/windows/pcp_gettimeofday.h - src/windows/pcp_win_defines.h - src/windows/stdint.h - ) -endif(WIN32) - - -add_library(pcp STATIC ${LIBPCP_SOURCES}) - - -if(WIN32) - if(MINGW) - target_compile_definitions(pcp PRIVATE HAVE_GETTIMEOFDAY) - endif(MINGW) - - # target XP - target_compile_definitions(pcp PRIVATE WIN32 NTDDI_VERSION=0x06000000 _WIN32_WINNT=0x0600) - target_link_libraries(pcp INTERFACE iphlpapi ws2_32) -endif(WIN32) - -suppress_warnings(pcp) - -set_target_properties(pcp PROPERTIES FOLDER "3rdparty") -target_link_libraries(pcp PUBLIC compiler) - -target_include_directories(pcp SYSTEM PUBLIC - "${CMAKE_CURRENT_SOURCE_DIR}/include") diff --git a/lib/libpcp/src/net/pcp_socket.c b/lib/libpcp/src/net/pcp_socket.c deleted file mode 100644 index 1a528fae431..00000000000 --- a/lib/libpcp/src/net/pcp_socket.c +++ /dev/null @@ -1,345 +0,0 @@ -/* - Copyright (c) 2014 by Cisco Systems, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#else -#include "default_config.h" -#endif - -#include -#include -#include -#ifdef WIN32 -#include "pcp_win_defines.h" -#else //WIN32 -#include -#include -#ifndef PCP_SOCKET_IS_VOIDPTR -#include -#include -#include -#endif //PCP_SOCKET_IS_VOIDPTR -#endif //!WIN32 -#include "pcp.h" -#include "unp.h" -#include "pcp_utils.h" -#include "pcp_socket.h" - -static PCP_SOCKET pcp_socket_create_impl(int domain, int type, int protocol); -static ssize_t pcp_socket_recvfrom_impl(PCP_SOCKET sock, void *buf, size_t len, - int flags, struct sockaddr *src_addr, socklen_t *addrlen); -static ssize_t pcp_socket_sendto_impl(PCP_SOCKET sock, const void *buf, - size_t len, int flags, struct sockaddr *dest_addr, socklen_t addrlen); -static int pcp_socket_close_impl(PCP_SOCKET sock); - -pcp_socket_vt_t default_socket_vt={ - pcp_socket_create_impl, - pcp_socket_recvfrom_impl, - pcp_socket_sendto_impl, - pcp_socket_close_impl -}; - -#ifdef WIN32 -// function calling WSAStartup (used in pcp-server and pcp_app) -int pcp_win_sock_startup() -{ - int err; - WORD wVersionRequested; - WSADATA wsaData; - OSVERSIONINFOEX osvi; - - /* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */ - wVersionRequested=MAKEWORD(2, 2); - err=WSAStartup(wVersionRequested, &wsaData); - if (err != 0) { - /* Tell the user that we could not find a usable */ - /* Winsock DLL. */ - perror("WSAStartup failed with error"); - return 1; - } - //find windows version - ZeroMemory(&osvi, sizeof(osvi)); - osvi.dwOSVersionInfoSize=sizeof(osvi); - - if (!GetVersionEx((LPOSVERSIONINFO)(&osvi))) { - printf("pcp_app: GetVersionEx failed"); - return 1; - } - - return 0; -} - -/* function calling WSACleanup - * returns 0 on success and 1 on failure - */ -int pcp_win_sock_cleanup() -{ - if (WSACleanup() == PCP_SOCKET_ERROR) { - printf("WSACleanup failed.\n"); - return 1; - } - return 0; -} -#endif - -void pcp_fill_in6_addr(struct in6_addr *dst_ip6, uint16_t *dst_port, - struct sockaddr *src) -{ - if (src->sa_family == AF_INET) { - struct sockaddr_in *src_ip4=(struct sockaddr_in *)src; - - if (dst_ip6) { - if (src_ip4->sin_addr.s_addr != INADDR_ANY) { - S6_ADDR32(dst_ip6)[0]=0; - S6_ADDR32(dst_ip6)[1]=0; - S6_ADDR32(dst_ip6)[2]=htonl(0xFFFF); - S6_ADDR32(dst_ip6)[3]=src_ip4->sin_addr.s_addr; - } else { - unsigned i; - for (i=0; i < 4; ++i) - S6_ADDR32(dst_ip6)[i]=0; - } - } - if (dst_port) { - *dst_port=src_ip4->sin_port; - } - } else if (src->sa_family == AF_INET6) { - struct sockaddr_in6 *src_ip6=(struct sockaddr_in6 *)src; - - if (dst_ip6) { - memcpy(dst_ip6, src_ip6->sin6_addr.s6_addr, sizeof(*dst_ip6)); - } - if (dst_port) { - *dst_port=src_ip6->sin6_port; - } - } -} - -void pcp_fill_sockaddr(struct sockaddr *dst, struct in6_addr *sip, - uint16_t sport, int ret_ipv6_mapped_ipv4, uint32_t scope_id) -{ - if ((!ret_ipv6_mapped_ipv4) && (IN6_IS_ADDR_V4MAPPED(sip))) { - struct sockaddr_in *s=(struct sockaddr_in *)dst; - - s->sin_family=AF_INET; - s->sin_addr.s_addr=S6_ADDR32(sip)[3]; - s->sin_port=sport; - SET_SA_LEN(s, sizeof(struct sockaddr_in)); - } else { - struct sockaddr_in6 *s=(struct sockaddr_in6 *)dst; - - s->sin6_family=AF_INET6; - s->sin6_addr=*sip; - s->sin6_port=sport; - s->sin6_scope_id=scope_id; - SET_SA_LEN(s, sizeof(struct sockaddr_in6)); - } -} - -#ifndef PCP_SOCKET_IS_VOIDPTR -static pcp_errno pcp_get_error() -{ -#ifdef WIN32 - int errnum=WSAGetLastError(); - - switch (errnum) { - case WSAEADDRINUSE: - return PCP_ERR_ADDRINUSE; - case WSAEWOULDBLOCK: - return PCP_ERR_WOULDBLOCK; - default: - return PCP_ERR_UNKNOWN; - } -#else - switch (errno) { - case EADDRINUSE: - return PCP_ERR_ADDRINUSE; -// case EAGAIN: - case EWOULDBLOCK: - return PCP_ERR_WOULDBLOCK; - default: - return PCP_ERR_UNKNOWN; - } -#endif -} -#endif - -PCP_SOCKET pcp_socket_create(struct pcp_ctx_s *ctx, int domain, int type, - int protocol) -{ - assert(ctx && ctx->virt_socket_tb && ctx->virt_socket_tb->sock_create); - - return ctx->virt_socket_tb->sock_create(domain, type, protocol); -} - -ssize_t pcp_socket_recvfrom(struct pcp_ctx_s *ctx, void *buf, size_t len, - int flags, struct sockaddr *src_addr, socklen_t *addrlen) -{ - assert(ctx && ctx->virt_socket_tb && ctx->virt_socket_tb->sock_recvfrom); - - return ctx->virt_socket_tb->sock_recvfrom(ctx->socket, buf, len, flags, - src_addr, addrlen); -} - -ssize_t pcp_socket_sendto(struct pcp_ctx_s *ctx, const void *buf, size_t len, - int flags, struct sockaddr *dest_addr, socklen_t addrlen) -{ - assert(ctx && ctx->virt_socket_tb && ctx->virt_socket_tb->sock_sendto); - - return ctx->virt_socket_tb->sock_sendto(ctx->socket, buf, len, flags, - dest_addr, addrlen); -} - -int pcp_socket_close(struct pcp_ctx_s *ctx) -{ - assert(ctx && ctx->virt_socket_tb && ctx->virt_socket_tb->sock_close); - - return ctx->virt_socket_tb->sock_close(ctx->socket); -} - -static PCP_SOCKET pcp_socket_create_impl(int domain, int type, int protocol) -{ -#ifdef PCP_SOCKET_IS_VOIDPTR - return PCP_INVALID_SOCKET; -#else - PCP_SOCKET s; - uint32_t flg; - unsigned long iMode=1; - struct sockaddr_storage sas; - struct sockaddr_in *sin=(struct sockaddr_in *)&sas; - struct sockaddr_in6 *sin6=(struct sockaddr_in6 *)&sas; - - OSDEP(iMode); - OSDEP(flg); - - memset(&sas, 0, sizeof(sas)); - sas.ss_family=domain; - if (domain == AF_INET) { - sin->sin_port=htons(5350); - SET_SA_LEN(sin, sizeof(struct sockaddr_in)); - } else if (domain == AF_INET6) { - sin6->sin6_port=htons(5350); - SET_SA_LEN(sin6, sizeof(struct sockaddr_in6)); - } else { - PCP_LOG(PCP_LOGLVL_ERR, "Unsupported socket domain:%d", domain); - } - - s=(PCP_SOCKET)socket(domain, type, protocol); - if (s == PCP_INVALID_SOCKET) - return PCP_INVALID_SOCKET; - -#ifdef WIN32 - if (ioctlsocket(s, FIONBIO, &iMode)) { - PCP_LOG(PCP_LOGLVL_ERR, "%s", - "Unable to set nonblocking mode for socket."); - CLOSE(s); - return PCP_INVALID_SOCKET; - } -#else - flg=fcntl(s, F_GETFL, 0); - if (fcntl(s, F_SETFL, flg | O_NONBLOCK)) { - PCP_LOG(PCP_LOGLVL_ERR, "%s", - "Unable to set nonblocking mode for socket."); - CLOSE(s); - return PCP_INVALID_SOCKET; - } -#endif -#ifdef PCP_USE_IPV6_SOCKET - flg=0; - if (PCP_SOCKET_ERROR - == setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&flg, - sizeof(flg))) { - PCP_LOG(PCP_LOGLVL_ERR, "%s", - "Dual-stack sockets are not supported on this platform. " - "Recompile library with disabled IPv6 support."); - CLOSE(s); - return PCP_INVALID_SOCKET; - } -#endif //PCP_USE_IPV6_SOCKET - while (bind(s, (struct sockaddr *)&sas, - SA_LEN((struct sockaddr *)&sas)) == PCP_SOCKET_ERROR) { - if (pcp_get_error() == PCP_ERR_ADDRINUSE) { - if (sas.ss_family == AF_INET) { - sin->sin_port=htons(ntohs(sin->sin_port) + 1); - } else { - sin6->sin6_port=htons(ntohs(sin6->sin6_port) + 1); - } - } else { - PCP_LOG(PCP_LOGLVL_ERR, "%s", "bind error"); - CLOSE(s); - return PCP_INVALID_SOCKET; - } - } - PCP_LOG(PCP_LOGLVL_DEBUG, "%s: return %d", __FUNCTION__, s); - return s; -#endif -} - -static ssize_t pcp_socket_recvfrom_impl(PCP_SOCKET sock, void *buf, - size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) -{ - ssize_t ret=-1; - -#ifndef PCP_SOCKET_IS_VOIDPTR - ret=recvfrom(sock, buf, len, flags, src_addr, addrlen); - if (ret == PCP_SOCKET_ERROR) { - if (pcp_get_error() == PCP_ERR_WOULDBLOCK) { - ret=PCP_ERR_WOULDBLOCK; - } else { - ret=PCP_ERR_RECV_FAILED; - } - } -#endif //PCP_SOCKET_IS_VOIDPTR - - return ret; -} - -static ssize_t pcp_socket_sendto_impl(PCP_SOCKET sock, const void *buf, - size_t len, int flags UNUSED, struct sockaddr *dest_addr, socklen_t addrlen) -{ - ssize_t ret=-1; - -#ifndef PCP_SOCKET_IS_VOIDPTR - ret=sendto(sock, buf, len, 0, dest_addr, addrlen); - if ((ret == PCP_SOCKET_ERROR) || (ret != (ssize_t)len)) { - if (pcp_get_error() == PCP_ERR_WOULDBLOCK) { - ret=PCP_ERR_WOULDBLOCK; - } else { - ret=PCP_ERR_SEND_FAILED; - } - } -#endif - return ret; -} - -static int pcp_socket_close_impl(PCP_SOCKET sock) -{ -#ifndef PCP_SOCKET_IS_VOIDPTR - return CLOSE(sock); -#else - return PCP_SOCKET_ERROR; -#endif -} diff --git a/lib/libpcp/src/net/sock_ntop.c b/lib/libpcp/src/net/sock_ntop.c deleted file mode 100644 index 5bc489d3517..00000000000 --- a/lib/libpcp/src/net/sock_ntop.c +++ /dev/null @@ -1,353 +0,0 @@ -/* - Copyright (c) 2014 by Cisco Systems, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#else -#include "default_config.h" -#endif - -#ifdef _MSC_VER -#define _CRT_SECURE_NO_WARNINGS 1 -#endif - -#include -#include -#include -#include /* basic system data types */ -#ifdef WIN32 -#include "pcp_win_defines.h" -#else -#include /* basic socket definitions */ -#include /* sockaddr_in{} and other Internet defns */ -#include /* inet(3) functions */ -#include -#endif -#include -#include -#include "pcp_utils.h" -#include "unp.h" - -#ifdef HAVE_SOCKADDR_DL_STRUCT -#include -#endif - -/* include sock_ntop */ -char *sock_ntop(const struct sockaddr *sa, socklen_t salen) -{ - char portstr[8]; - static char str[128]; /* Unix domain is largest */ - - switch (sa->sa_family) { - case AF_INET: { - const struct sockaddr_in *sin=(const struct sockaddr_in *)sa; - - if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL) - return (NULL); - if (ntohs(sin->sin_port) != 0) { - snprintf(portstr, sizeof(portstr) - 1, ":%d", - ntohs(sin->sin_port)); - portstr[sizeof(portstr) - 1]='\0'; - strcat(str, portstr); - } - return (str); - } - /* end sock_ntop */ - -#ifdef AF_INET6 - case AF_INET6: { - const struct sockaddr_in6 *sin6=(const struct sockaddr_in6 *)sa; - - str[0]='['; - if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, - sizeof(str) - 1) == NULL) - return (NULL); - if (ntohs(sin6->sin6_port) != 0) { - snprintf(portstr, sizeof(portstr) - 1, "]:%d", - ntohs(sin6->sin6_port)); - portstr[sizeof(portstr) - 1]='\0'; - strcat(str, portstr); - return (str); - } - return (str + 1); - } -#endif - - default: - snprintf(str, sizeof(str) - 1, - "sock_ntop: unknown AF_xxx: %d, len %d", sa->sa_family, - salen); - str[sizeof(str) - 1]='\0'; - return (str); - } - return (NULL); -} - -char * -Sock_ntop(const struct sockaddr *sa, socklen_t salen) -{ - char *ptr; - - if ( (ptr = sock_ntop(sa, salen)) == NULL) - perror("sock_ntop"); /* inet_ntop() sets errno */ //LCOV_EXCL_LINE - return(ptr); -} - -int -sock_pton(const char* cp, struct sockaddr *sa) -{ - const char * ip_end; - char * host_name = NULL; - const char* port=NULL; - if ((!cp)||(!sa)) { - return -1; - } - - //skip ws - while ((cp)&&(isspace(*cp))) { - ++cp; - } - - ip_end = cp; - if (*cp=='[') { //find matching bracket ']' - ++cp; - while ((*ip_end)&&(*ip_end!=']')) { - ++ip_end; - } - - if (!*ip_end) { - return -2; - } - host_name=strndup(cp, ip_end-cp); - ++ip_end; - } - { //find start of port part - while (*ip_end) { - if (*ip_end==':') { - if (!port) { - port = ip_end+1; - } else if (host_name==NULL) { // means addr has [] block - port=NULL; // more than 1 ":" => assume the whole addr is IPv6 address w/o port - host_name=strdup(cp); - break; - } - } - ++ip_end; - } - if (!host_name) { - if ((*ip_end==0)&&(port!=NULL)) { - if (port-cp>1) { //only port entered - host_name=strndup(cp, port-cp-1); - } - } else { - host_name=strndup(cp, ip_end-cp); - } - } - } - - // getaddrinfo for host - { - struct addrinfo hints, *servinfo, *p; - int rv; - - memset(&hints, 0, sizeof hints); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_DGRAM; - hints.ai_flags = AI_V4MAPPED; - - if ((rv = getaddrinfo(host_name, port, &hints, &servinfo)) != 0) { - fprintf(stderr, "getaddrinfo: %s\n", - gai_strerror(rv)); - if (host_name) - free (host_name); - return -2; - } - - for(p = servinfo; p != NULL; p = p->ai_next) { - if ((p->ai_family == AF_INET)||(p->ai_family == AF_INET6)) { - memcpy(sa, p->ai_addr, SA_LEN(p->ai_addr)); - if(host_name==NULL) { // getaddrinfo returns localhost ip if hostname is null - switch (p->ai_family) { - case AF_INET: - ((struct sockaddr_in*)sa)->sin_addr.s_addr = INADDR_ANY; - break; - case AF_INET6: - memset(&((struct sockaddr_in6*)sa)->sin6_addr, 0, - sizeof(struct sockaddr_in6)); - break; - default: // Should never happen LCOV_EXCL_START - if (host_name) - free (host_name); - return -2; - } //LCOV_EXCL_STOP - } - break; - } - } - freeaddrinfo(servinfo); - } - - if (host_name) - free (host_name); - return 0; -} - -struct sockaddr *Sock_pton(const char* cp) -{ - static struct sockaddr_storage sa_s; - if (sock_pton(cp, (struct sockaddr *)&sa_s)==0) { - return (struct sockaddr *)&sa_s; - } else { - return NULL; - } -} - -int -sock_pton_with_prefix(const char* cp, struct sockaddr *sa, int *int_prefix) -{ - const char * prefix_begin = NULL; - char * prefix = NULL; - - const char * ip_end; - char * host_name = NULL; - const char* port=NULL; - - if ((!cp)||(!sa)||(!int_prefix)) { - return -1; - } - - //skip ws - while ((cp)&&(isspace(*cp))) { - ++cp; - } - - ip_end = cp; - if (*cp=='[') { //find matching bracket ']' - ++cp; - while ((*ip_end)&&(*ip_end!=']')) { - if (*ip_end == '/' ){ - prefix_begin = ip_end+1; - } - ++ip_end; - } - - if (!*ip_end) { - return -2; - } - - if (prefix_begin){ - host_name=strndup(cp, prefix_begin-cp-1); - prefix = strndup(prefix_begin, ip_end-prefix_begin); - if (prefix) { - *int_prefix = atoi(prefix); - free(prefix); - } - } else { - host_name=strndup(cp, ip_end-cp); - *int_prefix=128; - } - ++ip_end; - } else { - return -2; - } - - { //find start of port part - while (*ip_end) { - if (*ip_end==':') { - if (!port) { - port = ip_end+1; - } else if (host_name==NULL) { // means addr has [] block - port=NULL; // more than 1 ":" => assume the whole addr is IPv6 address w/o port - host_name=strdup(cp); - break; - } - } - ++ip_end; - } - if (!host_name) { - if ((*ip_end==0)&&(port!=NULL)) { - if (port-cp>1) { //only port entered - host_name=strndup(cp, port-cp-1); - } - } else { - host_name=strndup(cp, ip_end-cp); - } - } - } - - // getaddrinfo for host - { - struct addrinfo hints, *servinfo, *p; - int rv; - - memset(&hints, 0, sizeof hints); - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_DGRAM; - hints.ai_flags = AI_V4MAPPED; - - if ((rv = getaddrinfo(host_name, port, &hints, &servinfo)) != 0) { - fprintf(stderr, "getaddrinfo: %s\n", - gai_strerror(rv)); - if (host_name) - free (host_name); - return -2; - } - - for(p = servinfo; p != NULL; p = p->ai_next) { - if ((p->ai_family == AF_INET)||(p->ai_family == AF_INET6)) { - memcpy(sa, p->ai_addr, SA_LEN(p->ai_addr)); - if(host_name==NULL) { // getaddrinfo returns localhost ip if hostname is null - switch (p->ai_family) { - case AF_INET6: - memset(&((struct sockaddr_in6*)sa)->sin6_addr, 0, - sizeof(struct sockaddr_in6)); - break; - default: // Should never happen LCOV_EXCL_START - if (host_name) - free (host_name); - return -2; - } //LCOV_EXCL_STOP - } - break; - } - } - freeaddrinfo(servinfo); - } - - if (host_name) - free (host_name); - - if ((sa->sa_family==AF_INET)&&(*int_prefix > 32)) { - - return -2; - } - - if ((sa->sa_family==AF_INET6)&&(*int_prefix > 128)) { - - return -2; - } - - return 0; -} diff --git a/lib/libpcp/src/pcp_api.c b/lib/libpcp/src/pcp_api.c deleted file mode 100644 index aaf72ef6033..00000000000 --- a/lib/libpcp/src/pcp_api.c +++ /dev/null @@ -1,777 +0,0 @@ -/* - Copyright (c) 2014 by Cisco Systems, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#else -#include "default_config.h" -#endif - -#ifdef _MSC_VER -#define _CRT_SECURE_NO_WARNINGS 1 -#endif - -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include -#include "pcp_win_defines.h" -#include "pcp_gettimeofday.h" -#else -#include -#include -#include -#include -#include -#include -#include -#endif -#include "pcp.h" -#include "pcp_socket.h" -#include "pcp_client_db.h" -#include "pcp_logger.h" -#include "pcp_event_handler.h" -#include "pcp_utils.h" -#include "pcp_server_discovery.h" -#include "net/findsaddr.h" - -PCP_SOCKET pcp_get_socket(pcp_ctx_t *ctx) -{ - - return ctx ? ctx->socket : PCP_INVALID_SOCKET; -} - -int pcp_add_server(pcp_ctx_t *ctx, struct sockaddr *pcp_server, - uint8_t pcp_version) -{ - int res; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - - if (!ctx) { - return PCP_ERR_BAD_ARGS; - } - if (pcp_version > PCP_MAX_SUPPORTED_VERSION) { - PCP_LOG_END(PCP_LOGLVL_INFO); - return PCP_ERR_UNSUP_VERSION; - } - - res=psd_add_pcp_server(ctx, pcp_server, pcp_version); - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return res; -} - -pcp_ctx_t *pcp_init(uint8_t autodiscovery, pcp_socket_vt_t *socket_vt) -{ - pcp_ctx_t *ctx=(pcp_ctx_t *)calloc(1, sizeof(pcp_ctx_t)); - - pcp_logger_init(); - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - - if (!ctx) { - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return NULL; - } - - if (socket_vt) { - ctx->virt_socket_tb=socket_vt; - } else { - ctx->virt_socket_tb=&default_socket_vt; - } - - ctx->socket=pcp_socket_create(ctx, -#ifdef PCP_USE_IPV6_SOCKET - AF_INET6, -#else - AF_INET, -#endif - SOCK_DGRAM, 0); - - if (ctx->socket == PCP_INVALID_SOCKET) { - PCP_LOG(PCP_LOGLVL_WARN, "%s", - "Error occurred while creating a PCP socket."); - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return NULL; - } - PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Created a new PCP socket."); - - if (autodiscovery) - psd_add_gws(ctx); - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return ctx; -} - -int pcp_eval_flow_state(pcp_flow_t *flow, pcp_fstate_e *fstate) -{ - pcp_flow_t *fiter; - int nexit_states=0; - int fpresent_no_exit_state=0; - int fsuccess=0; - int ffailed=0; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - - for (fiter=flow; fiter != NULL; fiter=fiter->next_child) { - switch (fiter->state) { - case pfs_wait_for_lifetime_renew: - fsuccess=1; - ++nexit_states; - break; - case pfs_failed: - ffailed=1; - ++nexit_states; - break; - case pfs_wait_after_short_life_error: - ++nexit_states; - break; - default: - fpresent_no_exit_state=1; - break; - } - } - - if (fstate) { - if (fpresent_no_exit_state) { - if (fsuccess) { - *fstate=pcp_state_partial_result; - } else { - *fstate=pcp_state_processing; - } - } else { - if (fsuccess) { - *fstate=pcp_state_succeeded; - } else if (ffailed) { - *fstate=pcp_state_failed; - } else { - *fstate=pcp_state_short_lifetime_error; - } - } - } - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return nexit_states; -} - -pcp_fstate_e pcp_wait(pcp_flow_t *flow, int timeout, int exit_on_partial_res) -{ -#ifdef PCP_SOCKET_IS_VOIDPTR - return pcp_state_failed; -#else - fd_set read_fds; - int fdmax; - PCP_SOCKET fd; - struct timeval tout_end; - struct timeval tout_select; - pcp_fstate_e fstate; - int nflow_exit_states=pcp_eval_flow_state(flow, &fstate); - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - - if (!flow) { - PCP_LOG(PCP_LOGLVL_PERR, "Flow argument of %s function set to NULL!", - __FUNCTION__); - return pcp_state_failed; - } - - switch (fstate) { - case pcp_state_partial_result: - case pcp_state_processing: - break; - default: - nflow_exit_states=0; - break; - } - - gettimeofday(&tout_end, NULL); - tout_end.tv_usec+=(timeout * 1000) % 1000000; - tout_end.tv_sec+=tout_end.tv_usec / 1000000; - tout_end.tv_usec=tout_end.tv_usec % 1000000; - tout_end.tv_sec+=timeout / 1000; - - PCP_LOG(PCP_LOGLVL_INFO, - "Initialized wait for result of flow: %d, wait timeout %d ms", - flow->key_bucket, timeout); - - FD_ZERO(&read_fds); - - fd=pcp_get_socket(flow->ctx); - fdmax=fd + 1; - - // main loop - for (;;) { - int ret_count; - pcp_fstate_e ret_state; - struct timeval ctv; - - OSDEP(ret_count); - // check expiration of wait timeout - gettimeofday(&ctv, NULL); - if ((timeval_subtract(&tout_select, &tout_end, &ctv)) - || ((tout_select.tv_sec == 0) && (tout_select.tv_usec == 0)) - || (tout_select.tv_sec < 0)) { - return pcp_state_processing; - } - - //process all events and get timeout value for next select - pcp_pulse(flow->ctx, &tout_select); - - // check flow for reaching one of exit from wait states - // (also handles case when flow is MAP for 0.0.0.0) - if (pcp_eval_flow_state(flow, &ret_state) > nflow_exit_states) { - if ((exit_on_partial_res) - || (ret_state != pcp_state_partial_result)) { - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return ret_state; - } - } - - FD_ZERO(&read_fds); - FD_SET(fd, &read_fds); - - PCP_LOG(PCP_LOGLVL_DEBUG, - "Executing select with fdmax=%d, timeout = %ld s; %ld us", - fdmax, tout_select.tv_sec, (long int)tout_select.tv_usec); - - ret_count=select(fdmax, &read_fds, NULL, NULL, &tout_select); - - // check of select result // only for debug purposes -#ifdef DEBUG - if (ret_count == -1) { - char error[ERR_BUF_LEN]; - pcp_strerror(errno, error, sizeof(error)); - PCP_LOG(PCP_LOGLVL_PERR, - "select failed: %s", error); - } else if (ret_count == 0) { - PCP_LOG(PCP_LOGLVL_DEBUG, "%s", - "select timed out"); - } else { - PCP_LOG(PCP_LOGLVL_DEBUG, - "select returned %d i/o events.", ret_count); - } -#endif - }PCP_LOG_END(PCP_LOGLVL_DEBUG); - return pcp_state_succeeded; -#endif //PCP_SOCKET_IS_VOIDPTR -} - -static inline void init_flow(pcp_flow_t *f, pcp_server_t *s, int lifetime, - struct sockaddr *ext_addr) -{ - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - if (f && s) { - struct timeval curtime; - f->ctx=s->ctx; - - switch (f->kd.operation) { - case PCP_OPCODE_MAP: - case PCP_OPCODE_PEER: - pcp_fill_in6_addr(&f->map_peer.ext_ip, &f->map_peer.ext_port, - ext_addr); - break; - default: - assert(!ext_addr); - break; - } - - gettimeofday(&curtime, NULL); - f->lifetime=lifetime; - f->timeout=curtime; - - if (s->server_state == pss_wait_io) { - f->state=pfs_send; - } else { - f->state=pfs_wait_for_server_init; - } - - s->next_timeout=curtime; - f->user_data=NULL; - - pcp_db_add_flow(f); - PCP_LOG_FLOW(f, "Added new flow"); - } - PCP_LOG_END(PCP_LOGLVL_DEBUG); -} - -struct caasi_data { - struct flow_key_data *kd; - pcp_flow_t *fprev; - pcp_flow_t *ffirst; - uint32_t lifetime; - struct sockaddr *ext_addr; - struct in6_addr *src_ip; - uint8_t toler_fields; - char *app_name; - void *userdata; -}; - -static int chain_and_assign_src_ip(pcp_server_t *s, void *data) -{ - struct caasi_data *d=(struct caasi_data *)data; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - - if (s->server_state == pss_not_working) { - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return 0; - } - - if ((IN6_IS_ADDR_UNSPECIFIED(d->src_ip)) - || (IN6_ARE_ADDR_EQUAL(d->src_ip, (struct in6_addr *) s->src_ip))) { - pcp_flow_t *f=NULL; - - memcpy(&d->kd->src_ip, s->src_ip, sizeof(d->kd->src_ip)); - memcpy(&d->kd->pcp_server_ip, s->pcp_ip, sizeof(d->kd->pcp_server_ip)); - memcpy(&d->kd->nonce, &s->nonce, sizeof(d->kd->nonce)); - - f=pcp_create_flow(s, d->kd); - if (!f) { - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return 1; - } -#ifdef PCP_SADSCP - if (d->kd->operation == PCP_OPCODE_SADSCP) { - f->sadscp.toler_fields = d->toler_fields; - if (d->app_name) { - f->sadscp.app_name_length = strlen(d->app_name); - f->sadscp_app_name = strdup(d->app_name); - } else { - f->sadscp.app_name_length = 0; - f->sadscp_app_name = NULL; - } - } -#endif - init_flow(f, s, d->lifetime, d->ext_addr); - f->user_data=d->userdata; - if (d->fprev) { - d->fprev->next_child=f; - } else { - d->ffirst=f; - } - d->fprev=f; - } - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return 0; -} - -pcp_flow_t *pcp_new_flow(pcp_ctx_t *ctx, struct sockaddr *src_addr, - struct sockaddr *dst_addr, struct sockaddr *ext_addr, uint8_t protocol, - uint32_t lifetime, void *userdata) -{ - struct flow_key_data kd; - struct caasi_data data; - struct in6_addr src_ip; - struct sockaddr_storage tmp_ext_addr; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - - memset(&kd, 0, sizeof(kd)); - - if ((!src_addr) || (!ctx)) { - return NULL; - } - pcp_fill_in6_addr(&src_ip, &kd.map_peer.src_port, src_addr); - - kd.map_peer.protocol=protocol; - - if (dst_addr) { - switch (dst_addr->sa_family) { - case AF_INET: - if (((struct sockaddr_in*)(dst_addr))->sin_addr.s_addr - == INADDR_ANY) { - dst_addr=NULL; - } - break; - case AF_INET6: - if (IN6_IS_ADDR_UNSPECIFIED( - &((struct sockaddr_in6 *)(dst_addr))->sin6_addr)) { - dst_addr=NULL; - } - break; - default: - dst_addr=NULL; - break; - } - } - - if (dst_addr) { - pcp_fill_in6_addr(&kd.map_peer.dst_ip, &kd.map_peer.dst_port, dst_addr); - kd.operation=PCP_OPCODE_PEER; - if (src_addr->sa_family == AF_INET) { - if (S6_ADDR32(&src_ip)[3] == INADDR_ANY) { - findsaddr((struct sockaddr_in*)dst_addr, &src_ip); - } - } else if (IN6_IS_ADDR_UNSPECIFIED(&src_ip)) { - findsaddr6((struct sockaddr_in6*)dst_addr, &src_ip); - } else if (dst_addr->sa_family != src_addr->sa_family) { - PCP_LOG(PCP_LOGLVL_PERR, "%s", - "Socket family mismatch."); - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return NULL; - } - } else { - kd.operation=PCP_OPCODE_MAP; - } - - if (!ext_addr) { - struct sockaddr_in *te4=(struct sockaddr_in *)&tmp_ext_addr; - struct sockaddr_in6 *te6=(struct sockaddr_in6 *)&tmp_ext_addr; - tmp_ext_addr.ss_family=src_addr->sa_family; - switch (tmp_ext_addr.ss_family) { - case AF_INET: - memset(&te4->sin_addr, 0, sizeof(te4->sin_addr)); - te4->sin_port=0; - break; - case AF_INET6: - memset(&te6->sin6_addr, 0, sizeof(te6->sin6_addr)); - te6->sin6_port=0; - break; - default: - PCP_LOG(PCP_LOGLVL_PERR, "%s", - "Unsupported address family."); - return NULL; - } - ext_addr=(struct sockaddr *)&tmp_ext_addr; - } - - data.fprev=NULL; - data.lifetime=lifetime; - data.ext_addr=ext_addr; - data.src_ip=&src_ip; - data.kd=&kd; - data.ffirst=NULL; - data.userdata=userdata; - - if (pcp_db_foreach_server(ctx, chain_and_assign_src_ip, &data) - != PCP_ERR_MAX_SIZE) { // didn't iterate through each server => error happened - pcp_delete_flow(data.ffirst); - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return NULL; - } - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return data.ffirst; -} - -void pcp_flow_set_lifetime(pcp_flow_t *f, uint32_t lifetime) -{ - pcp_flow_t *fiter; - - for (fiter=f; fiter != NULL; fiter=fiter->next_child) { - fiter->lifetime=lifetime; - - pcp_flow_updated(fiter); - } -} - -void pcp_flow_set_3rd_party_opt(pcp_flow_t *f, struct sockaddr *thirdp_addr) -{ - pcp_flow_t *fiter; - - for (fiter=f; fiter != NULL; fiter=fiter->next_child) { - fiter->third_party_option_present=1; - pcp_fill_in6_addr(&fiter->third_party_ip, NULL, thirdp_addr); - pcp_flow_updated(fiter); - } -} - -void pcp_flow_set_filter_opt(pcp_flow_t *f, struct sockaddr *filter_ip, - uint8_t filter_prefix) -{ - pcp_flow_t *fiter; - - for (fiter=f; fiter != NULL; fiter=fiter->next_child) { - if (!fiter->filter_option_present) { - fiter->filter_option_present=1; - } - pcp_fill_in6_addr(&fiter->filter_ip, &fiter->filter_port, filter_ip); - fiter->filter_prefix=filter_prefix; - pcp_flow_updated(fiter); - } -} - -void pcp_flow_set_prefer_failure_opt(pcp_flow_t *f) -{ - pcp_flow_t *fiter; - - for (fiter=f; fiter != NULL; fiter=fiter->next_child) { - if (!fiter->pfailure_option_present) { - fiter->pfailure_option_present=1; - pcp_flow_updated(fiter); - } - } -} -#ifdef PCP_EXPERIMENTAL -int pcp_flow_set_userid(pcp_flow_t *f, pcp_userid_option_p user) -{ - pcp_flow_t *fiter; - - for (fiter=f; fiter; fiter=fiter->next_child) { - memcpy(&(fiter->f_userid.userid[0]), &(user->userid[0]), MAX_USER_ID); - pcp_flow_updated(fiter); - } - return 0; -} - -int pcp_flow_set_location(pcp_flow_t *f, pcp_location_option_p loc) -{ - pcp_flow_t *fiter; - - for (fiter=f; fiter; fiter=fiter->next_child) { - memcpy(&(fiter->f_location.location[0]), &(loc->location[0]), MAX_GEO_STR); - pcp_flow_updated(fiter); - } - - return 0; -} - -int pcp_flow_set_deviceid(pcp_flow_t *f, pcp_deviceid_option_p dev) -{ - pcp_flow_t *fiter; - - for (fiter=f; fiter; fiter=fiter->next_child) { - memcpy(&(fiter->f_deviceid.deviceid[0]), &(dev->deviceid[0]), MAX_DEVICE_ID); - pcp_flow_updated(fiter); - } - return 0; -} - -void -pcp_flow_add_md (pcp_flow_t *f, uint32_t md_id, void *value, size_t val_len) -{ - pcp_flow_t *fiter; - - for (fiter=f; fiter!=NULL; fiter=fiter->next_child) { - pcp_db_add_md(fiter, md_id, value, val_len); - pcp_flow_updated(fiter); - } -} -#endif - -#ifdef PCP_FLOW_PRIORITY -void pcp_flow_set_flowp(pcp_flow_t *f, uint8_t dscp_up, uint8_t dscp_down) -{ - pcp_flow_t *fiter; - - for (fiter=f; fiter; fiter=fiter->next_child) { - uint8_t fpresent = (dscp_up!=0)||(dscp_down!=0); - if (fiter->flowp_option_present != fpresent) { - fiter->flowp_option_present=fpresent; - } - if (fpresent) { - fiter->flowp_dscp_up=dscp_up; - fiter->flowp_dscp_down=dscp_down; - } - pcp_flow_updated(fiter); - } -} -#endif - -static inline void pcp_close_flow_intern(pcp_flow_t *f) -{ - switch (f->state) { - case pfs_wait_for_server_init: - case pfs_idle: - case pfs_failed: - f->state=pfs_failed; - break; - default: - f->lifetime=0; - pcp_flow_updated(f); - break; - } - if ((f->state != pfs_wait_for_server_init) && (f->state != pfs_idle) - && (f->state != pfs_failed)) { - PCP_LOG_FLOW(f, "Flow closed"); - f->lifetime=0; - pcp_flow_updated(f); - } else { - f->state=pfs_failed; - } -} - -void pcp_close_flow(pcp_flow_t *f) -{ - pcp_flow_t *fiter; - - for (fiter=f; fiter; fiter=fiter->next_child) { - pcp_close_flow_intern(fiter); - } - - if (f) { - pcp_pulse(f->ctx, NULL); - } -} - -void pcp_delete_flow(pcp_flow_t *f) -{ - pcp_flow_t *fiter=f; - pcp_flow_t *fnext=NULL; - - while (fiter) { - fnext=fiter->next_child; - pcp_delete_flow_intern(fiter); - fiter=fnext; - } -} - -static int delete_flow_iter(pcp_flow_t *f, void *data) -{ - if (data) { - pcp_close_flow_intern(f); - pcp_pulse(f->ctx, NULL); - } - pcp_delete_flow_intern(f); - - return 0; -} - -void pcp_terminate(pcp_ctx_t *ctx, int close_flows) -{ - pcp_db_foreach_flow(ctx, delete_flow_iter, close_flows ? (void *)1 : NULL); - pcp_db_free_pcp_servers(ctx); - pcp_socket_close(ctx); -} - -pcp_flow_info_t *pcp_flow_get_info(pcp_flow_t *f, size_t *info_count) -{ - pcp_flow_t *fiter; - pcp_flow_info_t *info_buf; - pcp_flow_info_t *info_iter; - uint32_t cnt=0; - - if (!info_count) { - return NULL; - } - - for (fiter=f; fiter; fiter=fiter->next_child) { - ++cnt; - } - - info_buf=(pcp_flow_info_t *)calloc(cnt, sizeof(pcp_flow_info_t)); - if (!info_buf) { - PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Error allocating memory"); - return NULL; - } - - for (fiter=f, info_iter=info_buf; fiter != NULL; - fiter=fiter->next_child, ++info_iter) { - - switch (fiter->state) { - case pfs_wait_after_short_life_error: - info_iter->result=pcp_state_short_lifetime_error; - break; - case pfs_wait_for_lifetime_renew: - info_iter->result=pcp_state_succeeded; - break; - case pfs_failed: - info_iter->result=pcp_state_failed; - break; - default: - info_iter->result=pcp_state_processing; - break; - } - - info_iter->recv_lifetime_end=fiter->recv_lifetime; - info_iter->lifetime_renew_s=fiter->lifetime; - info_iter->pcp_result_code=fiter->recv_result; - memcpy(&info_iter->int_ip, &fiter->kd.src_ip, sizeof(struct in6_addr)); - memcpy(&info_iter->pcp_server_ip, &fiter->kd.pcp_server_ip, - sizeof(info_iter->pcp_server_ip)); - if ((fiter->kd.operation == PCP_OPCODE_MAP) - || (fiter->kd.operation == PCP_OPCODE_PEER)) { - memcpy(&info_iter->dst_ip, &fiter->kd.map_peer.dst_ip, - sizeof(info_iter->dst_ip)); - memcpy(&info_iter->ext_ip, &fiter->map_peer.ext_ip, - sizeof(info_iter->ext_ip)); - info_iter->int_port=fiter->kd.map_peer.src_port; - info_iter->dst_port=fiter->kd.map_peer.dst_port; - info_iter->ext_port=fiter->map_peer.ext_port; - info_iter->protocol=fiter->kd.map_peer.protocol; -#ifdef PCP_SADSCP - } else if (fiter->kd.operation == PCP_OPCODE_SADSCP) { - info_iter->learned_dscp=fiter->sadscp.learned_dscp; -#endif - } - } - *info_count=cnt; - - return info_buf; -} - -void pcp_flow_set_user_data(pcp_flow_t *f, void *userdata) -{ - pcp_flow_t *fiter=f; - - while (fiter) { - fiter->user_data=userdata; - fiter=fiter->next_child; - } -} - -void *pcp_flow_get_user_data(pcp_flow_t *f) -{ - return (f ? f->user_data : NULL); -} - -#ifdef PCP_SADSCP -pcp_flow_t *pcp_learn_dscp(pcp_ctx_t *ctx, uint8_t delay_tol, uint8_t loss_tol, - uint8_t jitter_tol, char *app_name) -{ - struct flow_key_data kd; - struct caasi_data data; - struct in6_addr src_ip=IN6ADDR_ANY_INIT; - - memset(&data, 0 ,sizeof(data)); - memset(&kd, 0 ,sizeof(kd)); - - kd.operation=PCP_OPCODE_SADSCP; - - data.fprev=NULL; - data.src_ip=&src_ip; - data.kd=&kd; - data.ffirst=NULL; - data.lifetime=0; - data.ext_addr=NULL; - data.toler_fields=(delay_tol&3)<<6 | ((loss_tol&3)<<4) - | ((jitter_tol&3)<<2); - data.app_name=app_name; - - pcp_db_foreach_server(ctx, chain_and_assign_src_ip, &data); - - return data.ffirst; -} -#endif diff --git a/lib/libpcp/src/pcp_event_handler.c b/lib/libpcp/src/pcp_event_handler.c deleted file mode 100644 index 37f83dccd7d..00000000000 --- a/lib/libpcp/src/pcp_event_handler.c +++ /dev/null @@ -1,1363 +0,0 @@ -/* - Copyright (c) 2014 by Cisco Systems, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#else -#include "default_config.h" -#endif - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef WIN32 -#include "pcp_win_defines.h" -#include "pcp_gettimeofday.h" -#else -//#include -#include -#include -#include -#include -#include -#endif - -#include "pcp.h" -#include "pcp_utils.h" -#include "pcp_msg.h" -#include "pcp_msg_structs.h" -#include "pcp_logger.h" -#include "pcp_event_handler.h" -#include "pcp_server_discovery.h" -#include "pcp_socket.h" - -#define MIN(a, b) (ab?a:b) -#define PCP_RT(rtprev) ((rtprev=rtprev<<1),(((8192+(1024-(rand()&2047))) \ - * MIN (MAX(rtprev,PCP_RETX_IRT), PCP_RETX_MRT))>>13)) - -static pcp_flow_event_e fhndl_send(pcp_flow_t *f, pcp_recv_msg_t *msg); -static pcp_flow_event_e fhndl_resend(pcp_flow_t *f, pcp_recv_msg_t *msg); -static pcp_flow_event_e fhndl_send_renew(pcp_flow_t *f, pcp_recv_msg_t *msg); -static pcp_flow_event_e fhndl_shortlifeerror(pcp_flow_t *f, pcp_recv_msg_t *msg); -static pcp_flow_event_e fhndl_received_success(pcp_flow_t *f, pcp_recv_msg_t *msg); -static pcp_flow_event_e fhndl_clear_timeouts(pcp_flow_t *f, pcp_recv_msg_t *msg); -static pcp_flow_event_e fhndl_waitresp(pcp_flow_t *f, pcp_recv_msg_t *msg); - -static pcp_server_state_e handle_wait_io_receive_msg(pcp_server_t *s); -static pcp_server_state_e handle_server_ping(pcp_server_t *s); -static pcp_server_state_e handle_wait_ping_resp_timeout(pcp_server_t *s); -static pcp_server_state_e handle_wait_ping_resp_recv(pcp_server_t *s); -static pcp_server_state_e handle_version_negotiation(pcp_server_t *s); -static pcp_server_state_e handle_send_all_msgs(pcp_server_t *s); -static pcp_server_state_e handle_server_restart(pcp_server_t *s); -static pcp_server_state_e handle_wait_io_timeout(pcp_server_t *s); -static pcp_server_state_e handle_server_set_not_working(pcp_server_t *s); -static pcp_server_state_e handle_server_not_working(pcp_server_t *s); -static pcp_server_state_e handle_server_reping(pcp_server_t *s); -static pcp_server_state_e pcp_terminate_server(pcp_server_t *s); -static pcp_server_state_e log_unexepected_state_event(pcp_server_t *s); -static pcp_server_state_e ignore_events(pcp_server_t *s); - -#if PCP_MAX_LOG_LEVEL>=PCP_LOGLVL_DEBUG - -//LCOV_EXCL_START -static const char *dbg_get_func_name(void *f) -{ - if (f == fhndl_send) { - return "fhndl_send"; - } else if (f == fhndl_send_renew) { - return "fhndl_send_renew"; - } else if (f == fhndl_resend) { - return "fhndl_resend"; - } else if (f == fhndl_shortlifeerror) { - return "fhndl_shortlifeerror"; - } else if (f == fhndl_received_success) { - return "fhndl_received_success"; - } else if (f == fhndl_clear_timeouts) { - return "fhndl_clear_timeouts"; - } else if (f == fhndl_waitresp) { - return "fhndl_waitresp"; - } else if (f == handle_wait_io_receive_msg) { - return "handle_wait_io_receive_msg"; - } else if (f == handle_server_ping) { - return "handle_server_ping"; - } else if (f == handle_wait_ping_resp_timeout) { - return "handle_wait_ping_resp_timeout"; - } else if (f == handle_wait_ping_resp_recv) { - return "handle_wait_ping_resp_recv"; - } else if (f == handle_version_negotiation) { - return "handle_version_negotiation"; - } else if (f == handle_send_all_msgs) { - return "handle_send_all_msgs"; - } else if (f == handle_server_restart) { - return "handle_server_restart"; - } else if (f == handle_wait_io_timeout) { - return "handle_wait_io_timeout"; - } else if (f == handle_server_set_not_working) { - return "handle_server_set_not_working"; - } else if (f == handle_server_not_working) { - return "handle_server_not_working"; - } else if (f == handle_server_reping) { - return "handle_server_reping"; - } else if (f == pcp_terminate_server) { - return "pcp_terminate_server"; - } else if (f == log_unexepected_state_event) { - return "log_unexepected_state_event"; - } else if (f == ignore_events) { - return "ignore_events"; - } else { - return "unknown"; - } -} - -static const char *dbg_get_event_name(pcp_flow_event_e ev) -{ - static const char *event_names[]={ - "fev_flow_timedout", - "fev_server_initialized", - "fev_send", - "fev_msg_sent", - "fev_failed", - "fev_none", - "fev_server_restarted", - "fev_ignored", - "fev_res_success", - "fev_res_unsupp_version", - "fev_res_not_authorized", - "fev_res_malformed_request", - "fev_res_unsupp_opcode", - "fev_res_unsupp_option", - "fev_res_malformed_option", - "fev_res_network_failure", - "fev_res_no_resources", - "fev_res_unsupp_protocol", - "fev_res_user_ex_quota", - "fev_res_cant_provide_ext", - "fev_res_address_mismatch", - "fev_res_exc_remote_peers", - }; - - assert(((int)ev < sizeof(event_names) / sizeof(event_names[0]))); - - return (int)ev >= 0 ? event_names[ev] : ""; -} - -static const char *dbg_get_state_name(pcp_flow_state_e s) -{ - static const char *state_names[]={ - "pfs_idle", - "pfs_wait_for_server_init", - "pfs_send", - "pfs_wait_resp", - "pfs_wait_after_short_life_error", - "pfs_wait_for_lifetime_renew", - "pfs_send_renew", - "pfs_failed" - }; - - assert((int)s < (int)(sizeof(state_names) / sizeof(state_names[0]))); - - return s >= 0 ? state_names[s] : ""; -} - -static const char *dbg_get_sevent_name(pcp_event_e ev) -{ - static const char *sevent_names[]={ - "pcpe_any", - "pcpe_timeout", - "pcpe_io_event", - "pcpe_terminate" - }; - - assert((int) ev < sizeof(sevent_names) / sizeof(sevent_names[0])); - - return sevent_names[ev]; -} - -static const char *dbg_get_sstate_name(pcp_server_state_e s) -{ - static const char *server_state_names[]={ - "pss_unitialized", - "pss_allocated", - "pss_ping", - "pss_wait_ping_resp", - "pss_version_negotiation", - "pss_send_all_msgs", - "pss_wait_io", - "pss_wait_io_calc_nearest_timeout", - "pss_server_restart", - "pss_server_reping", - "pss_set_not_working", - "pss_not_working" - }; - - assert((int)s < (int)(sizeof(server_state_names) / - sizeof(server_state_names[0]))); - - return (s >=0 ) ? server_state_names[s] : ""; -} - -static const char *dbg_get_fstate_name(pcp_fstate_e s) -{ - static const char *flow_state_names[]={"pcp_state_processing", - "pcp_state_succeeded", "pcp_state_partial_result", - "pcp_state_short_lifetime_error", "pcp_state_failed"}; - - assert((int)s < (int)sizeof(flow_state_names) / - sizeof(flow_state_names[0])); - - return flow_state_names[s]; -} -//LCOV_EXCL_STOP -#endif - -//////////////////////////////////////////////////////////////////////////////// -// Flow State Machine definition - -typedef pcp_flow_event_e (*handle_flow_state_event)(pcp_flow_t *f, pcp_recv_msg_t *msg); - -typedef struct pcp_flow_state_trans { - pcp_flow_state_e state_from; - pcp_flow_state_e state_to; - handle_flow_state_event handler; -} pcp_flow_state_trans_t; - -pcp_flow_state_trans_t flow_transitions[]={ - {pfs_any, pfs_wait_resp, fhndl_waitresp}, - {pfs_wait_resp, pfs_send, fhndl_resend}, - {pfs_any, pfs_send, fhndl_send}, - {pfs_any, pfs_wait_after_short_life_error, fhndl_shortlifeerror}, - {pfs_wait_resp, pfs_wait_for_lifetime_renew, fhndl_received_success}, - {pfs_any, pfs_send_renew, fhndl_send_renew}, - {pfs_wait_for_lifetime_renew, pfs_wait_for_lifetime_renew, fhndl_received_success}, - {pfs_any, pfs_wait_for_server_init, fhndl_clear_timeouts}, - {pfs_any, pfs_failed, fhndl_clear_timeouts}, -}; - -#define FLOW_TRANS_COUNT (sizeof(flow_transitions)/sizeof(*flow_transitions)) - -typedef struct pcp_flow_state_event { - pcp_flow_state_e state; - pcp_flow_event_e event; - pcp_flow_state_e new_state; -} pcp_flow_state_events_t; - -pcp_flow_state_events_t flow_events_sm[]={ - {pfs_any, fev_send, pfs_send}, - {pfs_wait_for_server_init, fev_server_initialized, pfs_send}, - {pfs_wait_resp, fev_res_success, pfs_wait_for_lifetime_renew}, - {pfs_wait_resp, fev_res_unsupp_version, pfs_wait_for_server_init}, - {pfs_wait_resp, fev_res_network_failure, pfs_wait_after_short_life_error}, - {pfs_wait_resp, fev_res_no_resources, pfs_wait_after_short_life_error}, - {pfs_wait_resp, fev_res_exc_remote_peers, pfs_wait_after_short_life_error}, - {pfs_wait_resp, fev_res_user_ex_quota, pfs_wait_after_short_life_error}, - {pfs_wait_resp, fev_flow_timedout, pfs_send}, - {pfs_wait_resp, fev_server_initialized, pfs_send}, - {pfs_send, fev_server_initialized, pfs_send}, - {pfs_send, fev_msg_sent, pfs_wait_resp}, - {pfs_send, fev_flow_timedout, pfs_send}, - {pfs_wait_after_short_life_error, fev_flow_timedout, pfs_send}, - {pfs_wait_for_lifetime_renew, fev_flow_timedout, pfs_send_renew}, - {pfs_wait_for_lifetime_renew, fev_res_success, pfs_wait_for_lifetime_renew}, - {pfs_wait_for_lifetime_renew, fev_res_unsupp_version,pfs_wait_for_server_init}, - {pfs_wait_for_lifetime_renew, fev_res_network_failure, pfs_send_renew}, - {pfs_wait_for_lifetime_renew, fev_res_no_resources, pfs_send_renew}, - {pfs_wait_for_lifetime_renew, fev_res_exc_remote_peers, pfs_send_renew}, - {pfs_wait_for_lifetime_renew, fev_failed, pfs_send}, - {pfs_wait_for_lifetime_renew, fev_res_user_ex_quota, pfs_send_renew}, - {pfs_send_renew, fev_msg_sent, pfs_wait_for_lifetime_renew}, - {pfs_send_renew, fev_flow_timedout, pfs_send_renew}, - {pfs_send_renew, fev_failed, pfs_send}, - {pfs_send, fev_ignored, pfs_wait_for_lifetime_renew}, -// { pfs_failed, fev_server_restarted, pfs_send}, - {pfs_any, fev_server_restarted, pfs_send}, - {pfs_any, fev_failed, pfs_failed}, -/////////////////////////////////////////////////////////////////////////////// -// Long lifetime Error Responses from PCP server - {pfs_wait_resp, fev_res_not_authorized, pfs_failed}, - {pfs_wait_resp, fev_res_malformed_request, pfs_failed}, - {pfs_wait_resp, fev_res_unsupp_opcode, pfs_failed}, - {pfs_wait_resp, fev_res_unsupp_option, pfs_failed}, - {pfs_wait_resp, fev_res_unsupp_protocol, pfs_failed}, - {pfs_wait_resp, fev_res_cant_provide_ext, pfs_failed}, - {pfs_wait_resp, fev_res_address_mismatch, pfs_failed}, - {pfs_wait_for_lifetime_renew, fev_res_not_authorized, pfs_failed}, - {pfs_wait_for_lifetime_renew, fev_res_malformed_request, pfs_failed}, - {pfs_wait_for_lifetime_renew, fev_res_unsupp_opcode, pfs_failed}, - {pfs_wait_for_lifetime_renew, fev_res_unsupp_option, pfs_failed}, - {pfs_wait_for_lifetime_renew, fev_res_unsupp_protocol, pfs_failed}, - {pfs_wait_for_lifetime_renew, fev_res_cant_provide_ext, pfs_failed}, - {pfs_wait_for_lifetime_renew, fev_res_address_mismatch, pfs_failed}, -}; - -#define FLOW_EVENTS_SM_COUNT (sizeof(flow_events_sm)/sizeof(*flow_events_sm)) - -static pcp_errno pcp_flow_send_msg(pcp_flow_t *flow, pcp_server_t *s) -{ - ssize_t ret; - size_t to_send_count; - pcp_ctx_t *ctx=s->ctx; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - - if ((!flow->pcp_msg_buffer) || (flow->pcp_msg_len == 0)) { - build_pcp_msg(flow); - if (flow->pcp_msg_buffer == NULL) { - PCP_LOG(PCP_LOGLVL_DEBUG, "Cannot build PCP MSG (flow bucket:%d)", - flow->key_bucket); - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return PCP_ERR_SEND_FAILED; - } - } - - to_send_count=flow->pcp_msg_len; - - while (to_send_count != 0) { - ret=flow->pcp_msg_len - to_send_count; - - ret=pcp_socket_sendto(ctx, flow->pcp_msg_buffer + ret, - flow->pcp_msg_len - ret, MSG_DONTWAIT, - (struct sockaddr*)&s->pcp_server_saddr, - SA_LEN((struct sockaddr*)&s->pcp_server_saddr)); - if (ret <= 0) { - PCP_LOG(PCP_LOGLVL_WARN, "Error occurred while sending " - "PCP packet to server %s", s->pcp_server_paddr); - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return PCP_ERR_SEND_FAILED; - } - to_send_count-=ret; - } - - PCP_LOG(PCP_LOGLVL_INFO, "Sent PCP MSG (flow bucket:%d)", - flow->key_bucket); - - pcp_flow_clear_msg_buf(flow); - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return PCP_ERR_SUCCESS; -} - -static pcp_errno read_msg(pcp_ctx_t *ctx, pcp_recv_msg_t *msg) -{ - ssize_t ret; - socklen_t src_len=sizeof(msg->rcvd_from_addr); - - memset(msg, 0, sizeof(*msg)); - - if ((ret=pcp_socket_recvfrom(ctx, msg->pcp_msg_buffer, - sizeof(msg->pcp_msg_buffer), MSG_DONTWAIT, - (struct sockaddr*)&msg->rcvd_from_addr, &src_len)) < 0) { - return ret; - } - - msg->pcp_msg_len=ret; - - return PCP_ERR_SUCCESS; -} - -/////////////////////////////////////////////////////////////////////////////// -// Flow State Transitions Handlers - -static pcp_flow_event_e fhndl_send(pcp_flow_t *f, UNUSED pcp_recv_msg_t *msg) -{ - pcp_server_t*s=get_pcp_server(f->ctx, f->pcp_server_indx); - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - - if (!s) { - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return fev_failed; - } - - if (s->restart_flow_msg == f) { - return fev_ignored; - } - - if (pcp_flow_send_msg(f, s) != PCP_ERR_SUCCESS) { - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return fev_failed; - } - - f->resend_timeout=PCP_RETX_IRT; - //set timeout field - gettimeofday(&f->timeout, NULL); - f->timeout.tv_sec+=f->resend_timeout / 1000; - f->timeout.tv_usec+=(f->resend_timeout % 1000) * 1000; - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return fev_msg_sent; -} - -static pcp_flow_event_e fhndl_resend(pcp_flow_t *f, UNUSED pcp_recv_msg_t *msg) -{ - pcp_server_t *s=get_pcp_server(f->ctx, f->pcp_server_indx); - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - - if (!s) { - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return fev_failed; - } - -#if PCP_RETX_MRC>0 - if (++f->retry_count >= PCP_RETX_MRC) { - return fev_failed; - } -#endif - - if (pcp_flow_send_msg(f, s) != PCP_ERR_SUCCESS) { - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return fev_failed; - } - - f->resend_timeout=PCP_RT(f->resend_timeout); - -#if (PCP_RETX_MRD>0) - { - int tdiff = (curtime - f->created_time)*1000; - if (tdiff > PCP_RETX_MRD) { - return fev_failed; - } - if (tdiff > f->resend_timeout) { - f->resend_timeout = tdiff; - } - } -#endif - - //set timeout field - gettimeofday(&f->timeout, NULL); - f->timeout.tv_sec+=f->resend_timeout / 1000; - f->timeout.tv_usec+=(f->resend_timeout % 1000) * 1000; - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return fev_msg_sent; -} - -static pcp_flow_event_e fhndl_shortlifeerror(pcp_flow_t *f, pcp_recv_msg_t *msg) -{ - PCP_LOG(PCP_LOGLVL_DEBUG, - "f->pcp_server_index=%d, f->state = %d, f->key_bucket=%d", - f->pcp_server_indx, f->state, f->key_bucket); - - f->recv_result=msg->recv_result; - - gettimeofday(&f->timeout, NULL); - f->timeout.tv_sec+=msg->recv_lifetime; - - return fev_none; -} - -static pcp_flow_event_e fhndl_received_success(pcp_flow_t *f, - pcp_recv_msg_t *msg) -{ - struct timeval ctv; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - f->recv_lifetime=msg->received_time + msg->recv_lifetime; - if ((f->kd.operation == PCP_OPCODE_MAP) - || (f->kd.operation == PCP_OPCODE_PEER)) { - f->map_peer.ext_ip=msg->assigned_ext_ip; - f->map_peer.ext_port=msg->assigned_ext_port; -#ifdef PCP_SADSCP - } else if (f->kd.operation == PCP_OPCODE_SADSCP) { - f->sadscp.learned_dscp = msg->recv_dscp; -#endif - } - f->recv_result=msg->recv_result; - - gettimeofday(&ctv, NULL); - - if (msg->recv_lifetime == 0) { - f->timeout.tv_sec=0; - f->timeout.tv_usec=0; - } else { - f->timeout=ctv; - f->timeout.tv_sec+=(long int)((f->recv_lifetime - ctv.tv_sec) >> 1); - } - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return fev_none; -} - -static pcp_flow_event_e fhndl_send_renew(pcp_flow_t *f, - UNUSED pcp_recv_msg_t *msg) -{ - pcp_server_t *s=get_pcp_server(f->ctx, f->pcp_server_indx); - long timeout_add; - - if (!s) { - return fev_failed; - } - - if (pcp_flow_send_msg(f, s) != PCP_ERR_SUCCESS) { - return fev_failed; - } - - gettimeofday(&f->timeout, NULL); - timeout_add=(long)((f->recv_lifetime - f->timeout.tv_sec) >> 1); - - if (timeout_add == 0) { - return fev_failed; - } else { - f->timeout.tv_sec+=timeout_add; - } - - return fev_msg_sent; -} - -static pcp_flow_event_e fhndl_clear_timeouts(pcp_flow_t *f, pcp_recv_msg_t *msg) -{ - if (msg) { - f->recv_result=msg->recv_result; - } - pcp_flow_clear_msg_buf(f); - f->timeout.tv_sec=0; - f->timeout.tv_usec=0; - - return fev_none; -} - -static pcp_flow_event_e fhndl_waitresp(pcp_flow_t *f, - UNUSED pcp_recv_msg_t *msg) -{ - struct timeval ctv; - - gettimeofday(&ctv, NULL); - if (timeval_comp(&f->timeout, &ctv) < 0) { - return fev_failed; - } - - return fev_none; -} - -static void flow_change_notify(pcp_flow_t *flow, pcp_fstate_e state); - -static pcp_flow_state_e handle_flow_event(pcp_flow_t *f, pcp_flow_event_e ev, - pcp_recv_msg_t *r) -{ - pcp_flow_state_e cur_state=f->state, next_state; - pcp_flow_state_events_t *esm; - pcp_flow_state_events_t *esm_end=flow_events_sm + FLOW_EVENTS_SM_COUNT; - pcp_flow_state_trans_t *trans; - pcp_flow_state_trans_t *trans_end=flow_transitions + FLOW_TRANS_COUNT; - pcp_fstate_e before, after; - struct in6_addr prev_ext_addr=f->map_peer.ext_ip; - uint16_t prev_ext_port=f->map_peer.ext_port; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - pcp_eval_flow_state(f, &before); - for (;;) { - for (esm=flow_events_sm; esm < esm_end; ++esm) { - if (((esm->state == cur_state) || (esm->state == pfs_any)) - && (esm->event == ev)) { - break; - } - } - - if (esm == esm_end) { - //TODO:log - goto end; - } - - next_state=esm->new_state; - - for (trans=flow_transitions; trans < trans_end; ++trans) { - if (((trans->state_from == cur_state) - || (trans->state_from == pfs_any)) - && (trans->state_to == next_state)) { - -#if PCP_MAX_LOG_LEVEL>=PCP_LOGLVL_DEBUG - pcp_flow_event_e prev_ev=ev; -#endif - f->state=next_state; - - PCP_LOG_DEBUG( - "Executing event handler %s\n flow \t: %d (server %d)\n" - " states\t: %s => %s\n event\t: %s", - dbg_get_func_name(trans->handler), f->key_bucket, f->pcp_server_indx, dbg_get_state_name(cur_state), dbg_get_state_name(next_state), dbg_get_event_name(prev_ev)); - - ev=trans->handler(f, r); - - PCP_LOG_DEBUG( - "Return from event handler's %s \n result event: %s", - dbg_get_func_name(trans->handler), dbg_get_event_name(ev)); - - cur_state=next_state; - - if (ev == fev_none) { - goto end; - } - break; - } - } - - //no transition handler - if (trans == trans_end) { - f->state=next_state; - goto end; - } - } -end: - pcp_eval_flow_state(f, &after); - if ((before != after) - || (!IN6_ARE_ADDR_EQUAL(&prev_ext_addr, &f->map_peer.ext_ip)) - || (prev_ext_port != f->map_peer.ext_port)) { - flow_change_notify(f, after); - } - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return f->state; -} - -/////////////////////////////////////////////////////////////////////////////// -// Helper functions for server state handlers - -static pcp_flow_t *server_process_rcvd_pcp_msg(pcp_server_t *s, - pcp_recv_msg_t *msg) -{ - pcp_flow_t *f; -#ifndef PCP_DISABLE_NATPMP - if (msg->recv_version == 0) { - if (msg->kd.operation == NATPMP_OPCODE_ANNOUNCE) { - s->natpmp_ext_addr=S6_ADDR32(&msg->assigned_ext_ip)[3]; - if ((s->pcp_version == 0) && (s->ping_flow_msg) - && (s->ping_flow_msg->kd.operation == PCP_OPCODE_ANNOUNCE)) { - f=s->ping_flow_msg; - } else { - f=NULL; - } - } else { - S6_ADDR32(&msg->assigned_ext_ip)[3]=s->natpmp_ext_addr; - S6_ADDR32(&msg->assigned_ext_ip)[2]=htonl(0xFFFF); - S6_ADDR32(&msg->assigned_ext_ip)[1]=0; - S6_ADDR32(&msg->assigned_ext_ip)[0]=0; - - f=pcp_get_flow(&msg->kd, s); - } - } else { - f=pcp_get_flow(&msg->kd, s); - } -#else - f = pcp_get_flow(&msg->kd, s->index); -#endif - - if (!f) { - char in6[INET6_ADDRSTRLEN]; - - PCP_LOG(PCP_LOGLVL_INFO, "%s", - "Couldn't find matching flow to received PCP message."); - PCP_LOG(PCP_LOGLVL_PERR, " Operation : %u", msg->kd.operation); - if ((msg->kd.operation == PCP_OPCODE_MAP) - || (msg->kd.operation == PCP_OPCODE_PEER)) { - PCP_LOG(PCP_LOGLVL_PERR, " Protocol : %u", - msg->kd.map_peer.protocol); - PCP_LOG(PCP_LOGLVL_PERR, " Source : %s:%hu", - inet_ntop(s->af, &msg->kd.src_ip, in6, sizeof(in6)), ntohs(msg->kd.map_peer.src_port)); - PCP_LOG(PCP_LOGLVL_PERR, " Destination : %s:%hu", - inet_ntop(s->af, &msg->kd.map_peer.dst_ip, in6, sizeof(in6)), ntohs(msg->kd.map_peer.dst_port)); - } else { - //TODO: add print of SADSCP params - } - return NULL; - } - - PCP_LOG(PCP_LOGLVL_INFO, - "Found matching flow %d to received PCP message.", f->key_bucket); - - handle_flow_event(f, FEV_RES_BEGIN + msg->recv_result, msg); - - return f; -} - -static int check_flow_timeout(pcp_flow_t *f, void *timeout) -{ - struct timeval *tout=timeout; - struct timeval ctv; - - if ((f->timeout.tv_sec == 0) && (f->timeout.tv_usec == 0)) { - return 0; - } - - gettimeofday(&ctv, NULL); - if (timeval_comp(&f->timeout, &ctv) <= 0) { - // timed out - if (f->state == pfs_wait_resp) { - PCP_LOG(PCP_LOGLVL_WARN, - "Recv of PCP response for flow %d timed out.", - f->key_bucket); - } - handle_flow_event(f, fev_flow_timedout, NULL); - } - - if ((f->timeout.tv_sec == 0) && (f->timeout.tv_usec == 0)) { - return 0; - } - - timeval_subtract(&ctv, &f->timeout, &ctv); - - if ((tout->tv_sec == 0) && (tout->tv_usec == 0)) { - *tout=ctv; - return 0; - } - - if (timeval_comp(&ctv, tout) < 0) { - *tout=ctv; - } - - return 0; -} - -struct get_first_flow_iter_data { - pcp_server_t *s; - pcp_flow_t *msg; -}; - -static int get_first_flow_iter(pcp_flow_t *f, void *data) -{ - struct get_first_flow_iter_data *d=(struct get_first_flow_iter_data *)data; - - if (f->pcp_server_indx == d->s->index) { - d->msg=f; - return 1; - } else { - return 0; - } -} - -#ifndef PCP_DISABLE_NATPMP -static inline pcp_flow_t *create_natpmp_ann_msg(pcp_server_t *s) -{ - struct flow_key_data kd; - - memset(&kd, 0, sizeof(kd)); - memcpy(&kd.src_ip, s->src_ip, sizeof(kd.src_ip)); - memcpy(&kd.pcp_server_ip, s->pcp_ip, sizeof(kd.pcp_server_ip)); - memcpy(&kd.nonce, &s->nonce, sizeof(kd.nonce)); - kd.operation=NATPMP_OPCODE_ANNOUNCE; - - s->ping_flow_msg=pcp_create_flow(s, &kd); - pcp_db_add_flow(s->ping_flow_msg); - - return s->ping_flow_msg; -} -#endif - -static inline pcp_flow_t *get_ping_msg(pcp_server_t *s) -{ - struct get_first_flow_iter_data find_data; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - if (!s) - return NULL; - - find_data.s=s; - find_data.msg=NULL; - - pcp_db_foreach_flow(s->ctx, get_first_flow_iter, &find_data); - - s->ping_flow_msg=find_data.msg; - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return find_data.msg; -} - -struct flow_iterator_data { - pcp_server_t *s; - pcp_flow_event_e event; -}; - -static int flow_send_event_iter(pcp_flow_t *f, void *data) -{ - struct flow_iterator_data *d=(struct flow_iterator_data *)data; - - if (f->pcp_server_indx == d->s->index) { - handle_flow_event(f, d->event, NULL); - check_flow_timeout(f, &d->s->next_timeout); - } - - return 0; -} - -/////////////////////////////////////////////////////////////////////////////// -// Server state machine event handlers - -static pcp_server_state_e handle_server_ping(pcp_server_t *s) -{ - pcp_flow_t *msg; - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - s->ping_count=0; - - msg=get_ping_msg(s); - - if (!msg) { - s->next_timeout.tv_sec=0; - s->next_timeout.tv_usec=0; - return pss_ping; - } - - msg->retry_count=0; - - PCP_LOG(PCP_LOGLVL_INFO, "Pinging PCP server at address %s", - s->pcp_server_paddr); - - if (handle_flow_event(msg, fev_send, NULL) != pfs_failed) { - s->next_timeout=msg->timeout; - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return pss_wait_ping_resp; - } - - gettimeofday(&s->next_timeout, NULL); - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return pss_set_not_working; -} - -static pcp_server_state_e handle_wait_ping_resp_timeout(pcp_server_t *s) -{ - if (++s->ping_count >= PCP_MAX_PING_COUNT) { - gettimeofday(&s->next_timeout, NULL); - return pss_set_not_working; - } - - if (!s->ping_flow_msg) { - gettimeofday(&s->next_timeout, NULL); - return pss_ping; - } - - if (handle_flow_event(s->ping_flow_msg, fev_flow_timedout, NULL) - == pfs_failed) { - gettimeofday(&s->next_timeout, NULL); - return pss_set_not_working; - } - - if (s->ping_flow_msg) { - s->next_timeout=s->ping_flow_msg->timeout; - } else { - s->next_timeout.tv_sec=0; - s->next_timeout.tv_usec=0; - return pss_ping; - } - return pss_wait_ping_resp; -} - -static pcp_server_state_e handle_wait_ping_resp_recv(pcp_server_t *s) -{ - pcp_server_state_e res=handle_wait_io_receive_msg(s); - - switch (res) { - case pss_wait_io_calc_nearest_timeout: - res=pss_send_all_msgs; - break; - case pss_wait_io: - res=pss_wait_ping_resp; - break; - default: - break; - } - return res; -} - -static pcp_server_state_e handle_version_negotiation(pcp_server_t *s) -{ - pcp_flow_t *ping_msg; - - if (s->next_version == s->pcp_version) { - s->next_version--; - } - - if (s->pcp_version == 0 -#if PCP_MIN_SUPPORTED_VERSION>0 - || (s->next_version < PCP_MIN_SUPPORTED_VERSION) -#endif - ) { - PCP_LOG(PCP_LOGLVL_WARN, - "Version negotiation failed for PCP server %s. " - "Disabling sending PCP messages to this server.", - s->pcp_server_paddr); - - return pss_set_not_working; - } - - PCP_LOG(PCP_LOGLVL_INFO, - "Version %d not supported by server %s. Trying version %d.", - s->pcp_version, s->pcp_server_paddr, s->next_version); - s->pcp_version=s->next_version; - - ping_msg=s->ping_flow_msg; - -#ifndef PCP_DISABLE_NATPMP - if (s->pcp_version == 0) { - if (ping_msg) { - ping_msg->state=pfs_wait_for_server_init; - ping_msg->timeout.tv_sec=0; - ping_msg->timeout.tv_usec=0; - } - ping_msg=create_natpmp_ann_msg(s); - } -#endif - - if (!ping_msg) { - ping_msg=get_ping_msg(s); - if (!ping_msg) { - s->next_timeout.tv_sec=0; - s->next_timeout.tv_usec=0; - return pss_ping; - } - } - - ping_msg->retry_count=0; - ping_msg->resend_timeout=0; - - handle_flow_event(ping_msg, fev_send, NULL); - if (ping_msg->state == pfs_failed) { - return pss_set_not_working; - } - - s->next_timeout=ping_msg->timeout; - - return pss_wait_ping_resp; -} - -static pcp_server_state_e handle_send_all_msgs(pcp_server_t *s) -{ - struct flow_iterator_data d={s, fev_server_initialized}; - - pcp_db_foreach_flow(s->ctx, flow_send_event_iter, &d); - gettimeofday(&s->next_timeout, NULL); - - return pss_wait_io_calc_nearest_timeout; -} - -static pcp_server_state_e handle_server_restart(pcp_server_t *s) -{ - struct flow_iterator_data d={s, fev_server_restarted}; - - pcp_db_foreach_flow(s->ctx, flow_send_event_iter, &d); - s->restart_flow_msg=NULL; - gettimeofday(&s->next_timeout, NULL); - - return pss_wait_io_calc_nearest_timeout; -} - -static pcp_server_state_e handle_wait_io_receive_msg(pcp_server_t *s) -{ - pcp_recv_msg_t *msg=&s->ctx->msg; - pcp_flow_t *f; - - PCP_LOG(PCP_LOGLVL_INFO, - "Received PCP packet from server at %s, size %d, result_code %d, epoch %d", - s->pcp_server_paddr, msg->pcp_msg_len, msg->recv_result, msg->recv_epoch); - - switch (msg->recv_result) { - case PCP_RES_UNSUPP_VERSION: - PCP_LOG(PCP_LOGLVL_DEBUG, "PCP server %s returned " - "result_code=Unsupported version", s->pcp_server_paddr); - gettimeofday(&s->next_timeout, NULL); - s->next_version=msg->recv_version; - return pss_version_negotiation; - case PCP_RES_ADDRESS_MISMATCH: - PCP_LOG(PCP_LOGLVL_WARN, "There is PCP-unaware NAT present " - "between client and PCP server %s. " - "Sending of PCP messages was disabled.", s->pcp_server_paddr); - gettimeofday(&s->next_timeout, NULL); - return pss_set_not_working; - } - - f=server_process_rcvd_pcp_msg(s, msg); - - if (compare_epochs(msg, s)) { - s->epoch=msg->recv_epoch; - s->cepoch=msg->received_time; - gettimeofday(&s->next_timeout, NULL); - s->restart_flow_msg=f; - - return pss_server_restart; - } - - gettimeofday(&s->next_timeout, NULL); - - return pss_wait_io_calc_nearest_timeout; -} - -static pcp_server_state_e handle_wait_io_timeout(pcp_server_t *s) -{ - struct timeval ctv; - - s->next_timeout.tv_sec=0; - s->next_timeout.tv_usec=0; - - pcp_db_foreach_flow(s->ctx, check_flow_timeout, &s->next_timeout); - - if ((s->next_timeout.tv_sec != 0) || (s->next_timeout.tv_usec != 0)) { - gettimeofday(&ctv, NULL); - s->next_timeout.tv_sec+=ctv.tv_sec; - s->next_timeout.tv_usec+=ctv.tv_usec; - timeval_align(&s->next_timeout); - } - - return pss_wait_io; -} - -static pcp_server_state_e handle_server_set_not_working(pcp_server_t *s) -{ - struct flow_iterator_data d={s, fev_failed}; - - PCP_LOG(PCP_LOGLVL_DEBUG, "Entered function %s", __FUNCTION__); - PCP_LOG(PCP_LOGLVL_WARN, "PCP server %s failed to respond. " - "Disabling sending of PCP messages to this server for %d minutes.", - s->pcp_server_paddr, PCP_SERVER_DISCOVERY_RETRY_DELAY / 60); - - pcp_db_foreach_flow(s->ctx, flow_send_event_iter, &d); - - gettimeofday(&s->next_timeout, NULL); - s->next_timeout.tv_sec+=PCP_SERVER_DISCOVERY_RETRY_DELAY; - - return pss_not_working; -} - -static pcp_server_state_e handle_server_not_working(pcp_server_t *s) -{ - struct timeval ctv; - - gettimeofday(&ctv, NULL); - if (timeval_comp(&ctv, &s->next_timeout) < 0) { - pcp_recv_msg_t *msg=&s->ctx->msg; - pcp_flow_t *f; - - PCP_LOG(PCP_LOGLVL_INFO, - "Received PCP packet from server at %s, size %d, result_code %d, epoch %d", - s->pcp_server_paddr, msg->pcp_msg_len, msg->recv_result, msg->recv_epoch); - - switch (msg->recv_result) { - case PCP_RES_UNSUPP_VERSION: - return pss_not_working; - case PCP_RES_ADDRESS_MISMATCH: - return pss_not_working; - } - - f=server_process_rcvd_pcp_msg(s, msg); - - s->epoch=msg->recv_epoch; - s->cepoch=msg->received_time; - gettimeofday(&s->next_timeout, NULL); - s->restart_flow_msg=f; - - return pss_server_restart; - } - - s->next_timeout=ctv; - - return pss_server_reping; - -} - -static pcp_server_state_e handle_server_reping(pcp_server_t *s) -{ - PCP_LOG(PCP_LOGLVL_INFO, "Trying to ping PCP server %s again. ", - s->pcp_server_paddr); - - s->pcp_version=PCP_MAX_SUPPORTED_VERSION; - gettimeofday(&s->next_timeout, NULL); - - return pss_ping; -} - -static pcp_server_state_e pcp_terminate_server(pcp_server_t *s) -{ - s->next_timeout.tv_sec=0; - s->next_timeout.tv_usec=0; - - PCP_LOG(PCP_LOGLVL_INFO, "PCP server %s terminated. ", - s->pcp_server_paddr); - - return pss_allocated; -} - -static pcp_server_state_e ignore_events(pcp_server_t *s) -{ - s->next_timeout.tv_sec=0; - s->next_timeout.tv_usec=0; - - return s->server_state; -} - -//LCOV_EXCL_START -static pcp_server_state_e log_unexepected_state_event(pcp_server_t *s) -{ - PCP_LOG(PCP_LOGLVL_PERR, "Event happened in the state %d on PCP server %s" - " and there is no event handler defined.", - s->server_state, s->pcp_server_paddr); - - gettimeofday(&s->next_timeout, NULL); - return pss_set_not_working; -} -//LCOV_EXCL_STOP -//////////////////////////////////////////////////////////////////////////////// -// Server State Machine definition - -typedef pcp_server_state_e (*handle_server_state_event)(pcp_server_t *s); - -typedef struct pcp_server_state_machine { - pcp_server_state_e state; - pcp_event_e event; - handle_server_state_event handler; -} pcp_server_state_machine_t; - -pcp_server_state_machine_t server_sm[]={{pss_any, pcpe_terminate, - pcp_terminate_server}, -// -> allocated - {pss_ping, pcpe_any, handle_server_ping}, - // -> wait_ping_resp | set_not_working - {pss_wait_ping_resp, pcpe_timeout, handle_wait_ping_resp_timeout}, - // -> wait_ping_resp | set_not_working - {pss_wait_ping_resp, pcpe_io_event, handle_wait_ping_resp_recv}, - // -> wait ping_resp | pss_send_waiting_msgs | set_not_working | version_neg - {pss_version_negotiation, pcpe_any, handle_version_negotiation}, - // -> wait ping_resp | set_not_working - {pss_send_all_msgs, pcpe_any, handle_send_all_msgs}, - // -> wait_io - {pss_wait_io, pcpe_io_event, handle_wait_io_receive_msg}, - // -> wait_io_calc_nearest_timeout | server_restart |version_negotiation | set_not_working - {pss_wait_io, pcpe_timeout, handle_wait_io_timeout}, - // -> wait_io | server_restart - {pss_wait_io_calc_nearest_timeout, pcpe_any, handle_wait_io_timeout}, - // -> wait_io - {pss_server_restart, pcpe_any, handle_server_restart}, - // -> wait_io - {pss_server_reping, pcpe_any, handle_server_reping}, - // -> ping - {pss_set_not_working, pcpe_any, handle_server_set_not_working}, - // -> not_working - {pss_not_working, pcpe_any, handle_server_not_working}, - // -> reping - {pss_allocated, pcpe_any, ignore_events}, - {pss_any, pcpe_any, log_unexepected_state_event} -// -> last_state - }; - -#define SERVER_STATE_MACHINE_COUNT (sizeof(server_sm)/sizeof(*server_sm)) - -pcp_errno run_server_state_machine(pcp_server_t *s, pcp_event_e event) -{ - unsigned i; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - if (!s) { - return PCP_ERR_BAD_ARGS; - } - - for (i=0; i < SERVER_STATE_MACHINE_COUNT; ++i) { - pcp_server_state_machine_t *state_def=server_sm + i; - if ((state_def->state == s->server_state) - || (state_def->state == pss_any)) { - if ((state_def->event == pcpe_any) || (state_def->event == event)) { - PCP_LOG_DEBUG( - "Executing server state handler %s\n server \t: %s (index %d)\n" - " state\t: %s\n" - " event\t: %s", - dbg_get_func_name(state_def->handler), s->pcp_server_paddr, s->index, dbg_get_sstate_name(s->server_state), dbg_get_sevent_name(event)); - - s->server_state=state_def->handler(s); - - PCP_LOG_DEBUG( - "Return from server state handler's %s \n result state: %s", - dbg_get_func_name(state_def->handler), dbg_get_sstate_name(s->server_state)); - - break; - } - } - }PCP_LOG_END(PCP_LOGLVL_DEBUG); - return PCP_ERR_SUCCESS; -} - -struct hserver_iter_data { - struct timeval *res_timeout; - pcp_event_e ev; -}; - -static int hserver_iter(pcp_server_t *s, void *data) -{ - pcp_event_e ev=((struct hserver_iter_data*)data)->ev; - struct timeval *res_timeout=((struct hserver_iter_data*)data)->res_timeout; - struct timeval ctv; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - if ((s == NULL) || (s->server_state == pss_unitialized) || (data == NULL)) { - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return 0; - } - - if (ev != pcpe_timeout) - run_server_state_machine(s, ev); - - while (1) { - gettimeofday(&ctv, NULL); - if (((s->next_timeout.tv_sec == 0) && (s->next_timeout.tv_usec == 0)) - || (!timeval_subtract(&ctv, &s->next_timeout, &ctv))) { - break; - } - run_server_state_machine(s, pcpe_timeout); - } - - if ((!res_timeout) - || ((s->next_timeout.tv_sec == 0) && (s->next_timeout.tv_usec == 0))) { - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return 0; - } - - if ((res_timeout->tv_sec == 0) && (res_timeout->tv_usec == 0)) { - - *res_timeout=ctv; - - } else if (timeval_comp(&ctv, res_timeout) < 0) { - - *res_timeout=ctv; - } - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return 0; -} - -//////////////////////////////////////////////////////////////////////////////// -// Exported functions - -int pcp_pulse(pcp_ctx_t *ctx, struct timeval *next_timeout) -{ - pcp_recv_msg_t *msg; - struct timeval tmp_timeout={0, 0}; - - if (!ctx) { - return PCP_ERR_BAD_ARGS; - } - - msg=&ctx->msg; - - if (!next_timeout) { - next_timeout=&tmp_timeout; - } - - memset(msg, 1, sizeof(*msg)); - - if (read_msg(ctx, msg) == PCP_ERR_SUCCESS) { - struct in6_addr ip6; - pcp_server_t *s; - struct hserver_iter_data param={NULL, pcpe_io_event}; - - msg->received_time=time(NULL); - - if (!validate_pcp_msg(msg)) { - PCP_LOG(PCP_LOGLVL_PERR, "%s", "Invalid PCP msg"); - goto process_timeouts; - } - - if ((parse_response(msg)) != PCP_ERR_SUCCESS) { - PCP_LOG(PCP_LOGLVL_PERR, "%s", "Cannot parse PCP msg"); - goto process_timeouts; - } - - pcp_fill_in6_addr(&ip6, NULL, (struct sockaddr*)&msg->rcvd_from_addr); - s=get_pcp_server_by_ip(ctx, &ip6); - - if (s) { - msg->pcp_server_indx=s->index; - memcpy(&msg->kd.src_ip, s->src_ip, sizeof(struct in6_addr)); - memcpy(&msg->kd.pcp_server_ip, s->pcp_ip, sizeof(struct in6_addr)); - if (msg->recv_version < 2) { - memcpy(&msg->kd.nonce, &s->nonce, sizeof(struct pcp_nonce)); - } - - // process pcpe_io_event for server - hserver_iter(s, ¶m); - } - } - -process_timeouts: - { - struct hserver_iter_data param={next_timeout, pcpe_timeout}; - pcp_db_foreach_server(ctx, hserver_iter, ¶m); - } - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return (next_timeout->tv_sec * 1000) + (next_timeout->tv_usec / 1000); -} - -void pcp_flow_updated(pcp_flow_t *f) -{ - struct timeval curtime; - pcp_server_t*s; - - if (!f) - return; - - gettimeofday(&curtime, NULL); - s=get_pcp_server(f->ctx, f->pcp_server_indx); - if (s) { - s->next_timeout=curtime; - } - pcp_flow_clear_msg_buf(f); - f->timeout=curtime; - if ((f->state != pfs_wait_for_server_init) && (f->state != pfs_idle) - && (f->state != pfs_failed)) { - f->state=pfs_send; - } -} - -void pcp_set_flow_change_cb(pcp_ctx_t *ctx, pcp_flow_change_notify cb_fun, - void *cb_arg) -{ - if (ctx) { - ctx->flow_change_cb_fun=cb_fun; - ctx->flow_change_cb_arg=cb_arg; - } -} - -static void flow_change_notify(pcp_flow_t *flow, pcp_fstate_e state) -{ - struct sockaddr_storage src_addr, ext_addr; - pcp_ctx_t *ctx=flow->ctx; - - PCP_LOG_DEBUG( "Flow's %d state changed to: %s", - flow->key_bucket, dbg_get_fstate_name(state)); - - if (ctx->flow_change_cb_fun) { - pcp_fill_sockaddr((struct sockaddr*)&src_addr, &flow->kd.src_ip, - flow->kd.map_peer.src_port, 0, 0/* scope_id */); - if (state == pcp_state_succeeded) { - pcp_fill_sockaddr((struct sockaddr*)&ext_addr, - &flow->map_peer.ext_ip, flow->map_peer.ext_port, 0, - 0/* scope_id */); - } else { - memset(&ext_addr, 0, sizeof(ext_addr)); - ext_addr.ss_family=AF_INET; - } - ctx->flow_change_cb_fun(flow, (struct sockaddr*)&src_addr, - (struct sockaddr*)&ext_addr, state, ctx->flow_change_cb_arg); - } -} diff --git a/lib/libpcp/src/pcp_logger.h b/lib/libpcp/src/pcp_logger.h deleted file mode 100644 index 9d02baf4f01..00000000000 --- a/lib/libpcp/src/pcp_logger.h +++ /dev/null @@ -1,108 +0,0 @@ -/* - Copyright (c) 2014 by Cisco Systems, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef PCP_LOGGER_H_ -#define PCP_LOGGER_H_ - -#define ERR_BUF_LEN 256 - -#include "pcp.h" - -#ifdef NDEBUG -#undef DEBUG -#endif - -void pcp_logger_init(void); - -#ifdef WIN32 -void pcp_logger(pcp_loglvl_e log_level, const char* fmt, ...); -#else -void pcp_logger(pcp_loglvl_e log_level, const char* fmt, ...) - __attribute__((format(printf, 2, 3))); -#endif - -#ifdef DEBUG - -#ifndef PCP_MAX_LOG_LEVEL -// Maximal log level for compile-time check -#define PCP_MAX_LOG_LEVEL PCP_LOGLVL_DEBUG -#endif - -#define PCP_LOG(level, fmt, ...) { if (level<=PCP_MAX_LOG_LEVEL) \ -pcp_logger(level, "FILE: %s:%d; Func: %s:\n " fmt,\ -__FILE__, __LINE__, __FUNCTION__, __VA_ARGS__); } - -#define PCP_LOG_END(level) { if (level<=PCP_MAX_LOG_LEVEL) \ -pcp_logger(level, "FILE: %s:%d; Func: %s: END \n " ,\ -__FILE__, __LINE__, __FUNCTION__); } - -#define PCP_LOG_BEGIN(level) { if (level<=PCP_MAX_LOG_LEVEL) \ -pcp_logger(level, "FILE: %s:%d; Func: %s: BEGIN \n ",\ -__FILE__, __LINE__, __FUNCTION__); } - -#else //DEBUG -#ifndef PCP_MAX_LOG_LEVEL -// Maximal log level for compile-time check -#define PCP_MAX_LOG_LEVEL PCP_LOGLVL_INFO -#endif - -#define PCP_LOG(level, fmt, ...) { \ -if (level<=PCP_MAX_LOG_LEVEL) pcp_logger(level, fmt, __VA_ARGS__); } - -#define PCP_LOG_END(level) - -#define PCP_LOG_BEGIN(level) - -#endif // DEBUG -#if PCP_MAX_LOG_LEVEL>=PCP_LOGLVL_DEBUG -#define PCP_LOG_DEBUG(fmt, ...) PCP_LOG(PCP_LOGLVL_DEBUG, fmt, __VA_ARGS__) -#else -#define PCP_LOG_DEBUG(fmt, ...) -#endif - -#if PCP_MAX_LOG_LEVEL>=PCP_LOGLVL_INFO -#define PCP_LOG_FLOW(f, msg) \ -do { \ - if (pcp_log_level >= PCP_LOGLVL_INFO) { \ - char src_buf[INET6_ADDRSTRLEN]="Unknown"; \ - char dst_buf[INET6_ADDRSTRLEN]="Unknown"; \ - char pcp_buf[INET6_ADDRSTRLEN]="Unknown"; \ -\ - inet_ntop(AF_INET6, &f->kd.src_ip, src_buf, sizeof(src_buf)); \ - inet_ntop(AF_INET6, &f->kd.map_peer.dst_ip, dst_buf, \ - sizeof(dst_buf)); \ - inet_ntop(AF_INET6, &f->kd.pcp_server_ip, pcp_buf, sizeof(pcp_buf)); \ - PCP_LOG(PCP_LOGLVL_INFO, \ - "%s(PCP server: %s; Int. addr: [%s]:%d; Dest. addr: [%s]:%d; Key bucket: %d)", \ - msg, pcp_buf, src_buf, ntohs(f->kd.map_peer.src_port), dst_buf, ntohs(f->kd.map_peer.dst_port), f->key_bucket); \ - } \ -} while(0) -#else -#define PCP_LOG_FLOW(f, msg) do{} while(0) -#endif - -void pcp_strerror(int errnum, char *buf, size_t buflen); - -#endif /* PCP_LOGGER_H_ */ diff --git a/lib/libpcp/src/pcp_msg.c b/lib/libpcp/src/pcp_msg.c deleted file mode 100644 index c0a836ba7d5..00000000000 --- a/lib/libpcp/src/pcp_msg.c +++ /dev/null @@ -1,714 +0,0 @@ -/* - Copyright (c) 2014 by Cisco Systems, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifdef HAVE_CONFIG_H -#include "config.h" -#else -#include "default_config.h" -#endif - -#include -#include -#include -#include -#include -#include -#include -#ifdef WIN32 -#include "pcp_win_defines.h" -#else -#include -#include -#include -#endif //WIN32 -#include "pcp.h" -#include "pcp_utils.h" -#include "pcp_msg.h" -#include "pcp_msg_structs.h" -#include "pcp_logger.h" - -static void *add_filter_option(pcp_flow_t *f, void *cur) -{ - pcp_filter_option_t *filter_op=(pcp_filter_option_t *)cur; - - filter_op->option=PCP_OPTION_FILTER; - filter_op->reserved=0; - filter_op->len=htons(sizeof(pcp_filter_option_t) - sizeof(pcp_options_hdr_t)); - filter_op->reserved2=0; - filter_op->filter_prefix=f->filter_prefix; - filter_op->filter_peer_port=f->filter_port; - memcpy(&filter_op->filter_peer_ip, &f->filter_ip, - sizeof(filter_op->filter_peer_ip)); - cur=filter_op->next_data; - - return cur; -} - -static void *add_prefer_failure_option(void *cur) -{ - pcp_prefer_fail_option_t *pfailure_op=(pcp_prefer_fail_option_t *)cur; - - pfailure_op->option=PCP_OPTION_PREF_FAIL; - pfailure_op->reserved=0; - pfailure_op->len=htons(sizeof(pcp_prefer_fail_option_t) - sizeof(pcp_options_hdr_t)); - cur=pfailure_op->next_data; - - return cur; -} - -static void *add_third_party_option(pcp_flow_t *f, void *cur) -{ - pcp_3rd_party_option_t *tp_op=(pcp_3rd_party_option_t *)cur; - - tp_op->option=PCP_OPTION_3RD_PARTY; - tp_op->reserved=0; - memcpy(tp_op->ip, &f->third_party_ip, sizeof(f->third_party_ip)); - tp_op->len=htons(sizeof(*tp_op) - sizeof(pcp_options_hdr_t)); - cur=tp_op->next_data; - - return cur; -} - -#ifdef PCP_EXPERIMENTAL -static void *add_userid_option(pcp_flow_t *f, void *cur) -{ - pcp_userid_option_t *userid_op = (pcp_userid_option_t *) cur; - - userid_op->option=PCP_OPTION_USERID; - userid_op->len=htons(sizeof(pcp_userid_option_t) - sizeof(pcp_options_hdr_t)); - memcpy(&(userid_op->userid[0]), &(f->f_userid.userid[0]), MAX_USER_ID); - cur=userid_op + 1; - - return cur; -} - -static void *add_location_option(pcp_flow_t *f, void *cur) -{ - pcp_location_option_t *location_op = (pcp_location_option_t *) cur; - - location_op->option=PCP_OPTION_LOCATION; - location_op->len=htons(sizeof(pcp_location_option_t) - sizeof(pcp_options_hdr_t)); - memcpy(&(location_op->location[0]), &(f->f_location.location[0]), MAX_GEO_STR); - cur=location_op + 1; - - return cur; -} - -static void *add_deviceid_option(pcp_flow_t *f, void *cur) -{ - pcp_deviceid_option_t *deviceid_op = (pcp_deviceid_option_t *) cur; - - deviceid_op->option=PCP_OPTION_DEVICEID; - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - deviceid_op->len=htons(sizeof(pcp_deviceid_option_t) - sizeof(pcp_options_hdr_t)); - memcpy(&(deviceid_op->deviceid[0]), &(f->f_deviceid.deviceid[0]), MAX_DEVICE_ID); - cur=deviceid_op + 1; - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return cur; -} -#endif - -#ifdef PCP_FLOW_PRIORITY -static void *add_flowp_option(pcp_flow_t *f, void *cur) -{ - pcp_flow_priority_option_t *flowp_op = (pcp_flow_priority_option_t *)cur; - - flowp_op->option=PCP_OPTION_FLOW_PRIORITY; - flowp_op->len=htons(sizeof(pcp_flow_priority_option_t) - sizeof(pcp_options_hdr_t)); - flowp_op->dscp_up=f->flowp_dscp_up; - flowp_op->dscp_down=f->flowp_dscp_down; - cur=flowp_op->next_data; - - return cur; -} -#endif - -#ifdef PCP_EXPERIMENTAL -static inline pcp_metadata_option_t *add_md_option(pcp_flow_t *f, - pcp_metadata_option_t *md_opt, md_val_t *md) -{ - size_t len_md=md->val_len; - uint32_t padding=(4 - (len_md % 4)) % 4; - size_t pcp_msg_len=((const char*)md_opt) - f->pcp_msg_buffer; - - if ( (pcp_msg_len + (sizeof(pcp_metadata_option_t) + len_md + padding)) > - PCP_MAX_LEN) { - return md_opt; - } - - md_opt->option=PCP_OPTION_METADATA; - md_opt->metadata_id=htonl(md->md_id); - memcpy(md_opt->metadata, md->val_buf, len_md); - md_opt->len=htons(sizeof(*md_opt) - sizeof(pcp_options_hdr_t) + len_md + padding); - - return (pcp_metadata_option_t *)(((uint8_t *)(md_opt+1)) + len_md + padding); -} - -static void *add_md_options(pcp_flow_t *f, void *cur) -{ - uint32_t i; - md_val_t *md; - pcp_metadata_option_t *md_opt=(pcp_metadata_option_t *)cur; - - for (i=f->md_val_count, md=f->md_vals; i>0 && md!=NULL; --i, ++md) - { - if (md->val_len) { - md_opt = add_md_option(f, md_opt, md); - } - } - return md_opt; -} -#endif - -static pcp_errno build_pcp_options(pcp_flow_t *flow, void *cur) -{ -#ifdef PCP_FLOW_PRIORITY - if (flow->flowp_option_present) { - cur=add_flowp_option(flow, cur); - } -#endif - if (flow->filter_option_present) { - cur=add_filter_option(flow, cur); - } - - if (flow->pfailure_option_present) { - cur=add_prefer_failure_option(cur); - } - if (flow->third_party_option_present) { - cur=add_third_party_option(flow, cur); - } -#ifdef PCP_EXPERIMENTAL - if (flow->f_deviceid.deviceid[0] != '\0') { - cur=add_deviceid_option(flow, cur); - } - - if (flow->f_userid.userid[0] != '\0') { - cur=add_userid_option(flow, cur); - } - - if (flow->f_location.location[0] != '\0') { - cur=add_location_option(flow, cur); - } - - if (flow->md_val_count>0) { - cur=add_md_options(flow, cur); - } -#endif - - flow->pcp_msg_len=((char*)cur) - flow->pcp_msg_buffer; - - //TODO: implement building all pcp options into msg - return PCP_ERR_SUCCESS; -} - -static pcp_errno build_pcp_peer(pcp_server_t *server, pcp_flow_t *flow, - void *peer_loc) -{ - void *next=NULL; - - if (server->pcp_version == 1) { - pcp_peer_v1_t *peer_info=(pcp_peer_v1_t *)peer_loc; - - peer_info->protocol=flow->kd.map_peer.protocol; - peer_info->int_port=flow->kd.map_peer.src_port; - peer_info->ext_port=flow->map_peer.ext_port; - peer_info->peer_port=flow->kd.map_peer.dst_port; - memcpy(peer_info->ext_ip, &flow->map_peer.ext_ip, - sizeof(peer_info->ext_ip)); - memcpy(peer_info->peer_ip, &flow->kd.map_peer.dst_ip, - sizeof(peer_info->peer_ip)); - next=peer_info + 1; - } else if (server->pcp_version == 2) { - pcp_peer_v2_t *peer_info=(pcp_peer_v2_t *)peer_loc; - - peer_info->protocol=flow->kd.map_peer.protocol; - peer_info->int_port=flow->kd.map_peer.src_port; - peer_info->ext_port=flow->map_peer.ext_port; - peer_info->peer_port=flow->kd.map_peer.dst_port; - memcpy(peer_info->ext_ip, &flow->map_peer.ext_ip, - sizeof(peer_info->ext_ip)); - memcpy(peer_info->peer_ip, &flow->kd.map_peer.dst_ip, - sizeof(peer_info->peer_ip)); - peer_info->nonce=flow->kd.nonce; - next=peer_info + 1; - } else { - return PCP_ERR_UNSUP_VERSION; - } - return build_pcp_options(flow, next); -} - -static pcp_errno build_pcp_map(pcp_server_t *server, pcp_flow_t *flow, - void *map_loc) -{ - void *next=NULL; - - if (server->pcp_version == 1) { - pcp_map_v1_t *map_info=(pcp_map_v1_t *)map_loc; - - map_info->protocol=flow->kd.map_peer.protocol; - map_info->int_port=flow->kd.map_peer.src_port; - map_info->ext_port=flow->map_peer.ext_port; - memcpy(map_info->ext_ip, &flow->map_peer.ext_ip, - sizeof(map_info->ext_ip)); - next=map_info + 1; - } else if (server->pcp_version == 2) { - pcp_map_v2_t *map_info=(pcp_map_v2_t *)map_loc; - - map_info->protocol=flow->kd.map_peer.protocol; - map_info->int_port=flow->kd.map_peer.src_port; - map_info->ext_port=flow->map_peer.ext_port; - memcpy(map_info->ext_ip, &flow->map_peer.ext_ip, - sizeof(map_info->ext_ip)); - map_info->nonce=flow->kd.nonce; - next=map_info + 1; - } else { - return PCP_ERR_UNSUP_VERSION; - } - - return build_pcp_options(flow, next); -} - -#ifdef PCP_SADSCP -static pcp_errno build_pcp_sadscp(pcp_server_t *server, pcp_flow_t *flow, - void *sadscp_loc) -{ - void *next=NULL; - - if (server->pcp_version == 1) { - return PCP_ERR_UNSUP_VERSION; - } else if (server->pcp_version == 2) { - size_t fill_len; - pcp_sadscp_req_t *sadscp=(pcp_sadscp_req_t *)sadscp_loc; - - sadscp->nonce=flow->kd.nonce; - sadscp->tolerance_fields=flow->sadscp.toler_fields; - - //app name fill size to multiple of 4 - fill_len=(4-((flow->sadscp.app_name_length+2)%4))%4; - - sadscp->app_name_length=flow->sadscp.app_name_length + fill_len; - if (flow->sadscp_app_name) { - memcpy(sadscp->app_name, flow->sadscp_app_name, - flow->sadscp.app_name_length); - } else { - memset(sadscp->app_name, 0, - flow->sadscp.app_name_length); - } - - next=((uint8_t *)sadscp_loc) + sizeof(pcp_sadscp_req_t) + - sadscp->app_name_length; - } else { - return PCP_ERR_UNSUP_VERSION; - } - - return build_pcp_options(flow, next); -} -#endif - -#ifndef PCP_DISABLE_NATPMP -static pcp_errno build_natpmp_msg(pcp_flow_t *flow) -{ - nat_pmp_announce_req_t *ann_msg; - nat_pmp_map_req_t *map_info; - - switch (flow->kd.operation) { - case PCP_OPCODE_ANNOUNCE: - ann_msg=(nat_pmp_announce_req_t *)flow->pcp_msg_buffer; - ann_msg->ver=0; - ann_msg->opcode=NATPMP_OPCODE_ANNOUNCE; - flow->pcp_msg_len=sizeof(*ann_msg); - return PCP_RES_SUCCESS; - - case PCP_OPCODE_MAP: - map_info=(nat_pmp_map_req_t *)flow->pcp_msg_buffer; - switch (flow->kd.map_peer.protocol) { - case IPPROTO_TCP: - map_info->opcode=NATPMP_OPCODE_MAP_TCP; - break; - case IPPROTO_UDP: - map_info->opcode=NATPMP_OPCODE_MAP_UDP; - break; - default: - return PCP_RES_UNSUPP_PROTOCOL; - } - map_info->ver=0; - map_info->lifetime=htonl(flow->lifetime); - map_info->int_port=flow->kd.map_peer.src_port; - map_info->ext_port=flow->map_peer.ext_port; - flow->pcp_msg_len=sizeof(*map_info); - return PCP_RES_SUCCESS; - - default: - return PCP_RES_UNSUPP_OPCODE; - } -} -#endif - -void *build_pcp_msg(pcp_flow_t *flow) -{ - ssize_t ret=-1; - pcp_server_t *pcp_server=NULL; - pcp_request_t *req; - // pointer used for referencing next data structure in linked list - void *next_data=NULL; - - PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); - - if (!flow) { - return NULL; - } - - pcp_server=get_pcp_server(flow->ctx, flow->pcp_server_indx); - - if (!pcp_server) { - return NULL; - } - - if (!flow->pcp_msg_buffer) { - flow->pcp_msg_buffer=(char*)calloc(1, PCP_MAX_LEN); - if (flow->pcp_msg_buffer == NULL) { - PCP_LOG(PCP_LOGLVL_ERR, "%s", - "Malloc can't allocate enough memory for the pcp_flow."); - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return NULL; - } - } - - req=(pcp_request_t *)flow->pcp_msg_buffer; - - if (pcp_server->pcp_version == 0) { - // NATPMP -#ifndef PCP_DISABLE_NATPMP - ret=build_natpmp_msg(flow); -#endif - } else { - - req->ver=pcp_server->pcp_version; - - req->r_opcode|=(uint8_t)(flow->kd.operation & 0x7f); //set opcode - req->req_lifetime=htonl((uint32_t)flow->lifetime); - - memcpy(&req->ip, &flow->kd.src_ip, 16); - // next data in the packet - next_data=req->next_data; - flow->pcp_msg_len=(uint8_t *)next_data - (uint8_t *)req; - - switch (flow->kd.operation) { - case PCP_OPCODE_PEER: - ret=build_pcp_peer(pcp_server, flow, next_data); - break; - case PCP_OPCODE_MAP: - ret=build_pcp_map(pcp_server, flow, next_data); - break; -#ifdef PCP_SADSCP - case PCP_OPCODE_SADSCP: - ret=build_pcp_sadscp(pcp_server, flow, next_data); - break; -#endif - case PCP_OPCODE_ANNOUNCE: - ret=0; - break; - } - } - - if (ret < 0) { - PCP_LOG(PCP_LOGLVL_ERR, "%s", "Unsupported operation."); - free(flow->pcp_msg_buffer); - flow->pcp_msg_buffer=NULL; - flow->pcp_msg_len=0; - req=NULL; - } - - PCP_LOG_END(PCP_LOGLVL_DEBUG); - return req; -} - -int validate_pcp_msg(pcp_recv_msg_t *f) -{ - pcp_response_t *resp; - - //check size - if (((f->pcp_msg_len & 3) != 0) || (f->pcp_msg_len < 4) - || (f->pcp_msg_len > PCP_MAX_LEN)) { - PCP_LOG(PCP_LOGLVL_WARN, "Received packet with invalid size %d)", - f->pcp_msg_len); - return 0; - } - - resp=(pcp_response_t *)f->pcp_msg_buffer; - if ((resp->ver)&&!(resp->r_opcode & 0x80)) { - PCP_LOG(PCP_LOGLVL_WARN, "%s", - "Received packet without response bit set"); - return 0; - } - - if (resp->ver > PCP_MAX_SUPPORTED_VERSION) { - PCP_LOG(PCP_LOGLVL_WARN, - "Received PCP msg using unsupported PCP version %d", resp->ver); - return 0; - } - - return 1; -} - -static pcp_errno parse_options(UNUSED pcp_recv_msg_t *f, UNUSED void *r) -{ - //TODO: implement parsing of pcp options - return PCP_ERR_SUCCESS; -} - -static pcp_errno parse_v1_map(pcp_recv_msg_t *f, void *r) -{ - pcp_map_v1_t *m; - size_t rest_size=f->pcp_msg_len - (((char*)r) - f->pcp_msg_buffer); - - if (rest_size < sizeof(pcp_map_v1_t)) { - return PCP_ERR_RECV_FAILED; - } - - m=(pcp_map_v1_t *)r; - f->kd.map_peer.src_port=m->int_port; - f->kd.map_peer.protocol=m->protocol; - f->assigned_ext_port=m->ext_port; - memcpy(&f->assigned_ext_ip, m->ext_ip, sizeof(f->assigned_ext_ip)); - - if (rest_size > sizeof(pcp_map_v1_t)) { - return parse_options(f, m + 1); - } - return PCP_ERR_SUCCESS; -} - -static pcp_errno parse_v2_map(pcp_recv_msg_t *f, void *r) -{ - pcp_map_v2_t *m; - size_t rest_size=f->pcp_msg_len - (((char*)r) - f->pcp_msg_buffer); - - if (rest_size < sizeof(pcp_map_v2_t)) { - return PCP_ERR_RECV_FAILED; - } - - m=(pcp_map_v2_t *)r; - f->kd.nonce=m->nonce; - f->kd.map_peer.src_port=m->int_port; - f->kd.map_peer.protocol=m->protocol; - f->assigned_ext_port=m->ext_port; - memcpy(&f->assigned_ext_ip, m->ext_ip, sizeof(f->assigned_ext_ip)); - - if (rest_size > sizeof(pcp_map_v2_t)) { - return parse_options(f, m + 1); - } - return PCP_ERR_SUCCESS; -} - -static pcp_errno parse_v1_peer(pcp_recv_msg_t *f, void *r) -{ - pcp_peer_v1_t *m; - size_t rest_size=f->pcp_msg_len - (((char*)r) - f->pcp_msg_buffer); - - if (rest_size < sizeof(pcp_peer_v1_t)) { - return PCP_ERR_RECV_FAILED; - } - - m=(pcp_peer_v1_t *)r; - f->kd.map_peer.src_port=m->int_port; - f->kd.map_peer.protocol=m->protocol; - f->kd.map_peer.dst_port=m->peer_port; - f->assigned_ext_port=m->ext_port; - memcpy(&f->kd.map_peer.dst_ip, m->peer_ip, sizeof(f->kd.map_peer.dst_ip)); - memcpy(&f->assigned_ext_ip, m->ext_ip, sizeof(f->assigned_ext_ip)); - - if (rest_size > sizeof(pcp_peer_v1_t)) { - return parse_options(f, m + 1); - } - return PCP_ERR_SUCCESS; -} - -static pcp_errno parse_v2_peer(pcp_recv_msg_t *f, void *r) -{ - pcp_peer_v2_t *m; - size_t rest_size=f->pcp_msg_len - (((char*)r) - f->pcp_msg_buffer); - - if (rest_size < sizeof(pcp_peer_v2_t)) { - return PCP_ERR_RECV_FAILED; - } - - m=(pcp_peer_v2_t *)r; - f->kd.nonce=m->nonce; - f->kd.map_peer.src_port=m->int_port; - f->kd.map_peer.protocol=m->protocol; - f->kd.map_peer.dst_port=m->peer_port; - f->assigned_ext_port=m->ext_port; - memcpy(&f->kd.map_peer.dst_ip, m->peer_ip, sizeof(f->kd.map_peer.dst_ip)); - memcpy(&f->assigned_ext_ip, m->ext_ip, sizeof(f->assigned_ext_ip)); - - if (rest_size > sizeof(pcp_peer_v2_t)) { - return parse_options(f, m + 1); - } - return PCP_ERR_SUCCESS; -} - -#ifdef PCP_SADSCP -static pcp_errno parse_sadscp(pcp_recv_msg_t *f, void *r) -{ - pcp_sadscp_resp_t *d; - size_t rest_size = f->pcp_msg_len - (((char*) r) - f->pcp_msg_buffer); - - if (rest_size < sizeof(pcp_sadscp_resp_t)) { - return PCP_ERR_RECV_FAILED; - } - d = (pcp_sadscp_resp_t *) r; - f->kd.nonce = d->nonce; - f->recv_dscp = d->a_r_dscp & (0x3f); //mask 6 lower bits - - return PCP_ERR_SUCCESS; -} -#endif - -#ifndef PCP_DISABLE_NATPMP -static pcp_errno parse_v0_resp(pcp_recv_msg_t *f, pcp_response_t *resp) -{ - switch(f->kd.operation) { - case PCP_OPCODE_ANNOUNCE: - if (f->pcp_msg_len == sizeof(nat_pmp_announce_resp_t)) { - nat_pmp_announce_resp_t *r=(nat_pmp_announce_resp_t *)resp; - - f->recv_epoch=ntohl(r->epoch); - S6_ADDR32(&f->assigned_ext_ip)[0]=0; - S6_ADDR32(&f->assigned_ext_ip)[1]=0; - S6_ADDR32(&f->assigned_ext_ip)[2]=htonl(0xFFFF); - S6_ADDR32(&f->assigned_ext_ip)[3]=r->ext_ip; - - return PCP_ERR_SUCCESS; - } - break; - case NATPMP_OPCODE_MAP_TCP: - case NATPMP_OPCODE_MAP_UDP: - if (f->pcp_msg_len == sizeof(nat_pmp_map_resp_t)) { - nat_pmp_map_resp_t *r=(nat_pmp_map_resp_t *)resp; - - f->assigned_ext_port=r->ext_port; - f->kd.map_peer.src_port=r->int_port; - f->recv_epoch=ntohl(r->epoch); - f->recv_lifetime=ntohl(r->lifetime); - f->recv_result=ntohs(r->result); - f->kd.map_peer.protocol= - f->kd.operation == NATPMP_OPCODE_MAP_TCP ? - IPPROTO_TCP : IPPROTO_UDP; - f->kd.operation=PCP_OPCODE_MAP; - return PCP_ERR_SUCCESS; - } - break; - default: - break; - } - - if (f->pcp_msg_len == sizeof(nat_pmp_inv_version_resp_t)) { - nat_pmp_inv_version_resp_t *r=(nat_pmp_inv_version_resp_t *)resp; - - f->recv_result=ntohs(r->result); - f->recv_epoch=ntohl(r->epoch); - return PCP_ERR_SUCCESS; - } - - return PCP_ERR_RECV_FAILED; -} -#endif - -static pcp_errno parse_v1_resp(pcp_recv_msg_t *f, pcp_response_t *resp) -{ - if (f->pcp_msg_len < sizeof(pcp_response_t)) { - return PCP_ERR_RECV_FAILED; - } - - f->recv_lifetime=ntohl(resp->lifetime); - f->recv_epoch=ntohl(resp->epochtime); - - switch (f->kd.operation) { - case PCP_OPCODE_ANNOUNCE: - return PCP_ERR_SUCCESS; - case PCP_OPCODE_MAP: - return parse_v1_map(f, resp->next_data); - case PCP_OPCODE_PEER: - return parse_v1_peer(f, resp->next_data); - default: - return PCP_ERR_RECV_FAILED; - } -} - -static pcp_errno parse_v2_resp(pcp_recv_msg_t *f, pcp_response_t *resp) -{ - if (f->pcp_msg_len < sizeof(pcp_response_t)) { - return PCP_ERR_RECV_FAILED; - } - - f->recv_lifetime=ntohl(resp->lifetime); - f->recv_epoch=ntohl(resp->epochtime); - - switch (f->kd.operation) { - case PCP_OPCODE_ANNOUNCE: - return PCP_ERR_SUCCESS; - case PCP_OPCODE_MAP: - return parse_v2_map(f, resp->next_data); - case PCP_OPCODE_PEER: - return parse_v2_peer(f, resp->next_data); -#ifdef PCP_SADSCP - case PCP_OPCODE_SADSCP: - return parse_sadscp(f, resp->next_data); -#endif - default: - return PCP_ERR_RECV_FAILED; - } -} - -pcp_errno parse_response(pcp_recv_msg_t *f) -{ - pcp_response_t *resp=(pcp_response_t *)f->pcp_msg_buffer; - - f->recv_version=resp->ver; - f->recv_result=resp->result_code; - memset(&f->kd, 0, sizeof(f->kd)); - - f->kd.operation=resp->r_opcode & 0x7f; - - PCP_LOG(PCP_LOGLVL_DEBUG, "parse_response: version: %d", f->recv_version); - PCP_LOG(PCP_LOGLVL_DEBUG, "parse_response: result: %d", f->recv_result); - - switch (f->recv_version) { -#ifndef PCP_DISABLE_NATPMP - case 0: - return parse_v0_resp(f, resp); - break; -#endif - case 1: - return parse_v1_resp(f, resp); - break; - case 2: - return parse_v2_resp(f, resp); - break; - } - return PCP_ERR_UNSUP_VERSION; -} - diff --git a/lib/libpcp/src/pcp_utils.h b/lib/libpcp/src/pcp_utils.h deleted file mode 100644 index e7e09512579..00000000000 --- a/lib/libpcp/src/pcp_utils.h +++ /dev/null @@ -1,250 +0,0 @@ -/* - Copyright (c) 2014 by Cisco Systems, Inc. - All rights reserved. - - Redistribution and use in source and binary forms, with or without - modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND - ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -#ifndef PCP_UTILS_H_ -#define PCP_UTILS_H_ - -#include -#include -#include -#include "pcp_logger.h" -#include "pcp_client_db.h" - -#ifndef max -#define max(a,b) \ - ({ typeof (a) _a=(a); \ - typeof (b) _b=(b); \ - _a > _b ? _a : _b; }) -#endif - -#ifndef min -#define min(a,b) \ - ({ typeof (a) _a=(a); \ - typeof (b) _b=(b); \ - _a > _b ? _b : _a; }) -#endif - - #ifdef __GNUC__ - #define UNUSED __attribute__ ((unused)) - #else - #define UNUSED - #endif - -#ifdef WIN32 -/* variable num of arguments*/ -#define DUPPRINT(fp, fmt, ...) \ - do { \ - printf(fmt, __VA_ARGS__); \ - if (fp != NULL) { \ - fprintf(fp, fmt, __VA_ARGS__); \ - } \ - } while(0) -#else /*WIN32*/ -#define DUPPRINT(fp, fmt...) \ - do { \ - printf(fmt); \ - if (fp != NULL) { \ - fprintf(fp,fmt); \ - } \ - } while(0) -#endif /*WIN32*/ - -#define log_err(STR) \ - do { \ - printf("%s:%d "#STR": %s \n", __FUNCTION__, __LINE__, strerror(errno)); \ - } while (0) - -#define log_debug_scr(STR) \ - do { \ - printf("%s:%d %s \n", __FUNCTION__, __LINE__, STR); \ - } while (0) - -#define log_debug(STR) \ - do { \ - printf("%s:%d "#STR" \n", __FUNCTION__, __LINE__); \ - } while (0) - -#define CHECK_RET_EXIT(func) \ - do { \ - if (func < 0) { \ - log_err(""); \ - exit (EXIT_FAILURE); \ - } \ - } while(0) - -#define CHECK_NULL_EXIT(func) \ - do { \ - if (func == NULL) { \ - log_err(""); \ - exit (EXIT_FAILURE); \ - } \ - } while(0) - - -#define CHECK_RET(func) \ - do { \ - if (func < 0) { \ - log_err(""); \ - } \ - } while(0) - -#define CHECK_RET_GOTO_ERROR(func) \ - do { \ - if (func < 0) { \ - log_err(""); \ - goto ERROR; \ - } \ - } while(0) - - - -#define OSDEP(x) (void)(x) - -#ifdef s6_addr32 -#define S6_ADDR32(sa6) (sa6)->s6_addr32 -#else -#define S6_ADDR32(sa6) ((uint32_t *)((sa6)->s6_addr)) -#endif - -#define IPV6_ADDR_COPY(dest, src) \ - do { \ - (S6_ADDR32(dest))[0]=(S6_ADDR32(src))[0]; \ - (S6_ADDR32(dest))[1]=(S6_ADDR32(src))[1]; \ - (S6_ADDR32(dest))[2]=(S6_ADDR32(src))[2]; \ - (S6_ADDR32(dest))[3]=(S6_ADDR32(src))[3]; \ - } while (0) - -#include "pcp_msg.h" -static inline int compare_epochs(pcp_recv_msg_t *f, pcp_server_t *s) -{ - uint32_t c_delta; - uint32_t s_delta; - - if (s->epoch == ~0u) { - s->epoch=f->recv_epoch; - s->cepoch=f->received_time; - } - c_delta=(uint32_t)(f->received_time - s->cepoch); - s_delta=f->recv_epoch - s->epoch; - - PCP_LOG(PCP_LOGLVL_DEBUG, - "Epoch - client delta = %u, server delta = %u", - c_delta, s_delta); - - return (c_delta + 2 < s_delta - (s_delta >> 4)) - || (s_delta + 2 < c_delta - (c_delta >> 4)); -} - -inline static void timeval_align(struct timeval *x) -{ - x->tv_sec+=x->tv_usec / 1000000; - x->tv_usec=x->tv_usec % 1000000; - if (x->tv_usec<0) { - x->tv_usec=1000000 + x->tv_usec; - x->tv_sec-=1; - } -} - -inline static int timeval_comp(struct timeval *x, struct timeval *y) -{ - timeval_align(x); - timeval_align(y); - if (x->tv_sec < y->tv_sec) { - return -1; - } else if (x->tv_sec > y->tv_sec) { - return 1; - } else if (x->tv_usec < y->tv_usec) { - return -1; - } else if (x->tv_usec > y->tv_usec) { - return 1; - } else { - return 0; - } -} - -inline static int timeval_subtract(struct timeval *result, struct timeval *x, - struct timeval *y) -{ - int ret=timeval_comp(x, y); - - if (ret<=0) { - result->tv_sec=0; - result->tv_usec=0; - return 1; - } - - // in case that tv_usec is unsigned -> perform the carry - if (x->tv_usec < y->tv_usec) { - int nsec=(y->tv_usec - x->tv_usec) / 1000000 + 1; - y->tv_usec-=1000000 * nsec; - y->tv_sec+=nsec; - } - - /* Compute the time remaining to wait. - tv_usec is certainly positive. */ - result->tv_sec=x->tv_sec - y->tv_sec; - result->tv_usec=x->tv_usec - y->tv_usec; - timeval_align(result); - - /* Return 1 if result is negative. */ - return ret <= 0; -} - -/* Nonce is part of the MAP and PEER requests/responses - as of version 2 of the PCP protocol */ -static inline void createNonce(struct pcp_nonce *nonce_field) -{ - int i; - for (i = 2; i >= 0; --i) -#ifdef WIN32 - nonce_field->n[i]=htonl (rand()); -#else //WIN32 - nonce_field->n[i]=htonl(random()); -#endif //WIN32 -} - -#ifndef HAVE_STRNDUP -static inline char *pcp_strndup(const char *s, size_t size) { - char *ret; - char *end=memchr(s, 0, size); - - if (end) { - /* Length + 1 */ - size=end - s + 1; - } else { - size++; - } - ret=malloc(size); - - if (ret) { - memcpy(ret, s, size); - ret[size-1]='\0'; - } - return ret; -} -#define strndup pcp_strndup -#endif - -#endif /* PCP_UTILS_H_ */ diff --git a/lib/libpcp/src/windows/stdint.h b/lib/libpcp/src/windows/stdint.h deleted file mode 100644 index 11b80d7bff5..00000000000 --- a/lib/libpcp/src/windows/stdint.h +++ /dev/null @@ -1,249 +0,0 @@ -// ISO C9x compliant stdint.h for Microsoft Visual Studio -// Based on ISO/IEC 9899:TC2 Committee draft (May 6, 2005) WG14/N1124 -// -// Copyright (c) 2006-2008 Alexander Chemeris -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, -// this list of conditions and the following disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright -// notice, this list of conditions and the following disclaimer in the -// documentation and/or other materials provided with the distribution. -// -// 3. The name of the author may be used to endorse or promote products -// derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED -// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -// EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -// -/////////////////////////////////////////////////////////////////////////////// - -#ifndef __MINGW32__ // [ -#ifndef _MSC_VER // [ -#error "Use this header only with Microsoft Visual C++ compilers!" -#endif // _MSC_VER ] -#endif // __MINGW32__ ] - -#ifndef _MSC_STDINT_H_ // [ -#define _MSC_STDINT_H_ - -#if _MSC_VER > 1000 -#pragma once -#endif - -#include - -// For Visual Studio 6 in C++ mode and for many Visual Studio versions when -// compiling for ARM we should wrap include with 'extern "C++" {}' -// or compiler give many errors like this: -// error C2733: second C linkage of overloaded function 'wmemchr' not allowed -#ifdef __cplusplus -extern "C" { -#endif -# include -#ifdef __cplusplus -} -#endif - -// Define _W64 macros to mark types changing their size, like intptr_t. -#ifndef _W64 -# if !defined(__midl) && (defined(_X86_) || defined(_M_IX86)) && _MSC_VER >= 1300 -# define _W64 __w64 -# else -# define _W64 -# endif -#endif - - -// 7.18.1 Integer types - -// 7.18.1.1 Exact-width integer types - -// Visual Studio 6 and Embedded Visual C++ 4 doesn't -// realize that, e.g. char has the same size as __int8 -// so we give up on __intX for them. -#if (_MSC_VER < 1300) - typedef char int8_t; - typedef short int16_t; - typedef int int32_t; - typedef unsigned char uint8_t; - typedef unsigned short uint16_t; - typedef unsigned int uint32_t; -#else - typedef __int8 int8_t; - typedef __int16 int16_t; - typedef __int32 int32_t; - typedef unsigned __int8 uint8_t; - typedef unsigned __int16 uint16_t; - typedef unsigned __int32 uint32_t; -#endif -typedef __int64 int64_t; -typedef unsigned __int64 uint64_t; - - -// 7.18.1.2 Minimum-width integer types -typedef int8_t int_least8_t; -typedef int16_t int_least16_t; -typedef int32_t int_least32_t; -typedef int64_t int_least64_t; -typedef uint8_t uint_least8_t; -typedef uint16_t uint_least16_t; -typedef uint32_t uint_least32_t; -typedef uint64_t uint_least64_t; - -// 7.18.1.3 Fastest minimum-width integer types -typedef int8_t int_fast8_t; -typedef int16_t int_fast16_t; -typedef int32_t int_fast32_t; -typedef int64_t int_fast64_t; -typedef uint8_t uint_fast8_t; -typedef uint16_t uint_fast16_t; -typedef uint32_t uint_fast32_t; -typedef uint64_t uint_fast64_t; - -// 7.18.1.4 Integer types capable of holding object pointers -#ifdef _WIN64 // [ - typedef __int64 intptr_t; - typedef unsigned __int64 uintptr_t; -#else // _WIN64 ][ - typedef _W64 int intptr_t; - typedef _W64 unsigned int uintptr_t; -#endif // _WIN64 ] - -// 7.18.1.5 Greatest-width integer types -typedef int64_t intmax_t; -typedef uint64_t uintmax_t; - - -// 7.18.2 Limits of specified-width integer types - -#if !defined(__cplusplus) || defined(__STDC_LIMIT_MACROS) // [ See footnote 220 at page 257 and footnote 221 at page 259 - -// 7.18.2.1 Limits of exact-width integer types -#define INT8_MIN ((int8_t)_I8_MIN) -#define INT8_MAX _I8_MAX -#define INT16_MIN ((int16_t)_I16_MIN) -#define INT16_MAX _I16_MAX -#define INT32_MIN ((int32_t)_I32_MIN) -#define INT32_MAX _I32_MAX -#define INT64_MIN ((int64_t)_I64_MIN) -#define INT64_MAX _I64_MAX -#define UINT8_MAX _UI8_MAX -#define UINT16_MAX _UI16_MAX -#define UINT32_MAX _UI32_MAX -#define UINT64_MAX _UI64_MAX - -// 7.18.2.2 Limits of minimum-width integer types -#define INT_LEAST8_MIN INT8_MIN -#define INT_LEAST8_MAX INT8_MAX -#define INT_LEAST16_MIN INT16_MIN -#define INT_LEAST16_MAX INT16_MAX -#define INT_LEAST32_MIN INT32_MIN -#define INT_LEAST32_MAX INT32_MAX -#define INT_LEAST64_MIN INT64_MIN -#define INT_LEAST64_MAX INT64_MAX -#define UINT_LEAST8_MAX UINT8_MAX -#define UINT_LEAST16_MAX UINT16_MAX -#define UINT_LEAST32_MAX UINT32_MAX -#define UINT_LEAST64_MAX UINT64_MAX - -// 7.18.2.3 Limits of fastest minimum-width integer types -#define INT_FAST8_MIN INT8_MIN -#define INT_FAST8_MAX INT8_MAX -#define INT_FAST16_MIN INT16_MIN -#define INT_FAST16_MAX INT16_MAX -#define INT_FAST32_MIN INT32_MIN -#define INT_FAST32_MAX INT32_MAX -#define INT_FAST64_MIN INT64_MIN -#define INT_FAST64_MAX INT64_MAX -#define UINT_FAST8_MAX UINT8_MAX -#define UINT_FAST16_MAX UINT16_MAX -#define UINT_FAST32_MAX UINT32_MAX -#define UINT_FAST64_MAX UINT64_MAX - -// 7.18.2.4 Limits of integer types capable of holding object pointers -#ifdef _WIN64 // [ -# define INTPTR_MIN INT64_MIN -# define INTPTR_MAX INT64_MAX -# define UINTPTR_MAX UINT64_MAX -#else // _WIN64 ][ -# define INTPTR_MIN INT32_MIN -# define INTPTR_MAX INT32_MAX -# define UINTPTR_MAX UINT32_MAX -#endif // _WIN64 ] - -// 7.18.2.5 Limits of greatest-width integer types -#define INTMAX_MIN INT64_MIN -#define INTMAX_MAX INT64_MAX -#define UINTMAX_MAX UINT64_MAX - -// 7.18.3 Limits of other integer types - -#ifdef _WIN64 // [ -# define PTRDIFF_MIN _I64_MIN -# define PTRDIFF_MAX _I64_MAX -#else // _WIN64 ][ -# define PTRDIFF_MIN _I32_MIN -# define PTRDIFF_MAX _I32_MAX -#endif // _WIN64 ] - -#define SIG_ATOMIC_MIN INT_MIN -#define SIG_ATOMIC_MAX INT_MAX - -#ifndef SIZE_MAX // [ -# ifdef _WIN64 // [ -# define SIZE_MAX _UI64_MAX -# else // _WIN64 ][ -# define SIZE_MAX _UI32_MAX -# endif // _WIN64 ] -#endif // SIZE_MAX ] - -// WCHAR_MIN and WCHAR_MAX are also defined in -#ifndef WCHAR_MIN // [ -# define WCHAR_MIN 0 -#endif // WCHAR_MIN ] -#ifndef WCHAR_MAX // [ -# define WCHAR_MAX _UI16_MAX -#endif // WCHAR_MAX ] - -#define WINT_MIN 0 -#define WINT_MAX _UI16_MAX - -#endif // __STDC_LIMIT_MACROS ] - - -// 7.18.4 Limits of other integer types - -#if !defined(__cplusplus) || defined(__STDC_CONSTANT_MACROS) // [ See footnote 224 at page 260 - -// 7.18.4.1 Macros for minimum-width integer constants - -#define INT8_C(val) val##i8 -#define INT16_C(val) val##i16 -#define INT32_C(val) val##i32 -#define INT64_C(val) val##i64 - -#define UINT8_C(val) val##ui8 -#define UINT16_C(val) val##ui16 -#define UINT32_C(val) val##ui32 -#define UINT64_C(val) val##ui64 - -// 7.18.4.2 Macros for greatest-width integer constants -#define INTMAX_C INT64_C -#define UINTMAX_C UINT64_C - -#endif // __STDC_CONSTANT_MACROS ] - - -#endif // _MSC_STDINT_H_ ] diff --git a/lib/libpcpnatpmp/CMakeLists.txt b/lib/libpcpnatpmp/CMakeLists.txt new file mode 100644 index 00000000000..0c765df9bab --- /dev/null +++ b/lib/libpcpnatpmp/CMakeLists.txt @@ -0,0 +1,72 @@ +set (LIBPCP_SRC_FILES + src/net/gateway.c + src/net/findsaddr-udp.c + src/pcp_api.c + src/pcp_client_db.c + src/pcp_event_handler.c + src/pcp_logger.c + src/pcp_msg.c + src/pcp_server_discovery.c + src/net/sock_ntop.c + src/net/pcp_socket.c +) + +if (WIN32) + list (APPEND LIBPCP_SRC_FILES + src/windows/pcp_gettimeofday.c + ) +endif () + +# header files for building PCP/NAT-PMP client library +set (LIBPCP_INC_FILES + include/pcpnatpmp.h + src/pcp_client_db.h + src/pcp_event_handler.h + src/pcp_logger.h + src/pcp_msg.h + src/pcp_server_discovery.h + src/net/unp.h + src/net/pcp_socket.h + src/net/gateway.h + src/net/findsaddr.h +) + +# additional header file when compiling on Windows +if (WIN32) + list (APPEND LIBPCP_INC_FILES + src/windows/pcp_win_defines.h + src/windows/pcp_gettimeofday.h + ) +endif () + +add_library (pcpnatpmp + STATIC + ${LIBPCP_SRC_FILES} + ${LIBPCP_INC_FILES} +) + +target_compile_definitions(pcpnatpmp PRIVATE PCP_USE_IPV6_SOCKET) + +if (WIN32) + target_compile_definitions(pcpnatpmp PRIVATE WIN32) + + if(MINGW) + target_compile_definitions(pcpnatpmp PRIVATE HAVE_GETTIMEOFDAY) + endif(MINGW) + + target_link_libraries (pcpnatpmp INTERFACE ws2_32.lib Iphlpapi.lib) +endif () + +suppress_warnings(pcpnatpmp) + +# include directories with source and header files +target_include_directories (pcpnatpmp PRIVATE src/ src/net/ .) + +# include directories with source and header files +target_include_directories (pcpnatpmp SYSTEM PUBLIC include) + +if (WIN32) + target_include_directories (pcpnatpmp PRIVATE src/windows ../win_utils) +endif () + +set_target_properties(pcpnatpmp PROPERTIES FOLDER "3rdparty") \ No newline at end of file diff --git a/lib/libpcpnatpmp/COPYING b/lib/libpcpnatpmp/COPYING new file mode 100644 index 00000000000..6df1f267bbf --- /dev/null +++ b/lib/libpcpnatpmp/COPYING @@ -0,0 +1,22 @@ +Copyright (c) 2013 by Cisco Systems, Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/lib/libpcp/Makefile.am b/lib/libpcpnatpmp/Makefile.am similarity index 77% rename from lib/libpcp/Makefile.am rename to lib/libpcpnatpmp/Makefile.am index 78c9213afe9..4e629c83f65 100644 --- a/lib/libpcp/Makefile.am +++ b/lib/libpcpnatpmp/Makefile.am @@ -1,13 +1,13 @@ -AM_CPPFLAGS = -I$(srcdir)/include -I$(srcdir)/src/net -I$(srcdir)/src +AM_CPPFLAGS = -I$(srcdir)/include -I$(srcdir)/src/net -I$(srcdir)/src -I$(srcdir)/src/windows AM_CPPFLAGS += $(PCP_CPPFLAGS) AM_CFLAGS = $(PCP_CFLAGS) -pkginclude_HEADERS = include/pcp.h +pkginclude_HEADERS = include/pcpnatpmp.h pkgconfigdir = $(libdir)/pkgconfig -pkgconfig_DATA = libpcp-client.pc +pkgconfig_DATA = libpcpnatpmp.pc -lib_LTLIBRARIES = libpcp-client.la -libpcp_client_la_SOURCES = src/pcp_logger.c\ +lib_LTLIBRARIES = libpcpnatpmp.la +libpcpnatpmp_la_SOURCES = src/pcp_logger.c\ src/pcp_server_discovery.c\ src/pcp_client_db.c\ src/pcp_msg.c\ @@ -30,7 +30,7 @@ noinst_HEADERS = src/net/pcp_socket.h\ src/net/findsaddr.h \ src/net/unp.h -libpcp_client_la_LDFLAGS = -version-info 0:0:0 -release 2 ${PCP_LDFLAGS} +libpcpnatpmp_la_LDFLAGS = -version-info 0:0:0 -release 2 ${PCP_LDFLAGS} -libpcp_client_la_LIBADD = $(GCOVLIB) +libpcpnatpmp_la_LIBADD = $(GCOVLIB) diff --git a/lib/libpcpnatpmp/README.md b/lib/libpcpnatpmp/README.md new file mode 100644 index 00000000000..3d54dfdae41 --- /dev/null +++ b/lib/libpcpnatpmp/README.md @@ -0,0 +1,23 @@ +Port Control Protocol (PCP) and NAT-PMP client library +====================================================== + +Library implements client side of PCP +([RFC 6887](https://datatracker.ietf.org/doc/html/rfc6887)) and +NAT-PMP ([RFC 6886](https://datatracker.ietf.org/doc/html/rfc6886)) protocols. +Switch to NAT-PMP is done automatically by version negotiation. This library +enables any network application to manage network edge device (e.g. to create +NAT mapping or ask router for specific flow treatment). + +Supported platforms are +Linux, Microsoft Windows (Vista and later) and macOS. + +Components +---------- + + - [lib](lib) - Client library + - [cli-client](cli-client) - Command-line interface client + - [test-server](test-server) - Test server + - [scapy](scapy) - PCP layer for Scapy + +Build instructions are located in [INSTALL.md](INSTALL.md) file. +More information about components are in each subdirectory's README.md file. diff --git a/lib/libpcp/src/default_config.h b/lib/libpcpnatpmp/default_config.h similarity index 98% rename from lib/libpcp/src/default_config.h rename to lib/libpcpnatpmp/default_config.h index 1c2dd582142..be3a9676718 100644 --- a/lib/libpcp/src/default_config.h +++ b/lib/libpcpnatpmp/default_config.h @@ -26,7 +26,7 @@ #ifndef DEFAULT_CONFIG_H_ #define DEFAULT_CONFIG_H_ -/* disable NATPMP support */ +/* disable NAT-PMP support */ /* #undef PCP_DISABLE_NATPMP_SUPPORT */ /* enable experimental PCP options support */ diff --git a/lib/libpcp/include/pcp.h b/lib/libpcpnatpmp/include/pcpnatpmp.h similarity index 77% rename from lib/libpcp/include/pcp.h rename to lib/libpcpnatpmp/include/pcpnatpmp.h index 1dec3fc0bdd..2592426f21c 100644 --- a/lib/libpcp/include/pcp.h +++ b/lib/libpcpnatpmp/include/pcpnatpmp.h @@ -28,68 +28,69 @@ #ifdef WIN32 #include + #include + #include + #include -#ifndef __MINGW32__ -#include "stdint.h" -#ifndef ssize_t +#if !defined ssize_t && defined _MSC_VER typedef int ssize_t; #endif -#endif -#else //WIN32 -#include -#include -#include +#else // WIN32 #include +#include +#include #endif +#include + #ifdef __cplusplus extern "C" { #endif #ifdef PCP_SOCKET_IS_VOIDPTR #define PCP_SOCKET void * -#else //PCP_SOCKET_IS_VOIDPTR +#else // PCP_SOCKET_IS_VOIDPTR #ifdef WIN32 #define PCP_SOCKET SOCKET -#else //WIN32 +#else // WIN32 #define PCP_SOCKET int -#endif //WIN32 -#endif //PCP_SOCKET_IS_VOIDPTR +#endif // WIN32 +#endif // PCP_SOCKET_IS_VOIDPTR #ifdef PCP_EXPERIMENTAL typedef struct pcp_userid_option *pcp_userid_option_p; typedef struct pcp_deviceid_option *pcp_deviceid_option_p; typedef struct pcp_location_option *pcp_location_option_p; -#endif //PCP_EXPERIMENTAL +#endif // PCP_EXPERIMENTAL typedef enum { - PCP_ERR_SUCCESS=0, - PCP_ERR_MAX_SIZE=-1, - PCP_ERR_OPT_ALREADY_PRESENT=-2, - PCP_ERR_BAD_AFINET=-3, - PCP_ERR_SEND_FAILED=-4, - PCP_ERR_RECV_FAILED=-5, - PCP_ERR_UNSUP_VERSION=-6, - PCP_ERR_NO_MEM=-7, - PCP_ERR_BAD_ARGS=-8, - PCP_ERR_UNKNOWN=-9, - PCP_ERR_SHORT_LIFETIME_ERR=-10, - PCP_ERR_TIMEOUT=-11, - PCP_ERR_NOT_FOUND=-12, - PCP_ERR_WOULDBLOCK=-13, - PCP_ERR_ADDRINUSE=-14 + PCP_ERR_SUCCESS = 0, + PCP_ERR_MAX_SIZE = -1, + PCP_ERR_OPT_ALREADY_PRESENT = -2, + PCP_ERR_BAD_AFINET = -3, + PCP_ERR_SEND_FAILED = -4, + PCP_ERR_RECV_FAILED = -5, + PCP_ERR_UNSUP_VERSION = -6, + PCP_ERR_NO_MEM = -7, + PCP_ERR_BAD_ARGS = -8, + PCP_ERR_UNKNOWN = -9, + PCP_ERR_SHORT_LIFETIME_ERR = -10, + PCP_ERR_TIMEOUT = -11, + PCP_ERR_NOT_FOUND = -12, + PCP_ERR_WOULDBLOCK = -13, + PCP_ERR_ADDRINUSE = -14 } pcp_errno; /* DEBUG levels */ typedef enum { - PCP_LOGLVL_NONE=0, - PCP_LOGLVL_ERR=1, - PCP_LOGLVL_WARN=2, - PCP_LOGLVL_INFO=3, - PCP_LOGLVL_PERR=4, - PCP_LOGLVL_DEBUG=5 + PCP_LOGLVL_NONE = 0, + PCP_LOGLVL_ERR = 1, + PCP_LOGLVL_WARN = 2, + PCP_LOGLVL_INFO = 3, + PCP_LOGLVL_PERR = 4, + PCP_LOGLVL_DEBUG = 5 } pcp_loglvl_e; typedef void (*external_logger)(pcp_loglvl_e, const char *); @@ -105,27 +106,29 @@ typedef struct pcp_ctx_s pcp_ctx_t; typedef struct pcp_socket_vt_s { PCP_SOCKET (*sock_create)(int domain, int type, int protocol); ssize_t (*sock_recvfrom)(PCP_SOCKET sockfd, void *buf, size_t len, - int flags, struct sockaddr *src_addr, socklen_t *addrlen); + int flags, struct sockaddr *src_addr, + socklen_t *addrlen, struct sockaddr_in6 *dst_addr); ssize_t (*sock_sendto)(PCP_SOCKET sockfd, const void *buf, size_t len, - int flags, struct sockaddr *dest_addr, socklen_t addrlen); + int flags, const struct sockaddr_in6 *src_addr, + struct sockaddr *dest_addr, socklen_t addrlen); int (*sock_close)(PCP_SOCKET sockfd); } pcp_socket_vt_t; /* * Initialize library, optionally initiate auto-discovery of PCP servers * autodiscovery - enable/disable auto-discovery of PCP servers - * socket_vt - optional - virt. table to override default socket functions. - * Pointer has to be valid until pcp_terminate is called. - * Pass NULL to use default socket functions - * return value - pcp context used in other functions. + * socket_vt - optional - virt. table to override default socket + * functions. Pointer has to be valid until pcp_terminate is called. Pass NULL + * to use default socket functions return value - pcp context used in other + * functions. */ -#define ENABLE_AUTODISCOVERY 1 +#define ENABLE_AUTODISCOVERY 1 #define DISABLE_AUTODISCOVERY 0 pcp_ctx_t *pcp_init(uint8_t autodiscovery, pcp_socket_vt_t *socket_vt); -//returns internal pcp server ID, -1 => error occurred +// returns internal pcp server ID, -1 => error occurred int pcp_add_server(pcp_ctx_t *ctx, struct sockaddr *pcp_server, - uint8_t pcp_version); + uint8_t pcp_version); /* * Close socket fds and clean up all settings, frees all library buffers @@ -149,8 +152,8 @@ void pcp_terminate(pcp_ctx_t *ctx, int close_flows); * pcp_flow_t *used in other functions to reference this flow. */ pcp_flow_t *pcp_new_flow(pcp_ctx_t *ctx, struct sockaddr *src_addr, - struct sockaddr *dst_addr, struct sockaddr *ext_addr, uint8_t protocol, - uint32_t lifetime, void *userdata); + struct sockaddr *dst_addr, struct sockaddr *ext_addr, + uint8_t protocol, uint32_t lifetime, void *userdata); void pcp_flow_set_lifetime(pcp_flow_t *f, uint32_t lifetime); @@ -192,7 +195,8 @@ void pcp_flow_set_flowp(pcp_flow_t *f, uint8_t dscp_up, uint8_t dscp_down); * if exists md with given id then replace with new value * if value is NULL then remove metadata with this id */ -void pcp_flow_add_md(pcp_flow_t *f, uint32_t md_id, void *value, size_t val_len); +void pcp_flow_add_md(pcp_flow_t *f, uint32_t md_id, void *value, + size_t val_len); int pcp_flow_set_userid(pcp_flow_t *f, pcp_userid_option_p userid); int pcp_flow_set_deviceid(pcp_flow_t *f, pcp_deviceid_option_p dev); @@ -203,7 +207,7 @@ int pcp_flow_set_location(pcp_flow_t *f, pcp_location_option_p loc); * Append filter option. */ void pcp_flow_set_filter_opt(pcp_flow_t *f, struct sockaddr *filter_ip, - uint8_t filter_prefix); + uint8_t filter_prefix); /* * Append prefer failure option. @@ -216,7 +220,7 @@ void pcp_flow_set_prefer_failure_opt(pcp_flow_t *f); * correct DSCP values to get desired flow treatment by router. */ pcp_flow_t *pcp_learn_dscp(pcp_ctx_t *ctx, uint8_t delay_tol, uint8_t loss_tol, - uint8_t jitter_tol, char *app_name); + uint8_t jitter_tol, char *app_name); #endif /* @@ -239,31 +243,34 @@ typedef enum { } pcp_fstate_e; typedef struct pcp_flow_info { - pcp_fstate_e result; - struct in6_addr pcp_server_ip; - struct in6_addr ext_ip; - uint16_t ext_port; //network byte order - time_t recv_lifetime_end; - time_t lifetime_renew_s; - uint8_t pcp_result_code; - struct in6_addr int_ip; - uint16_t int_port; //network byte order - struct in6_addr dst_ip; - uint16_t dst_port; //network byte order - uint8_t protocol; - uint8_t learned_dscp; //relevant only for flow created by pcp_learn_dscp + pcp_fstate_e result; + struct in6_addr pcp_server_ip; + struct in6_addr ext_ip; + uint16_t ext_port; // network byte order + time_t recv_lifetime_end; + time_t lifetime_renew_s; + uint8_t pcp_result_code; + struct in6_addr int_ip; + uint16_t int_port; // network byte order + uint32_t int_scope_id; + struct in6_addr dst_ip; + uint16_t dst_port; // network byte order + uint8_t protocol; + uint8_t learned_dscp; // relevant only for flow created by pcp_learn_dscp } pcp_flow_info_t; -// Allocates info_buf by malloc, has to be freed by client when no longer needed. +// Allocates info_buf by malloc, has to be freed by client when no longer +// needed. pcp_flow_info_t *pcp_flow_get_info(pcp_flow_t *f, size_t *info_count); -//callback function type - called when flow state has changed +// callback function type - called when flow state has changed typedef void (*pcp_flow_change_notify)(pcp_flow_t *f, struct sockaddr *src_addr, - struct sockaddr *ext_addr, pcp_fstate_e, void *cb_arg); + struct sockaddr *ext_addr, pcp_fstate_e, + void *cb_arg); -//set flow state change notify callback function +// set flow state change notify callback function void pcp_set_flow_change_cb(pcp_ctx_t *ctx, pcp_flow_change_notify cb_fun, - void *cb_arg); + void *cb_arg); /* evaluate flow state * params: @@ -293,7 +300,7 @@ int pcp_pulse(pcp_ctx_t *ctx, struct timeval *next_timeout); */ PCP_SOCKET pcp_get_socket(pcp_ctx_t *ctx); -//example of pcp_pulse and pcp_get_socket use in select loop: +// example of pcp_pulse and pcp_get_socket use in select loop: /* pcp_ctx_t *ctx=pcp_init(1, NULL); int sock=pcp_get_socket(ctx); diff --git a/lib/libpcp/libpcp-client.pc.in b/lib/libpcpnatpmp/libpcpnatpmp.pc.in similarity index 50% rename from lib/libpcp/libpcp-client.pc.in rename to lib/libpcpnatpmp/libpcpnatpmp.pc.in index b9c22462d59..6c106d5935f 100644 --- a/lib/libpcp/libpcp-client.pc.in +++ b/lib/libpcpnatpmp/libpcpnatpmp.pc.in @@ -1,16 +1,16 @@ -#libpcp-client pkg-config source file +#libpcpnatpmp pkg-config source file prefix=@prefix@ exec_prefix=@exec_prefix@ libdir=@libdir@ includedir=@includedir@ -Name: libpcp-client -Description: library implementing a PCP (Port Control Protocol) client +Name: libpcpnatpmp +Description: library implementing a Port Control Protocol (PCP) and NAT-PMP client Version: @VERSION@ Requires: Conflicts: -Libs: -L${libdir} -lpcp-client +Libs: -L${libdir} -lpcpnatpmp Libs.private: @LIBS@ Cflags: -I${includedir} diff --git a/lib/libpcp/src/net/findsaddr-udp.c b/lib/libpcpnatpmp/src/net/findsaddr-udp.c similarity index 72% rename from lib/libpcp/src/net/findsaddr-udp.c rename to lib/libpcpnatpmp/src/net/findsaddr-udp.c index fc8d2a6d4e6..576a287c7f6 100644 --- a/lib/libpcp/src/net/findsaddr-udp.c +++ b/lib/libpcpnatpmp/src/net/findsaddr-udp.c @@ -34,17 +34,18 @@ #include #ifndef WIN32 -# include -# include #include +#include +#include #else -# include -# include /*sockaddr, addrinfo etc.*/ -# include +#include +#include /*sockaddr, addrinfo etc.*/ + +#include #endif /*WIN32*/ -#include "pcp.h" #include "findsaddr.h" +#include "pcpnatpmp.h" #include "unp.h" /* @@ -69,39 +70,38 @@ #endif const char *findsaddr(register const struct sockaddr_in *to, - struct in6_addr *from) -{ + struct in6_addr *from) { const char *errstr; struct sockaddr_in cto, cfrom; SOCKET s; socklen_t len; - s=socket(AF_INET, SOCK_DGRAM, 0); + s = socket(AF_INET, SOCK_DGRAM, 0); if (s == INVALID_SOCKET) return ("failed to open DGRAM socket for src addr selection."); - errstr=NULL; - len=sizeof(struct sockaddr_in); + errstr = NULL; + len = sizeof(struct sockaddr_in); memcpy(&cto, to, len); - cto.sin_port=htons(65535); /* Dummy port for connect(2). */ + cto.sin_port = htons(65535); /* Dummy port for connect(2). */ if (connect(s, (struct sockaddr *)&cto, len) == -1) { - errstr="failed to connect to peer for src addr selection."; + errstr = "failed to connect to peer for src addr selection."; goto err; } if (getsockname(s, (struct sockaddr *)&cfrom, &len) == -1) { - errstr="failed to get socket name for src addr selection."; + errstr = "failed to get socket name for src addr selection."; goto err; } if (len != sizeof(struct sockaddr_in) || cfrom.sin_family != AF_INET) { - errstr="unexpected address family in src addr selection."; + errstr = "unexpected address family in src addr selection."; goto err; } - ((uint32_t *)from)[0]=0; - ((uint32_t *)from)[1]=0; - ((uint32_t *)from)[2]=htonl(0xffff); - ((uint32_t *)from)[3]=cfrom.sin_addr.s_addr; + ((uint32_t *)from)[0] = 0; + ((uint32_t *)from)[1] = 0; + ((uint32_t *)from)[2] = htonl(0xffff); + ((uint32_t *)from)[3] = cfrom.sin_addr.s_addr; err: (void)CLOSE(s); @@ -111,54 +111,57 @@ const char *findsaddr(register const struct sockaddr_in *to, } const char *findsaddr6(register const struct sockaddr_in6 *to, - register struct in6_addr *from) -{ + register struct in6_addr *from, + uint32_t *from_scope_id) { const char *errstr; struct sockaddr_in6 cto, cfrom; SOCKET s; socklen_t len; - uint32_t sock_flg=0; + uint32_t sock_flg = 0; if (IN6_IS_ADDR_LOOPBACK(&to->sin6_addr)) { memcpy(from, &to->sin6_addr, sizeof(struct in6_addr)); return NULL; } - s=socket(AF_INET6, SOCK_DGRAM, 0); + s = socket(AF_INET6, SOCK_DGRAM, 0); if (s == INVALID_SOCKET) return ("failed to open DGRAM socket for src addr selection."); - errstr=NULL; + errstr = NULL; - //Enable Dual-stack socket for Vista and higher + // Enable Dual-stack socket for Vista and higher if (setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&sock_flg, - sizeof(sock_flg)) == -1) { - errstr="setsockopt failed to set dual stack mode."; + sizeof(sock_flg)) == -1) { + errstr = "setsockopt failed to set dual stack mode."; goto err; } - len=sizeof(struct sockaddr_in6); + len = sizeof(struct sockaddr_in6); memcpy(&cto, to, len); - cto.sin6_port=htons(65535); /* Dummy port for connect(2). */ + cto.sin6_port = htons(65535); /* Dummy port for connect(2). */ if (connect(s, (struct sockaddr *)&cto, len) == -1) { - errstr="failed to connect to peer for src addr selection."; + errstr = "failed to connect to peer for src addr selection."; goto err; } if (getsockname(s, (struct sockaddr *)&cfrom, &len) == -1) { - errstr="failed to get socket name for src addr selection."; + errstr = "failed to get socket name for src addr selection."; goto err; } if (len != sizeof(struct sockaddr_in6) || cfrom.sin6_family != AF_INET6) { - errstr="unexpected address family in src addr selection."; + errstr = "unexpected address family in src addr selection."; goto err; } memcpy(from->s6_addr, cfrom.sin6_addr.s6_addr, sizeof(struct in6_addr)); + if (from_scope_id) { + *from_scope_id = cfrom.sin6_scope_id; + } err: - (void) CLOSE(s); + (void)CLOSE(s); /* No error (string) to return. */ return (errstr); diff --git a/lib/libpcp/src/net/findsaddr.h b/lib/libpcpnatpmp/src/net/findsaddr.h similarity index 90% rename from lib/libpcp/src/net/findsaddr.h rename to lib/libpcpnatpmp/src/net/findsaddr.h index c849443d768..25e25a53a71 100644 --- a/lib/libpcp/src/net/findsaddr.h +++ b/lib/libpcpnatpmp/src/net/findsaddr.h @@ -27,9 +27,9 @@ #define FINDSADDR_UDP_H_ const char *findsaddr(register const struct sockaddr_in *to, - struct in6_addr *from); + struct in6_addr *from); const char *findsaddr6(register const struct sockaddr_in6 *to, - register struct in6_addr *from); + register struct in6_addr *from, uint32_t *from_scope_id); -#endif //FINDSADDR_UDP_H_ +#endif // FINDSADDR_UDP_H_ diff --git a/lib/libpcp/src/net/gateway.c b/lib/libpcpnatpmp/src/net/gateway.c similarity index 67% rename from lib/libpcp/src/net/gateway.c rename to lib/libpcpnatpmp/src/net/gateway.c index fade3e10237..f314c2f4e5a 100644 --- a/lib/libpcp/src/net/gateway.c +++ b/lib/libpcpnatpmp/src/net/gateway.c @@ -29,82 +29,99 @@ #include "default_config.h" #endif -#include -#include -#include #include #include #include - #ifndef WIN32 -#include // place it before struct sockaddr -#include // ioctl() -#include // inet_addr() -#include // struct rt_msghdr -#include -#include -#include //IPPROTO_GRE sturct sockaddr_in INADDR_ANY -#endif //WIN32 - -#if defined(__linux__) +#include // place it before struct sockaddr +#endif // WIN32 +#ifdef __linux__ #define USE_NETLINK -#elif defined(WIN32) -#define USE_WIN32_CODE -#elif defined(__APPLE__) || defined(__FreeBSD__) -#define USE_SYSCTL_NET_ROUTE -#elif defined(BSD) || defined(__FreeBSD_kernel__) -#define USE_SYSCTL_NET_ROUTE -#elif (defined(sun) && defined(__SVR4)) -#define USE_SOCKET_ROUTE -#endif - -#ifdef USE_NETLINK -#include #include #include +#include #endif -#ifdef USE_WIN32_CODE +#include +#include +#include + +#ifndef WIN32 +#include //struct ifreq +#include // ioctl() +#endif // WIN32 +#ifdef WIN32 +#undef USE_NETLINK +#undef USE_SOCKET_ROUTE +#define USE_WIN32_CODE + #include #include -#include + +#include + #include + #include "pcp_win_defines.h" #endif -#ifdef USE_SYSCTL_NET_ROUTE +#if defined(__APPLE__) || defined(__FreeBSD__) +#include //struct sockaddr_dl + #include -#include //struct sockaddr_dl +#define USE_SOCKET_ROUTE #endif +#ifndef WIN32 +#include // inet_addr() +#include // struct rt_msghdr #ifdef USE_SOCKET_ROUTE -#include //getifaddrs() freeifaddrs() +#include //getifaddrs() freeifaddrs() +#endif +#include +#include +#include //IPPROTO_GRE sturct sockaddr_in INADDR_ANY +#endif + +#if defined(BSD) || defined(__FreeBSD_kernel__) +#define USE_SOCKET_ROUTE +#undef USE_WIN32_CODE +#endif + +#if (defined(sun) && defined(__SVR4)) +#define USE_SOCKET_ROUTE +#undef USE_WIN32_CODE #endif #include "gateway.h" #include "pcp_logger.h" -#include "unp.h" #include "pcp_utils.h" +#include "unp.h" +#ifndef WIN32 +#define SUCCESS (0) +#define FAILED (-1) +#define USE_WIN32_CODE +#endif -#define TO_IPV6MAPPED(x) S6_ADDR32(x)[3] = S6_ADDR32(x)[0];\ - S6_ADDR32(x)[0] = 0;\ - S6_ADDR32(x)[1] = 0;\ - S6_ADDR32(x)[2] = htonl(0xFFFF); +#define TO_IPV6MAPPED(x) \ + S6_ADDR32(x)[3] = S6_ADDR32(x)[0]; \ + S6_ADDR32(x)[0] = 0; \ + S6_ADDR32(x)[1] = 0; \ + S6_ADDR32(x)[2] = htonl(0xFFFF); -#if defined(USE_NETLINK) +#ifdef USE_NETLINK #define BUFSIZE 8192 static ssize_t readNlSock(int sockFd, char *bufPtr, unsigned seqNum, - unsigned pId) -{ + unsigned pId) { struct nlmsghdr *nlHdr; - ssize_t readLen=0, msgLen=0; + ssize_t readLen = 0, msgLen = 0; do { /* Receive response from the kernel */ - readLen=recv(sockFd, bufPtr, BUFSIZE - msgLen, 0); + readLen = recv(sockFd, bufPtr, BUFSIZE - msgLen, 0); if (readLen == -1) { char errmsg[128]; pcp_strerror(errno, errmsg, sizeof(errmsg)); @@ -112,11 +129,11 @@ static ssize_t readNlSock(int sockFd, char *bufPtr, unsigned seqNum, return -1; } - nlHdr=(struct nlmsghdr *)bufPtr; + nlHdr = (struct nlmsghdr *)bufPtr; /* Check if the header is valid */ - if ((NLMSG_OK(nlHdr, (unsigned)readLen) == 0) - || (nlHdr->nlmsg_type == NLMSG_ERROR)) { + if ((NLMSG_OK(nlHdr, (unsigned)readLen) == 0) || + (nlHdr->nlmsg_type == NLMSG_ERROR)) { PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Error in received packet"); return -1; } @@ -126,8 +143,8 @@ static ssize_t readNlSock(int sockFd, char *bufPtr, unsigned seqNum, break; } else { /* Else move the pointer to buffer appropriately */ - bufPtr+=readLen; - msgLen+=readLen; + bufPtr += readLen; + msgLen += readLen; } /* Check if its a multi part message */ @@ -139,12 +156,11 @@ static ssize_t readNlSock(int sockFd, char *bufPtr, unsigned seqNum, return msgLen; } -int getgateways(struct sockaddr_in6 **gws) -{ +int getgateways(struct sockaddr_in6 **gws) { struct nlmsghdr *nlMsg; char msgBuf[BUFSIZE]; - int sock, msgSeq=0; + int sock, msgSeq = 0; ssize_t len; int ret; @@ -153,7 +169,7 @@ int getgateways(struct sockaddr_in6 **gws) } /* Create Socket */ - sock=socket(PF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE); + sock = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE); if (sock < 0) { PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Netlink Socket Creation Failed..."); return PCP_ERR_UNKNOWN; @@ -163,55 +179,58 @@ int getgateways(struct sockaddr_in6 **gws) memset(msgBuf, 0, BUFSIZE); /* point the header and the msg structure pointers into the buffer */ - nlMsg=(struct nlmsghdr *)msgBuf; + nlMsg = (struct nlmsghdr *)msgBuf; /* Fill in the nlmsg header*/ - nlMsg->nlmsg_len=NLMSG_LENGTH(sizeof(struct rtmsg)); // Length of message. - nlMsg->nlmsg_type=RTM_GETROUTE; // Get the routes from kernel routing table. + nlMsg->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); // Length of message. + nlMsg->nlmsg_type = + RTM_GETROUTE; // Get the routes from kernel routing table. - nlMsg->nlmsg_flags=NLM_F_DUMP | NLM_F_REQUEST; // The message is a request for dump. - nlMsg->nlmsg_seq=msgSeq++; // Sequence of the message packet. - nlMsg->nlmsg_pid=getpid(); // PID of process sending the request. + nlMsg->nlmsg_flags = + NLM_F_DUMP | NLM_F_REQUEST; // The message is a request for dump. + nlMsg->nlmsg_seq = msgSeq++; // Sequence of the message packet. + nlMsg->nlmsg_pid = getpid(); // PID of process sending the request. /* Send the request */ - len=send(sock, nlMsg, nlMsg->nlmsg_len, 0); + len = send(sock, nlMsg, nlMsg->nlmsg_len, 0); if (len == -1) { PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Write To Netlink Socket Failed..."); - ret=PCP_ERR_SEND_FAILED; + ret = PCP_ERR_SEND_FAILED; goto end; } /* Read the response */ - len=readNlSock(sock, msgBuf, msgSeq, getpid()); + len = readNlSock(sock, msgBuf, msgSeq, getpid()); if (len < 0) { PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Read From Netlink Socket Failed..."); - ret=PCP_ERR_RECV_FAILED; + ret = PCP_ERR_RECV_FAILED; goto end; } /* Parse and print the response */ - ret=0; + ret = 0; - for (; NLMSG_OK(nlMsg,(unsigned)len); nlMsg=NLMSG_NEXT(nlMsg,len)) { + for (; NLMSG_OK(nlMsg, (unsigned)len); nlMsg = NLMSG_NEXT(nlMsg, len)) { struct rtmsg *rtMsg; struct rtattr *rtAttr; int rtLen; unsigned int scope_id = 0; struct in6_addr addr; int found = 0; - rtMsg=(struct rtmsg *)NLMSG_DATA(nlMsg); + rtMsg = (struct rtmsg *)NLMSG_DATA(nlMsg); /* If the route is not for AF_INET(6) or does not belong to main routing table then return. */ - if (((rtMsg->rtm_family != AF_INET) && (rtMsg->rtm_family != AF_INET6)) - || ((rtMsg->rtm_type != RTN_UNICAST) && (rtMsg->rtm_table != RT_TABLE_MAIN))) { + if (((rtMsg->rtm_family != AF_INET) && + (rtMsg->rtm_family != AF_INET6)) || + (rtMsg->rtm_table != RT_TABLE_MAIN)) { continue; } /* get the rtattr field */ - rtAttr=(struct rtattr *)RTM_RTA(rtMsg); - rtLen=RTM_PAYLOAD(nlMsg); - for (; RTA_OK(rtAttr,rtLen); rtAttr=RTA_NEXT(rtAttr,rtLen)) { - size_t rtaLen=RTA_PAYLOAD(rtAttr); + rtAttr = (struct rtattr *)RTM_RTA(rtMsg); + rtLen = RTM_PAYLOAD(nlMsg); + for (; RTA_OK(rtAttr, rtLen); rtAttr = RTA_NEXT(rtAttr, rtLen)) { + size_t rtaLen = RTA_PAYLOAD(rtAttr); if (rtaLen > sizeof(struct in6_addr)) { continue; } @@ -223,27 +242,26 @@ int getgateways(struct sockaddr_in6 **gws) if (rtMsg->rtm_family == AF_INET) { TO_IPV6MAPPED(&addr); } - found=1; + found = 1; } } if (found) { struct sockaddr_in6 *tmp_gws; - tmp_gws=(struct sockaddr_in6 *)realloc(*gws, - sizeof(struct sockaddr_in6) * (ret + 1)); + tmp_gws = (struct sockaddr_in6 *)realloc( + *gws, sizeof(struct sockaddr_in6) * (ret + 1)); if (!tmp_gws) { - PCP_LOG(PCP_LOGLVL_ERR, "%s", - "Error allocating memory"); + PCP_LOG(PCP_LOGLVL_ERR, "%s", "Error allocating memory"); if (*gws) { free(*gws); - *gws=NULL; + *gws = NULL; } - ret=PCP_ERR_NO_MEM; + ret = PCP_ERR_NO_MEM; goto end; } - *gws=tmp_gws; - (*gws + ret)->sin6_family=AF_INET6; + *gws = tmp_gws; + (*gws + ret)->sin6_family = AF_INET6; memcpy(&((*gws + ret)->sin6_addr), &addr, sizeof(addr)); - (*gws + ret)->sin6_scope_id=scope_id; + (*gws + ret)->sin6_scope_id = scope_id; SET_SA_LEN(*gws + ret, sizeof(struct sockaddr_in6)) ret++; } @@ -255,13 +273,14 @@ int getgateways(struct sockaddr_in6 **gws) return ret; } -#elif defined(USE_WIN32_CODE) +#endif /* #ifdef USE_NETLINK */ -#if 0 // WINVER>=NTDDI_VISTA -int getgateways(struct in6_addr **gws) -{ +#if defined(USE_WIN32_CODE) && defined(WIN32) + +int getgateways(struct sockaddr_in6 **gws) { PMIB_IPFORWARD_TABLE2 ipf_table; unsigned int i; + int ret = 0; if (!gws) { return PCP_ERR_UNKNOWN; @@ -275,245 +294,39 @@ int getgateways(struct in6_addr **gws) return PCP_ERR_UNKNOWN; } - *gws=(struct in6_addr *)calloc(ipf_table->NumEntries, - sizeof(struct in6_addr)); - if (*gws) { - PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Error allocating memory"); - return PCP_ERR_NO_MEM; - } - - for (i=0; i < ipf_table->NumEntries; ++i) { - if (ipf_table->Table[i].NextHop.si_family == AF_INET) { - S6_ADDR32((*gws)+i)[0]= - ipf_table->Table[i].NextHop.Ipv4.sin_addr.s_addr; - TO_IPV6MAPPED(((*gws)+i)); - } - if (ipf_table->Table[i].NextHop.si_family == AF_INET6) { - memcpy((*gws) + i, &ipf_table->Table[i].NextHop.Ipv6.sin6_addr, - sizeof(struct in6_addr)); - } - } - return i; -} -#else -int getgateways(struct sockaddr_in6 **gws) -{ - PMIB_IPFORWARDTABLE ipf_table; - DWORD ipft_size=0; - int i, ret; - - if (!gws) { - return PCP_ERR_UNKNOWN; - } - - ipf_table=(MIB_IPFORWARDTABLE *)malloc(sizeof(MIB_IPFORWARDTABLE)); - if (!ipf_table) { - PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Error allocating memory"); - ret=PCP_ERR_NO_MEM; - goto end; - } - - if (GetIpForwardTable(ipf_table, &ipft_size, 0) - == ERROR_INSUFFICIENT_BUFFER) { - MIB_IPFORWARDTABLE *new_ipf_table; - new_ipf_table=(MIB_IPFORWARDTABLE *)realloc(ipf_table, ipft_size); - if (!new_ipf_table) { - PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Error allocating memory"); - ret=PCP_ERR_NO_MEM; - goto end; - } - ipf_table=new_ipf_table; - } - - if (GetIpForwardTable(ipf_table, &ipft_size, 0) != NO_ERROR) { - PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "GetIpForwardTable failed."); - ret=PCP_ERR_UNKNOWN; - goto end; - } - - *gws=(struct sockaddr_in6 *)calloc(ipf_table->dwNumEntries, - sizeof(struct sockaddr_in6)); + *gws = (struct sockaddr_in6 *)calloc(ipf_table->NumEntries, + sizeof(struct sockaddr_in6)); if (!*gws) { PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Error allocating memory"); - ret=PCP_ERR_NO_MEM; - goto end; + return PCP_ERR_NO_MEM; } - for (ret=0, i=0; i < (int)ipf_table->dwNumEntries; i++) { - if (ipf_table->table[i].dwForwardType == MIB_IPROUTE_TYPE_INDIRECT) { + for (i = 0; i < ipf_table->NumEntries; ++i) { + MIB_IPFORWARD_ROW2 *row = ipf_table->Table + i; + if ((row->NextHop.si_family == AF_INET6) && + (IPV6_IS_ADDR_ANY(&row->NextHop.Ipv6.sin6_addr))) { + continue; + } else if ((row->NextHop.si_family == AF_INET) && + (&row->NextHop.Ipv6.sin6_addr == INADDR_ANY)) { + continue; + } else if (row->NextHop.si_family == AF_INET) { (*gws)[ret].sin6_family = AF_INET6; - S6_ADDR32(&(*gws)[ret].sin6_addr)[0]= - (uint32_t)ipf_table->table[i].dwForwardNextHop; + S6_ADDR32(&(*gws)[ret].sin6_addr) + [0] = row->NextHop.Ipv4.sin_addr.s_addr; TO_IPV6MAPPED(&(*gws)[ret].sin6_addr); - ret++; + ++ret; + } else if (row->NextHop.si_family == AF_INET6) { + memcpy((&(*gws)[ret]), &row->NextHop.Ipv6, + sizeof(struct sockaddr_in6)); + ++ret; } } -end: - if (ipf_table) - free(ipf_table); - + FreeMibTable(ipf_table); return ret; } -#endif - -#elif defined(USE_SOCKET_ROUTE) - -/* Adapted from Richard Stevens, UNIX Network Programming */ +#endif /* #ifdef USE_WIN32_CODE */ -#ifdef HAVE_SOCKADDR_SA_LEN -/* - * Round up 'a' to next multiple of 'size', which must be a power of 2 - */ -#define ROUNDUP(a, size) (((a) & ((size)-1)) ? (1 + ((a) | ((size)-1))) : (a)) -#else -#define ROUNDUP(a, size) (a) -#endif - -/* - * Step to next socket address structure; - * if sa_len is 0, assume it is sizeof(u_long). Using u_long only works on 32-bit - machines. In 64-bit machines it needs to be u_int32_t !! - */ -#define NEXT_SA(ap) ap = (struct sockaddr *) \ - ((caddr_t) ap + (SA_LEN(ap) ? ROUNDUP(SA_LEN(ap), sizeof(uint32_t)) : sizeof(uint32_t))) - - -#define NEXTADDR_CT(w, u) \ - if (msg.msghdr.rtm_addrs & (w)) {\ - len = SA_LEN(&(u)); memmove(cp, &(u), len); cp += len;\ - } - -/* thanks Stevens for this very handy function */ -static void get_rtaddrs(int addrs, struct sockaddr *sa, - struct sockaddr **rti_info) -{ - int i; - - for (i=0; i < RTAX_MAX; i++) { - if (addrs & (1 << i)) { - rti_info[i]=sa; - NEXT_SA(sa); - } else - rti_info[i]=NULL; - } -} - -int getgateways(struct sockaddr_in6 **gws) -{ - static int seq=0; - int err=0; - ssize_t len=0; - char *cp; - pid_t pid; - int rtcount=0; - struct sockaddr so_dst, so_mask; - - struct { - struct rt_msghdr msghdr; - char buf[512]; - } msg; - - if (!gws) { - return PCP_ERR_UNKNOWN; - } - - memset(&msg, 0, sizeof(msg)); - memset(&so_dst, 0, sizeof(so_dst)); - memset(&so_mask, 0 ,sizeof(so_mask)); - - cp=msg.buf; - pid=getpid(); - - msg.msghdr.rtm_type=RTM_GET; - msg.msghdr.rtm_version=RTM_VERSION; - msg.msghdr.rtm_pid=pid; - msg.msghdr.rtm_addrs=RTA_DST | RTA_NETMASK; - msg.msghdr.rtm_seq=++seq; - msg.msghdr.rtm_flags=RTF_UP | RTF_GATEWAY; - - so_dst.sa_family = AF_INET; - so_mask.sa_family = AF_INET; - - NEXTADDR_CT(RTA_DST, so_dst); - NEXTADDR_CT(RTA_NETMASK, so_mask); - - msg.msghdr.rtm_msglen=len=cp - (char *)&msg; - - int sock=socket(PF_ROUTE, SOCK_RAW, 0); - if (sock == -1) { - return PCP_ERR_UNKNOWN; - } - - if (write(sock, (char *)&msg, len) < 0) { - close(sock); - return PCP_ERR_UNKNOWN; - } - - - do { - len=read(sock, (char *)&msg, sizeof(msg)); - } while (len > 0 - && (msg.msghdr.rtm_seq != seq || msg.msghdr.rtm_pid != pid)); - - close(sock); - - if (len < 0) { - return PCP_ERR_UNKNOWN; - } else { - struct sockaddr *sa; - struct sockaddr *rti_info[RTAX_MAX]; - - if (msg.msghdr.rtm_version != RTM_VERSION) { - return PCP_ERR_UNKNOWN; - } - - if (msg.msghdr.rtm_errno) { - return PCP_ERR_UNKNOWN; - } - - cp=msg.buf; - if (msg.msghdr.rtm_addrs) { - sa=(struct sockaddr *)cp; - get_rtaddrs(msg.msghdr.rtm_addrs, sa, rti_info); - - if ((sa=rti_info[RTAX_GATEWAY]) != NULL) { - if ((msg.msghdr.rtm_addrs & (RTA_DST | RTA_GATEWAY)) - == (RTA_DST | RTA_GATEWAY)) { - struct sockaddr_in6 *in6=*gws; - - *gws=(struct sockaddr_in6 *)realloc(*gws, - sizeof(struct sockaddr_in6) * (rtcount + 1)); - - if (!*gws) { - if (in6) - free(in6); - return PCP_ERR_NO_MEM; - } - - in6=(*gws) + rtcount; - memset(in6, 0, sizeof(struct sockaddr_in6)); - - if (sa->sa_family == AF_INET) { - /* IPv4 gateways as returned as IPv4 mapped IPv6 addresses */ - in6->sin6_family = AF_INET6; - S6_ADDR32(&in6->sin6_addr)[0]= - ((struct sockaddr_in *)(rti_info[RTAX_GATEWAY]))->sin_addr.s_addr; - TO_IPV6MAPPED(&in6->sin6_addr); - } else if (sa->sa_family == AF_INET6) { - memcpy(in6, - (struct sockaddr_in6 *)rti_info[RTAX_GATEWAY], - sizeof(struct sockaddr_in6)); - } - rtcount++; - } - } - } - } - - return rtcount; -} - -#elif defined(USE_SYSCTL_NET_ROUTE) +#ifdef USE_SOCKET_ROUTE struct sockaddr; struct in6_addr; @@ -527,47 +340,46 @@ struct in6_addr; /* * Step to next socket address structure; - * if sa_len is 0, assume it is sizeof(u_long). Using u_long only works on 32-bit - machines. In 64-bit machines it needs to be u_int32_t !! + * if sa_len is 0, assume it is sizeof(u_long). Using u_long only works on + 32-bit machines. In 64-bit machines it needs to be u_int32_t !! */ -#define NEXT_SA(ap) ap = (struct sockaddr *) \ - ((caddr_t) ap + (ap->sa_len ? ROUNDUP(ap->sa_len, sizeof(uint32_t)) : \ - sizeof(uint32_t))) +#define NEXT_SA(ap) \ + ap = (struct sockaddr *)((caddr_t)ap + \ + (ap->sa_len \ + ? ROUNDUP(ap->sa_len, sizeof(uint32_t)) \ + : sizeof(uint32_t))) /* thanks Stevens for this very handy function */ static void get_rtaddrs(int addrs, struct sockaddr *sa, - struct sockaddr **rti_info) -{ + struct sockaddr **rti_info) { int i; - for (i=0; i < RTAX_MAX; i++) { + for (i = 0; i < RTAX_MAX; i++) { if (addrs & (1 << i)) { - rti_info[i]=sa; + rti_info[i] = sa; NEXT_SA(sa); } else - rti_info[i]=NULL; + rti_info[i] = NULL; } } /* Portable (hopefully) function to lookup routing tables. sysctl()'s advantage is that it does not need root permissions. Routing sockets need root permission since it is of type SOCK_RAW. */ -static char * -net_rt_dump(int type, int family, int flags, size_t *lenp) -{ +static char *net_rt_dump(int type, int family, int flags, size_t *lenp) { int mib[6]; char *buf; - mib[0]=CTL_NET; - mib[1]=AF_ROUTE; - mib[2]=0; - mib[3]=family; /* only addresses of this family */ - mib[4]=type; - mib[5]=flags; /* not looked at with NET_RT_DUMP */ + mib[0] = CTL_NET; + mib[1] = AF_ROUTE; + mib[2] = 0; + mib[3] = family; /* only addresses of this family */ + mib[4] = type; + mib[5] = flags; /* not looked at with NET_RT_DUMP */ if (sysctl(mib, 6, NULL, lenp, NULL, 0) < 0) return (NULL); - if ((buf=malloc(*lenp)) == NULL) + if ((buf = malloc(*lenp)) == NULL) return (NULL); if (sysctl(mib, 6, buf, lenp, NULL, 0) < 0) return (NULL); @@ -581,36 +393,35 @@ net_rt_dump(int type, int family, int flags, size_t *lenp) It is up to the caller to weed out duplicates */ -int getgateways(struct sockaddr_in6 **gws) -{ +int getgateways(struct sockaddr_in6 **gws) { char *buf, *next, *lim; size_t len; struct rt_msghdr *rtm; struct sockaddr *sa, *rti_info[RTAX_MAX]; - int rtcount=0; + int rtcount = 0; if (!gws) { return PCP_ERR_UNKNOWN; } /* net_rt_dump() will return all route entries with gateways */ - buf=net_rt_dump(NET_RT_FLAGS, 0, RTF_GATEWAY, &len); + buf = net_rt_dump(NET_RT_FLAGS, 0, RTF_GATEWAY, &len); if (!buf) return PCP_ERR_UNKNOWN; - lim=buf + len; - for (next=buf; next < lim; next+=rtm->rtm_msglen) { - rtm=(struct rt_msghdr *)next; - sa=(struct sockaddr *)(rtm + 1); + lim = buf + len; + for (next = buf; next < lim; next += rtm->rtm_msglen) { + rtm = (struct rt_msghdr *)next; + sa = (struct sockaddr *)(rtm + 1); get_rtaddrs(rtm->rtm_addrs, sa, rti_info); - if ((sa=rti_info[RTAX_GATEWAY]) != NULL) + if ((sa = rti_info[RTAX_GATEWAY]) != NULL) - if ((rtm->rtm_addrs & (RTA_DST | RTA_GATEWAY)) - == (RTA_DST | RTA_GATEWAY)) { - struct sockaddr_in6 *in6=*gws; + if ((rtm->rtm_addrs & (RTA_DST | RTA_GATEWAY)) == + (RTA_DST | RTA_GATEWAY)) { + struct sockaddr_in6 *in6 = *gws; - *gws=(struct sockaddr_in6 *)realloc(*gws, - sizeof(struct sockaddr_in6) * (rtcount + 1)); + *gws = (struct sockaddr_in6 *)realloc( + *gws, sizeof(struct sockaddr_in6) * (rtcount + 1)); if (!*gws) { if (in6) @@ -619,19 +430,20 @@ int getgateways(struct sockaddr_in6 **gws) return PCP_ERR_NO_MEM; } - in6=(*gws) + rtcount; + in6 = (*gws) + rtcount; memset(in6, 0, sizeof(struct sockaddr_in6)); if (sa->sa_family == AF_INET) { - /* IPv4 gateways as returned as IPv4 mapped IPv6 addresses */ + /* IPv4 gateways as returned as IPv4 mapped IPv6 addresses + */ in6->sin6_family = AF_INET6; - S6_ADDR32(&in6->sin6_addr)[0]= - ((struct sockaddr_in *)(rti_info[RTAX_GATEWAY]))->sin_addr.s_addr; + S6_ADDR32(&in6->sin6_addr) + [0] = ((struct sockaddr_in *)(rti_info[RTAX_GATEWAY])) + ->sin_addr.s_addr; TO_IPV6MAPPED(&in6->sin6_addr); } else if (sa->sa_family == AF_INET6) { - memcpy(in6, - (struct sockaddr_in6 *)rti_info[RTAX_GATEWAY], - sizeof(struct sockaddr_in6)); + memcpy(in6, (struct sockaddr_in6 *)rti_info[RTAX_GATEWAY], + sizeof(struct sockaddr_in6)); } else { continue; } @@ -718,12 +530,15 @@ static int route_op(u_char op, in_addr_t *dst, in_addr_t *mask, route_out_t *routeout) { -#define ROUNDUP_CT(n) ((n) > 0 ? (1 + (((n) - 1) | (sizeof(uint32_t) - 1))) : sizeof(uint32_t)) +#define ROUNDUP_CT(n) \ + ((n) > 0 ? (1 + (((n)-1) | (sizeof(uint32_t) - 1))) : sizeof(uint32_t)) #define ADVANCE_CT(x, n) (x += ROUNDUP_CT((n)->sa_len)) -#define NEXTADDR_CT(w, u) \ - if (msg.msghdr.rtm_addrs & (w)) {\ - len = ROUNDUP_CT(u.sa.sa_len); bcopy((char *)&(u), cp, len); cp += len;\ +#define NEXTADDR_CT(w, u) \ + if (msg.msghdr.rtm_addrs & (w)) { \ + len = ROUNDUP_CT(u.sa.sa_len); \ + bcopy((char *)&(u), cp, len); \ + cp += len; \ } static int seq=0; @@ -1093,6 +908,6 @@ static int get_if_addr_from_name(char *ifname, struct sockaddr *ifsock, freeifaddrs(ifaddr); return -1; } -#endif //0 +#endif // 0 #endif diff --git a/lib/libpcp/src/net/gateway.h b/lib/libpcpnatpmp/src/net/gateway.h similarity index 100% rename from lib/libpcp/src/net/gateway.h rename to lib/libpcpnatpmp/src/net/gateway.h diff --git a/lib/libpcpnatpmp/src/net/pcp_socket.c b/lib/libpcpnatpmp/src/net/pcp_socket.c new file mode 100644 index 00000000000..36cac6fb019 --- /dev/null +++ b/lib/libpcpnatpmp/src/net/pcp_socket.c @@ -0,0 +1,567 @@ +/* + Copyright (c) 2014 by Cisco Systems, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#define _GNU_SOURCE + +#ifdef HAVE_CONFIG_H +#include "config.h" +#else +#include "default_config.h" +#endif + +#include +#include +#include +#ifdef WIN32 +#include "pcp_win_defines.h" + +#include +#include +#else // WIN32 +#include +#include +#ifndef PCP_SOCKET_IS_VOIDPTR +#include +#include +#include +#endif // PCP_SOCKET_IS_VOIDPTR +#endif //! WIN32 +#include "pcpnatpmp.h" + +#include "pcp_socket.h" +#include "pcp_utils.h" +#include "unp.h" + +static PCP_SOCKET pcp_socket_create_impl(int domain, int type, int protocol); +static ssize_t pcp_socket_recvfrom_impl(PCP_SOCKET sock, void *buf, size_t len, + int flags, struct sockaddr *src_addr, + socklen_t *addrlen, + struct sockaddr_in6 *dst_addr); +static ssize_t pcp_socket_sendto_impl(PCP_SOCKET sock, const void *buf, + size_t len, int flags, + const struct sockaddr_in6 *src_addr, + struct sockaddr *dest_addr, + socklen_t addrlen); +static int pcp_socket_close_impl(PCP_SOCKET sock); + +pcp_socket_vt_t default_socket_vt = { + pcp_socket_create_impl, pcp_socket_recvfrom_impl, pcp_socket_sendto_impl, + pcp_socket_close_impl}; + +#ifdef WIN32 +// function calling WSAStartup (used in pcp-server and pcp_app) +int pcp_win_sock_startup() { + int err; + WORD wVersionRequested; + WSADATA wsaData; + OSVERSIONINFOEX osvi; + + /* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */ + wVersionRequested = MAKEWORD(2, 2); + err = WSAStartup(wVersionRequested, &wsaData); + if (err != 0) { + /* Tell the user that we could not find a usable */ + /* Winsock DLL. */ + perror("WSAStartup failed with error"); + return 1; + } + // find windows version + ZeroMemory(&osvi, sizeof(osvi)); + osvi.dwOSVersionInfoSize = sizeof(osvi); + + if (!GetVersionEx((LPOSVERSIONINFO)(&osvi))) { + printf("pcp_app: GetVersionEx failed"); + return 1; + } + + return 0; +} + +/* function calling WSACleanup + * returns 0 on success and 1 on failure + */ +int pcp_win_sock_cleanup() { + if (WSACleanup() == PCP_SOCKET_ERROR) { + printf("WSACleanup failed.\n"); + return 1; + } + return 0; +} +#endif + +void pcp_fill_in6_addr(struct in6_addr *dst_ip6, uint16_t *dst_port, + uint32_t *dst_scope_id, struct sockaddr *src) { + if (src->sa_family == AF_INET) { + struct sockaddr_in *src_ip4 = (struct sockaddr_in *)src; + + if (dst_ip6) { + S6_ADDR32(dst_ip6)[0] = 0; + S6_ADDR32(dst_ip6)[1] = 0; + S6_ADDR32(dst_ip6)[2] = htonl(0xFFFF); + S6_ADDR32(dst_ip6)[3] = src_ip4->sin_addr.s_addr; + } + if (dst_port) { + *dst_port = src_ip4->sin_port; + } + if (dst_scope_id) { + *dst_scope_id = 0; + } + } else if (src->sa_family == AF_INET6) { + struct sockaddr_in6 *src_ip6 = (struct sockaddr_in6 *)src; + + if (dst_ip6) { + memcpy(dst_ip6, src_ip6->sin6_addr.s6_addr, sizeof(*dst_ip6)); + } + if (dst_port) { + *dst_port = src_ip6->sin6_port; + } + if (dst_scope_id) { + *dst_scope_id = src_ip6->sin6_scope_id; + } + } +} + +void pcp_fill_sockaddr(struct sockaddr *dst, struct in6_addr *sip, + uint16_t sport, int ret_ipv6_mapped_ipv4, + uint32_t scope_id) { + if ((!ret_ipv6_mapped_ipv4) && (IN6_IS_ADDR_V4MAPPED(sip))) { + struct sockaddr_in *s = (struct sockaddr_in *)dst; + + s->sin_family = AF_INET; + s->sin_addr.s_addr = S6_ADDR32(sip)[3]; + s->sin_port = sport; + SET_SA_LEN(s, sizeof(struct sockaddr_in)); + } else { + struct sockaddr_in6 *s = (struct sockaddr_in6 *)dst; + + s->sin6_family = AF_INET6; + s->sin6_addr = *sip; + s->sin6_port = sport; + s->sin6_scope_id = scope_id; + SET_SA_LEN(s, sizeof(struct sockaddr_in6)); + } +} + +#ifndef PCP_SOCKET_IS_VOIDPTR +static pcp_errno pcp_get_error() { +#ifdef WIN32 + int errnum = WSAGetLastError(); + + switch (errnum) { + case WSAEADDRINUSE: + return PCP_ERR_ADDRINUSE; + case WSAEWOULDBLOCK: + return PCP_ERR_WOULDBLOCK; + default: + return PCP_ERR_UNKNOWN; + } +#else + switch (errno) { + case EADDRINUSE: + return PCP_ERR_ADDRINUSE; + // case EAGAIN: + case EWOULDBLOCK: + return PCP_ERR_WOULDBLOCK; + default: + return PCP_ERR_UNKNOWN; + } +#endif +} +#endif + +PCP_SOCKET pcp_socket_create(struct pcp_ctx_s *ctx, int domain, int type, + int protocol) { + assert(ctx && ctx->virt_socket_tb && ctx->virt_socket_tb->sock_create); + + return ctx->virt_socket_tb->sock_create(domain, type, protocol); +} + +ssize_t pcp_socket_recvfrom(struct pcp_ctx_s *ctx, void *buf, size_t len, + int flags, struct sockaddr *src_addr, + socklen_t *addrlen, struct sockaddr_in6 *dst_addr) { + assert(ctx && ctx->virt_socket_tb && ctx->virt_socket_tb->sock_recvfrom); + + return ctx->virt_socket_tb->sock_recvfrom(ctx->socket, buf, len, flags, + src_addr, addrlen, dst_addr); +} + +ssize_t pcp_socket_sendto(struct pcp_ctx_s *ctx, const void *buf, size_t len, + int flags, struct sockaddr_in6 *src_addr, + struct sockaddr *dest_addr, socklen_t addrlen) { + assert(ctx && ctx->virt_socket_tb && ctx->virt_socket_tb->sock_sendto); + + return ctx->virt_socket_tb->sock_sendto(ctx->socket, buf, len, flags, + src_addr, dest_addr, addrlen); +} + +int pcp_socket_close(struct pcp_ctx_s *ctx) { + assert(ctx && ctx->virt_socket_tb && ctx->virt_socket_tb->sock_close); + + return ctx->virt_socket_tb->sock_close(ctx->socket); +} + +static PCP_SOCKET pcp_socket_create_impl(int domain, int type, int protocol) { +#ifdef PCP_SOCKET_IS_VOIDPTR + return PCP_INVALID_SOCKET; +#else + PCP_SOCKET s; + uint32_t flg; + unsigned long iMode = 1; + struct sockaddr_storage sas; + struct sockaddr_in *sin = (struct sockaddr_in *)&sas; + struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&sas; + + OSDEP(iMode); + OSDEP(flg); + + memset(&sas, 0, sizeof(sas)); + sas.ss_family = domain; + if (domain == AF_INET) { + sin->sin_port = htons(5350); + SET_SA_LEN(sin, sizeof(struct sockaddr_in)); + } else if (domain == AF_INET6) { + sin6->sin6_port = htons(5350); + SET_SA_LEN(sin6, sizeof(struct sockaddr_in6)); + } else { + PCP_LOG(PCP_LOGLVL_ERR, "Unsupported socket domain:%d", domain); + } + + s = (PCP_SOCKET)socket(domain, type, protocol); + if (s == PCP_INVALID_SOCKET) + return PCP_INVALID_SOCKET; + +#ifdef WIN32 + if (ioctlsocket(s, FIONBIO, &iMode)) { + PCP_LOG(PCP_LOGLVL_ERR, "%s", + "Unable to set nonblocking mode for socket."); + CLOSE(s); + return PCP_INVALID_SOCKET; + } +#else // WIN32 + flg = fcntl(s, F_GETFL, 0); + if (fcntl(s, F_SETFL, flg | O_NONBLOCK)) { + PCP_LOG(PCP_LOGLVL_ERR, "%s", + "Unable to set nonblocking mode for socket."); + CLOSE(s); + return PCP_INVALID_SOCKET; + } +#endif //! WIN32 +#ifdef PCP_USE_IPV6_SOCKET + flg = 0; + if (PCP_SOCKET_ERROR == + setsockopt(s, IPPROTO_IPV6, IPV6_V6ONLY, (char *)&flg, sizeof(flg))) { + PCP_LOG(PCP_LOGLVL_ERR, "%s", + "Dual-stack sockets are not supported on this platform. " + "Recompile library with disabled IPv6 support."); + CLOSE(s); + return PCP_INVALID_SOCKET; + } +#endif // PCP_USE_IPV6_SOCKET +#if defined(IP_PKTINFO) || defined(IPV6_RECVPKTINFO) || defined(IPV6_PKTINFO) + { + int optval = 1; +#if defined WIN32 && defined IPV6_PKTINFO + if (setsockopt(s, IPPROTO_IPV6, IPV6_PKTINFO, (char *)&optval, + sizeof(optval)) < 0) { + PCP_LOG(PCP_LOGLVL_ERR, "%s", + "Unable to set IPV6_PKTINFO option for socket."); + CLOSE(s); + return PCP_INVALID_SOCKET; + } +#endif // WIN32 && IPV6_PKTINFO +#ifdef IP_PKTINFO + if (setsockopt(s, IPPROTO_IP, IP_PKTINFO, (char *)&optval, + sizeof(optval)) < 0) { + PCP_LOG(PCP_LOGLVL_ERR, "%s", + "Unable to set IP_PKTINFO option for socket."); + } +#endif // IP_PKTINFO +#ifdef IPV6_RECVPKTINFO + if (setsockopt(s, IPPROTO_IPV6, IPV6_RECVPKTINFO, (char *)&optval, + sizeof(optval)) < 0) { + PCP_LOG(PCP_LOGLVL_ERR, "%s", + "Unable to set IPV6_RECVPKTINFO option for socket."); + CLOSE(s); + return PCP_INVALID_SOCKET; + } +#endif // IPV6_RECVPKTINFO + } +#endif // IP_PKTINFO && IPV6_RECVPKTINFO + while (bind(s, (struct sockaddr *)&sas, SA_LEN((struct sockaddr *)&sas)) == + PCP_SOCKET_ERROR) { + if (pcp_get_error() == PCP_ERR_ADDRINUSE) { + if (sas.ss_family == AF_INET) { + sin->sin_port = htons(ntohs(sin->sin_port) + 1); + } else { + sin6->sin6_port = htons(ntohs(sin6->sin6_port) + 1); + } + } else { + PCP_LOG(PCP_LOGLVL_ERR, "%s", "bind error"); + CLOSE(s); + return PCP_INVALID_SOCKET; + } + } + PCP_LOG(PCP_LOGLVL_DEBUG, "%s: return %d", __FUNCTION__, s); + return s; +#endif +} + +#ifdef WIN32 +#define PCP_CMSG_DATA(msg) (WSA_CMSG_DATA(msg)) +#else // WIN32 +#define PCP_CMSG_DATA(msg) (CMSG_DATA(msg)) +#endif // WIN32 + +static ssize_t pcp_socket_recvfrom_impl(PCP_SOCKET sock, void *buf, size_t len, + int flags, struct sockaddr *src_addr, + socklen_t *addrlen, + struct sockaddr_in6 *dst_addr) { + ssize_t ret = -1; + +#ifndef PCP_SOCKET_IS_VOIDPTR +#if defined(IPV6_PKTINFO) && defined(IP_PKTINFO) + char control_buf[1024] = {0}; + +#ifndef WIN32 + struct msghdr msg; + struct iovec iov; + struct cmsghdr *cmsg; + + iov.iov_base = buf; + iov.iov_len = len; + + memset(&msg, 0, sizeof(msg)); + msg.msg_name = src_addr; + msg.msg_namelen = addrlen ? *addrlen : 0; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = control_buf; + msg.msg_controllen = sizeof(control_buf); + + // Receive message with IP_PKTINFO + ret = recvmsg(sock, &msg, 0); + +#else // WIN32 + WSAMSG msg; + WSABUF iov; + WSACMSGHDR *cmsg; + iov.buf = buf; + iov.len = len; + + memset(&msg, 0, sizeof(msg)); + msg.name = (struct sockaddr *)src_addr; + msg.namelen = addrlen ? *addrlen : 0; + msg.lpBuffers = &iov; + msg.dwBufferCount = 1; + msg.Control.buf = control_buf; + msg.Control.len = sizeof(control_buf); + msg.dwFlags = 0; + // Get WSARecvMsg function pointer + LPFN_WSARECVMSG WSARecvMsg; + GUID guid = WSAID_WSARECVMSG; + DWORD bytesReturned; + if (WSAIoctl(sock, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), + &WSARecvMsg, sizeof(WSARecvMsg), &bytesReturned, NULL, + NULL) == SOCKET_ERROR) { + perror("WSAIoctl failed"); + return 1; + } + + DWORD bytesReceived; + if (WSARecvMsg(sock, &msg, &bytesReceived, NULL, NULL) == SOCKET_ERROR) { + ret = -1; + } else { +#ifdef _WIN32 + if (bytesReceived > INT32_MAX) { + ret = -1; // Handle overflow error + } else { + ret = (ssize_t)bytesReceived; + } +#else + ret = bytesReceived; +#endif + } +#endif // WIN32 + + // Processing control message + if (ret > 0) { + for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; + cmsg = CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level == IPPROTO_IP && + cmsg->cmsg_type == IP_PKTINFO) { + struct in_pktinfo *pktinfo = + (struct in_pktinfo *)PCP_CMSG_DATA(cmsg); + S6_ADDR32(&dst_addr->sin6_addr)[0] = 0; + S6_ADDR32(&dst_addr->sin6_addr)[1] = 0; + S6_ADDR32(&dst_addr->sin6_addr)[2] = htonl(0xFFFF); + S6_ADDR32(&dst_addr->sin6_addr)[3] = pktinfo->ipi_addr.s_addr; + dst_addr->sin6_scope_id = 0; + dst_addr->sin6_family = AF_INET6; + } + if (cmsg->cmsg_level == IPPROTO_IPV6 && + cmsg->cmsg_type == IPV6_PKTINFO) { + struct in6_pktinfo *pktinfo6 = + (struct in6_pktinfo *)PCP_CMSG_DATA(cmsg); + IPV6_ADDR_COPY(&dst_addr->sin6_addr, &pktinfo6->ipi6_addr); + dst_addr->sin6_family = AF_INET6; + if (IN6_IS_ADDR_LINKLOCAL(&pktinfo6->ipi6_addr)) { + dst_addr->sin6_scope_id = pktinfo6->ipi6_ifindex; + } else { + dst_addr->sin6_scope_id = 0; + } + } + } + } +#else // IPV6_PKTINFO && IP_PKTINFO + ret = recvfrom(sock, buf, len, flags, src_addr, addrlen); +#endif // IPV6_PKTINFO && IP_PKTINFO + if (ret == PCP_SOCKET_ERROR) { + if (pcp_get_error() == PCP_ERR_WOULDBLOCK) { + ret = PCP_ERR_WOULDBLOCK; + } else { + ret = PCP_ERR_RECV_FAILED; + } + } +#endif // PCP_SOCKET_IS_VOIDPTR + + return ret; +} + +static ssize_t pcp_socket_sendto_impl(PCP_SOCKET sock, const void *buf, + size_t len, int flags UNUSED, + const struct sockaddr_in6 *src_addr, + struct sockaddr *dest_addr, + socklen_t addrlen) { + ssize_t ret = -1; + +#ifndef PCP_SOCKET_IS_VOIDPTR + +#if defined(IPV6_PKTINFO) + if (src_addr) { + struct in6_pktinfo ipi6 = {0}; + +#ifndef WIN32 + uint8_t c[CMSG_SPACE(sizeof(struct in6_pktinfo))] = {0}; + struct iovec iov; + struct msghdr msg; + struct cmsghdr *cmsg; + + iov.iov_base = (void *)buf; + iov.iov_len = len; + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + ipi6.ipi6_addr = src_addr->sin6_addr; + ipi6.ipi6_ifindex = src_addr->sin6_scope_id; + msg.msg_control = c; + msg.msg_controllen = sizeof(c); + cmsg = CMSG_FIRSTHDR(&msg); + cmsg->cmsg_level = IPPROTO_IPV6; + cmsg->cmsg_type = IPV6_PKTINFO; + cmsg->cmsg_len = CMSG_LEN(sizeof(ipi6)); + memcpy(CMSG_DATA(cmsg), &ipi6, sizeof(ipi6)); + msg.msg_name = (void *)dest_addr; + msg.msg_namelen = addrlen; + ret = sendmsg(sock, &msg, flags); +#else // WIN32 + WSABUF wsaBuf; + wsaBuf.buf = buf; + wsaBuf.len = len; + uint8_t c[WSA_CMSG_SPACE(sizeof(struct in6_pktinfo))] = {0}; + + WSAMSG wsaMsg; + memset(&wsaMsg, 0, sizeof(wsaMsg)); + wsaMsg.name = (struct sockaddr *)dest_addr; + wsaMsg.namelen = addrlen; + wsaMsg.lpBuffers = &wsaBuf; + wsaMsg.dwBufferCount = 1; + wsaMsg.Control.buf = c; + + // Set the source address inside the control message + if (IN6_IS_ADDR_V4MAPPED(&src_addr->sin6_addr)) { + wsaMsg.Control.len = WSA_CMSG_SPACE(sizeof(struct in_pktinfo)); + struct cmsghdr *cmsg = WSA_CMSG_FIRSTHDR(&wsaMsg); + cmsg->cmsg_level = IPPROTO_IP; + cmsg->cmsg_type = IP_PKTINFO; + cmsg->cmsg_len = WSA_CMSG_LEN(sizeof(struct in_pktinfo)); + struct in_pktinfo *pktinfo = + (struct in_pktinfo *)WSA_CMSG_DATA(cmsg); + pktinfo->ipi_addr.s_addr = S6_ADDR32(&src_addr->sin6_addr)[3]; + } else { + wsaMsg.Control.len = WSA_CMSG_SPACE(sizeof(struct in6_pktinfo)); + struct cmsghdr *cmsg = WSA_CMSG_FIRSTHDR(&wsaMsg); + cmsg->cmsg_level = IPPROTO_IPV6; + cmsg->cmsg_type = IPV6_PKTINFO; + cmsg->cmsg_len = WSA_CMSG_LEN(sizeof(struct in6_pktinfo)); + struct in6_pktinfo *pktinfo = + (struct in6_pktinfo *)WSA_CMSG_DATA(cmsg); + IPV6_ADDR_COPY(&pktinfo->ipi6_addr, &src_addr->sin6_addr); + pktinfo->ipi6_ifindex = src_addr->sin6_scope_id; + } + + LPFN_WSARECVMSG WSARecvMsg; + GUID WSARecvMsg_GUID = WSAID_WSARECVMSG; + DWORD dwBytesReturned; + + if (WSAIoctl(sock, SIO_GET_EXTENSION_FUNCTION_POINTER, &WSARecvMsg_GUID, + sizeof(GUID), &WSARecvMsg, sizeof(WSARecvMsg), + &dwBytesReturned, NULL, NULL) == SOCKET_ERROR) { + PCP_LOG(PCP_LOGLVL_PERR, ("WSAIoctl failed")); + return 1; + } + + // Send the packet + DWORD bytesSent = 0; + if (WSASendMsg(sock, &wsaMsg, 0, &bytesSent, NULL, NULL) == + SOCKET_ERROR) { + PCP_LOG(PCP_LOGLVL_PERR, "WSASendMsg failed: %d", + WSAGetLastError()); + } else { + ret = bytesSent; + } +#endif // WIN32 + } else +#else // IPV6_PKTINFO + ret = sendto(sock, buf, len, 0, dest_addr, addrlen); +#endif /* IPV6_PKTINFO */ + + if ((ret == PCP_SOCKET_ERROR) || (ret != (ssize_t)len)) { + if (pcp_get_error() == PCP_ERR_WOULDBLOCK) { + ret = PCP_ERR_WOULDBLOCK; + } else { + ret = PCP_ERR_SEND_FAILED; + } + } +#endif + return ret; +} + +static int pcp_socket_close_impl(PCP_SOCKET sock) { +#ifndef PCP_SOCKET_IS_VOIDPTR + return CLOSE(sock); +#else + return PCP_SOCKET_ERROR; +#endif +} diff --git a/lib/libpcp/src/net/pcp_socket.h b/lib/libpcpnatpmp/src/net/pcp_socket.h similarity index 76% rename from lib/libpcp/src/net/pcp_socket.h rename to lib/libpcpnatpmp/src/net/pcp_socket.h index 6c76038e02e..401f66a0b4c 100644 --- a/lib/libpcp/src/net/pcp_socket.h +++ b/lib/libpcpnatpmp/src/net/pcp_socket.h @@ -26,7 +26,7 @@ #ifndef PCP_SOCKET_H #define PCP_SOCKET_H -#include "pcp.h" +#include "pcpnatpmp.h" #ifdef PCP_SOCKET_IS_VOIDPTR #define PD_SOCKET_STARTUP() @@ -69,19 +69,22 @@ struct pcp_ctx_s; extern pcp_socket_vt_t default_socket_vt; void pcp_fill_in6_addr(struct in6_addr *dst_ip6, uint16_t *dst_port, - struct sockaddr *src); + uint32_t *dst_scope_id, struct sockaddr *src); void pcp_fill_sockaddr(struct sockaddr *dst, struct in6_addr *sip, - uint16_t sport, int ret_ipv6_mapped_ipv4, uint32_t scope_id); + uint16_t sport, int ret_ipv6_mapped_ipv4, + uint32_t scope_id); PCP_SOCKET pcp_socket_create(struct pcp_ctx_s *ctx, int domain, int type, - int protocol); + int protocol); ssize_t pcp_socket_recvfrom(struct pcp_ctx_s *ctx, void *buf, size_t len, - int flags, struct sockaddr *src_addr, socklen_t *addrlen); + int flags, struct sockaddr *src_addr, + socklen_t *addrlen, struct sockaddr_in6 *dst_addr); ssize_t pcp_socket_sendto(struct pcp_ctx_s *ctx, const void *buf, size_t len, - int flags, struct sockaddr *dest_addr, socklen_t addrlen); + int flags, struct sockaddr_in6 *src_addr, + struct sockaddr *dest_addr, socklen_t addrlen); int pcp_socket_close(struct pcp_ctx_s *ctx); @@ -92,29 +95,28 @@ int pcp_socket_close(struct pcp_ctx_s *ctx); #ifndef SA_LEN #ifdef HAVE_SOCKADDR_SA_LEN -#define SA_LEN(addr) ((addr)->sa_len) +#define SA_LEN(addr) ((addr)->sa_len) #else /* HAVE_SOCKADDR_SA_LEN */ -static inline size_t get_sa_len(struct sockaddr *addr) -{ +static inline size_t get_sa_len(struct sockaddr *addr) { switch (addr->sa_family) { - case AF_INET: - return (sizeof(struct sockaddr_in)); + case AF_INET: + return (sizeof(struct sockaddr_in)); - case AF_INET6: - return (sizeof(struct sockaddr_in6)); + case AF_INET6: + return (sizeof(struct sockaddr_in6)); - default: - return (sizeof(struct sockaddr)); + default: + return (sizeof(struct sockaddr)); } } -#define SA_LEN(addr) (get_sa_len(addr)) +#define SA_LEN(addr) (get_sa_len(addr)) #endif /* HAVE_SOCKADDR_SA_LEN */ #endif /* SA_LEN */ #ifdef HAVE_SOCKADDR_SA_LEN -#define SET_SA_LEN(s, l) ((struct sockaddr*)s)->sa_len=l +#define SET_SA_LEN(s, l) ((struct sockaddr *)s)->sa_len = l #else #define SET_SA_LEN(s, l) #endif diff --git a/lib/libpcpnatpmp/src/net/sock_ntop.c b/lib/libpcpnatpmp/src/net/sock_ntop.c new file mode 100644 index 00000000000..d9cff57090a --- /dev/null +++ b/lib/libpcpnatpmp/src/net/sock_ntop.c @@ -0,0 +1,346 @@ +/* + Copyright (c) 2014 by Cisco Systems, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#else +#include "default_config.h" +#endif + +#ifdef _MSC_VER +#define _CRT_SECURE_NO_WARNINGS 1 +#endif + +#include +#include +#include +#include /* basic system data types */ +#ifdef WIN32 +#include "pcp_win_defines.h" +#else +#include /* inet(3) functions */ +#include +#include /* sockaddr_in{} and other Internet defns */ +#include /* basic socket definitions */ +#endif +#include "pcp_utils.h" +#include "unp.h" +#include +#include + +#ifdef HAVE_SOCKADDR_DL_STRUCT +#include +#endif + +/* include sock_ntop */ +char *sock_ntop(const struct sockaddr *sa, socklen_t salen) { + char portstr[8]; + static char str[128]; /* Unix domain is largest */ + + switch (sa->sa_family) { + case AF_INET: { + const struct sockaddr_in *sin = (const struct sockaddr_in *)sa; + + if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL) + return (NULL); + if (ntohs(sin->sin_port) != 0) { + snprintf(portstr, sizeof(portstr) - 1, ":%d", ntohs(sin->sin_port)); + portstr[sizeof(portstr) - 1] = '\0'; + strcat(str, portstr); + } + return (str); + } + /* end sock_ntop */ + +#ifdef AF_INET6 + case AF_INET6: { + const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)sa; + + str[0] = '['; + if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == + NULL) + return (NULL); + if (ntohs(sin6->sin6_port) != 0) { + snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port)); + portstr[sizeof(portstr) - 1] = '\0'; + strcat(str, portstr); + return (str); + } + return (str + 1); + } +#endif + + default: + snprintf(str, sizeof(str) - 1, "sock_ntop: unknown AF_xxx: %d, len %d", + sa->sa_family, salen); + str[sizeof(str) - 1] = '\0'; + return (str); + } + return (NULL); +} + +char *Sock_ntop(const struct sockaddr *sa, socklen_t salen) { + char *ptr; + + if ((ptr = sock_ntop(sa, salen)) == NULL) + perror("sock_ntop"); /* inet_ntop() sets errno */ // LCOV_EXCL_LINE + return (ptr); +} + +int sock_pton(const char *cp, struct sockaddr *sa) { + const char *ip_end; + char *host_name = NULL; + const char *port = NULL; + if ((!cp) || (!sa)) { + return -1; + } + + // skip ws + while ((cp) && (isspace(*cp))) { + ++cp; + } + + ip_end = cp; + if (*cp == '[') { // find matching bracket ']' + ++cp; + while ((*ip_end) && (*ip_end != ']')) { + ++ip_end; + } + + if (!*ip_end) { + return -2; + } + host_name = strndup(cp, ip_end - cp); + ++ip_end; + } + { // find start of port part + while (*ip_end) { + if (*ip_end == ':') { + if (!port) { + port = ip_end + 1; + } else if (host_name == NULL) { // means addr has [] block + port = NULL; // more than 1 ":" => assume the whole addr is + // IPv6 address w/o port + host_name = strdup(cp); + break; + } + } + ++ip_end; + } + if (!host_name) { + if ((*ip_end == 0) && (port != NULL)) { + if (port - cp > 1) { // only port entered + host_name = strndup(cp, port - cp - 1); + } + } else { + host_name = strndup(cp, ip_end - cp); + } + } + } + + // getaddrinfo for host + { + struct addrinfo hints, *servinfo, *p; + int rv; + + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_V4MAPPED; + + if ((rv = getaddrinfo(host_name, port, &hints, &servinfo)) != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); + if (host_name) + free(host_name); + return -2; + } + + for (p = servinfo; p != NULL; p = p->ai_next) { + if ((p->ai_family == AF_INET) || (p->ai_family == AF_INET6)) { + memcpy(sa, p->ai_addr, SA_LEN(p->ai_addr)); + if (host_name == NULL) { // getaddrinfo returns localhost ip if + // hostname is null + switch (p->ai_family) { + case AF_INET: + ((struct sockaddr_in *)sa)->sin_addr.s_addr = + INADDR_ANY; + break; + case AF_INET6: + memset(&((struct sockaddr_in6 *)sa)->sin6_addr, 0, + sizeof(struct sockaddr_in6)); + break; + default: // Should never happen LCOV_EXCL_START + if (host_name) + free(host_name); + return -2; + } // LCOV_EXCL_STOP + } + break; + } + } + freeaddrinfo(servinfo); + } + + if (host_name) + free(host_name); + return 0; +} + +struct sockaddr *Sock_pton(const char *cp) { + static struct sockaddr_storage sa_s; + if (sock_pton(cp, (struct sockaddr *)&sa_s) == 0) { + return (struct sockaddr *)&sa_s; + } else { + return NULL; + } +} + +int sock_pton_with_prefix(const char *cp, struct sockaddr *sa, + int *int_prefix) { + const char *prefix_begin = NULL; + char *prefix = NULL; + + const char *ip_end; + char *host_name = NULL; + const char *port = NULL; + + if ((!cp) || (!sa) || (!int_prefix)) { + return -1; + } + + // skip ws + while ((cp) && (isspace(*cp))) { + ++cp; + } + + ip_end = cp; + if (*cp == '[') { // find matching bracket ']' + ++cp; + while ((*ip_end) && (*ip_end != ']')) { + if (*ip_end == '/') { + prefix_begin = ip_end + 1; + } + ++ip_end; + } + + if (!*ip_end) { + return -2; + } + + if (prefix_begin) { + host_name = strndup(cp, prefix_begin - cp - 1); + prefix = strndup(prefix_begin, ip_end - prefix_begin); + if (prefix) { + *int_prefix = atoi(prefix); + free(prefix); + } + } else { + host_name = strndup(cp, ip_end - cp); + *int_prefix = 128; + } + ++ip_end; + } else { + return -2; + } + + { // find start of port part + while (*ip_end) { + if (*ip_end == ':') { + if (!port) { + port = ip_end + 1; + } else if (host_name == NULL) { // means addr has [] block + port = NULL; // more than 1 ":" => assume the whole addr is + // IPv6 address w/o port + host_name = strdup(cp); + break; + } + } + ++ip_end; + } + if (!host_name) { + if ((*ip_end == 0) && (port != NULL)) { + if (port - cp > 1) { // only port entered + host_name = strndup(cp, port - cp - 1); + } + } else { + host_name = strndup(cp, ip_end - cp); + } + } + } + + // getaddrinfo for host + { + struct addrinfo hints, *servinfo, *p; + int rv; + + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_flags = AI_V4MAPPED; + + if ((rv = getaddrinfo(host_name, port, &hints, &servinfo)) != 0) { + fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv)); + if (host_name) + free(host_name); + return -2; + } + + for (p = servinfo; p != NULL; p = p->ai_next) { + if ((p->ai_family == AF_INET) || (p->ai_family == AF_INET6)) { + memcpy(sa, p->ai_addr, SA_LEN(p->ai_addr)); + if (host_name == NULL) { // getaddrinfo returns localhost ip if + // hostname is null + switch (p->ai_family) { + case AF_INET6: + memset(&((struct sockaddr_in6 *)sa)->sin6_addr, 0, + sizeof(struct sockaddr_in6)); + break; + default: // Should never happen LCOV_EXCL_START + if (host_name) + free(host_name); + return -2; + } // LCOV_EXCL_STOP + } + break; + } + } + freeaddrinfo(servinfo); + } + + if (host_name) + free(host_name); + + if ((sa->sa_family == AF_INET) && (*int_prefix > 32)) { + + return -2; + } + + if ((sa->sa_family == AF_INET6) && (*int_prefix > 128)) { + + return -2; + } + + return 0; +} diff --git a/lib/libpcp/src/net/unp.h b/lib/libpcpnatpmp/src/net/unp.h similarity index 81% rename from lib/libpcp/src/net/unp.h rename to lib/libpcpnatpmp/src/net/unp.h index 4f767efc573..55b3298eb76 100644 --- a/lib/libpcp/src/net/unp.h +++ b/lib/libpcpnatpmp/src/net/unp.h @@ -30,11 +30,11 @@ #include #include #else -#include -#include -#include -#include #include +#include +#include +#include +#include #endif #include "pcp_socket.h" @@ -45,17 +45,16 @@ char *sock_ntop(const SA *, socklen_t); char *sock_ntop_host(const SA *, socklen_t); char *Sock_ntop(const SA *, socklen_t); char *Sock_ntop_host(const SA *, socklen_t); -int Sockfd_to_family(int); +int Sockfd_to_family(int); -int sock_pton(const char* cp, struct sockaddr *sa); -int -sock_pton_with_prefix(const char* cp, struct sockaddr *sa, int *int_prefix); -struct sockaddr *Sock_pton(const char* cp); +int sock_pton(const char *cp, struct sockaddr *sa); +int sock_pton_with_prefix(const char *cp, struct sockaddr *sa, int *int_prefix); +struct sockaddr *Sock_pton(const char *cp); -void err_dump(const char *, ...); -void err_msg(const char *, ...); -void err_quit(const char *, ...); -void err_ret(const char *, ...); -void err_sys(const char *, ...); +void err_dump(const char *, ...); +void err_msg(const char *, ...); +void err_quit(const char *, ...); +void err_ret(const char *, ...); +void err_sys(const char *, ...); -#endif /* __unp_h */ +#endif /* __unp_h */ diff --git a/lib/libpcpnatpmp/src/pcp_api.c b/lib/libpcpnatpmp/src/pcp_api.c new file mode 100644 index 00000000000..dd140452797 --- /dev/null +++ b/lib/libpcpnatpmp/src/pcp_api.c @@ -0,0 +1,769 @@ +/* + Copyright (c) 2014 by Cisco Systems, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#else +#include "default_config.h" +#endif + +#ifdef _MSC_VER +#define _CRT_SECURE_NO_WARNINGS 1 +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include "pcp_gettimeofday.h" +#include "pcp_win_defines.h" +#include +#else +#include +#include +#include +#include +#include +#include +#include +#endif +#include "net/findsaddr.h" +#include "pcpnatpmp.h" + +#include "pcp_client_db.h" +#include "pcp_event_handler.h" +#include "pcp_logger.h" +#include "pcp_server_discovery.h" +#include "pcp_socket.h" +#include "pcp_utils.h" + +PCP_SOCKET pcp_get_socket(pcp_ctx_t *ctx) { + + return ctx ? ctx->socket : PCP_INVALID_SOCKET; +} + +int pcp_add_server(pcp_ctx_t *ctx, struct sockaddr *pcp_server, + uint8_t pcp_version) { + int res; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + + if (!ctx) { + return PCP_ERR_BAD_ARGS; + } + if (pcp_version > PCP_MAX_SUPPORTED_VERSION) { + PCP_LOG_END(PCP_LOGLVL_INFO); + return PCP_ERR_UNSUP_VERSION; + } + + res = psd_add_pcp_server(ctx, pcp_server, pcp_version); + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return res; +} + +pcp_ctx_t *pcp_init(uint8_t autodiscovery, pcp_socket_vt_t *socket_vt) { + pcp_ctx_t *ctx = (pcp_ctx_t *)calloc(1, sizeof(pcp_ctx_t)); + + pcp_logger_init(); + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + + if (!ctx) { + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return NULL; + } + + if (socket_vt) { + ctx->virt_socket_tb = socket_vt; + } else { + ctx->virt_socket_tb = &default_socket_vt; + } + + ctx->socket = pcp_socket_create(ctx, +#ifdef PCP_USE_IPV6_SOCKET + AF_INET6, +#else + AF_INET, +#endif + SOCK_DGRAM, 0); + + if (ctx->socket == PCP_INVALID_SOCKET) { + PCP_LOG(PCP_LOGLVL_WARN, "%s", + "Error occurred while creating a PCP socket."); + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return NULL; + } + PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Created a new PCP socket."); + + if (autodiscovery) + psd_add_gws(ctx); + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return ctx; +} + +int pcp_eval_flow_state(pcp_flow_t *flow, pcp_fstate_e *fstate) { + pcp_flow_t *fiter; + int nexit_states = 0; + int fpresent_no_exit_state = 0; + int fsuccess = 0; + int ffailed = 0; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + + for (fiter = flow; fiter != NULL; fiter = fiter->next_child) { + switch (fiter->state) { + case pfs_wait_for_lifetime_renew: + fsuccess = 1; + ++nexit_states; + break; + case pfs_failed: + ffailed = 1; + ++nexit_states; + break; + case pfs_wait_after_short_life_error: + ++nexit_states; + break; + default: + fpresent_no_exit_state = 1; + break; + } + } + + if (fstate) { + if (fpresent_no_exit_state) { + if (fsuccess) { + *fstate = pcp_state_partial_result; + } else { + *fstate = pcp_state_processing; + } + } else { + if (fsuccess) { + *fstate = pcp_state_succeeded; + } else if (ffailed) { + *fstate = pcp_state_failed; + } else { + *fstate = pcp_state_short_lifetime_error; + } + } + } + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return nexit_states; +} + +pcp_fstate_e pcp_wait(pcp_flow_t *flow, int timeout, int exit_on_partial_res) { +#ifdef PCP_SOCKET_IS_VOIDPTR + return pcp_state_failed; +#else + fd_set read_fds; + int fdmax; + PCP_SOCKET fd; + struct timeval tout_end; + struct timeval tout_select; + pcp_fstate_e fstate; + int nflow_exit_states = pcp_eval_flow_state(flow, &fstate); + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + + if (!flow) { + PCP_LOG(PCP_LOGLVL_PERR, "Flow argument of %s function set to NULL!", + __FUNCTION__); + return pcp_state_failed; + } + + switch (fstate) { + case pcp_state_partial_result: + case pcp_state_processing: + break; + default: + nflow_exit_states = 0; + break; + } + + gettimeofday(&tout_end, NULL); + tout_end.tv_usec += (timeout * 1000) % 1000000; + tout_end.tv_sec += tout_end.tv_usec / 1000000; + tout_end.tv_usec = tout_end.tv_usec % 1000000; + tout_end.tv_sec += timeout / 1000; + + PCP_LOG(PCP_LOGLVL_INFO, + "Initialized wait for result of flow: %d, wait timeout %d ms", + flow->key_bucket, timeout); + + FD_ZERO(&read_fds); + + fd = pcp_get_socket(flow->ctx); + fdmax = fd + 1; + + // main loop + for (;;) { + int ret_count; + pcp_fstate_e ret_state; + struct timeval ctv; + + OSDEP(ret_count); + // check expiration of wait timeout + gettimeofday(&ctv, NULL); + if ((timeval_subtract(&tout_select, &tout_end, &ctv)) || + ((tout_select.tv_sec == 0) && (tout_select.tv_usec == 0)) || + (tout_select.tv_sec < 0)) { + return pcp_state_processing; + } + + // process all events and get timeout value for next select + pcp_pulse(flow->ctx, &tout_select); + + // check flow for reaching one of exit from wait states + // (also handles case when flow is MAP for 0.0.0.0) + if (pcp_eval_flow_state(flow, &ret_state) > nflow_exit_states) { + if ((exit_on_partial_res) || + (ret_state != pcp_state_partial_result)) { + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return ret_state; + } + } + + FD_ZERO(&read_fds); + FD_SET(fd, &read_fds); + + PCP_LOG(PCP_LOGLVL_DEBUG, + "Executing select with fdmax=%d, timeout = %lld s; %ld us", + fdmax, (long long)tout_select.tv_sec, + (long int)tout_select.tv_usec); + + ret_count = select(fdmax, &read_fds, NULL, NULL, &tout_select); + + // check of select result // only for debug purposes +#ifdef DEBUG + if (ret_count == -1) { + char error[ERR_BUF_LEN]; + pcp_strerror(errno, error, sizeof(error)); + PCP_LOG(PCP_LOGLVL_PERR, "select failed: %s", error); + } else if (ret_count == 0) { + PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "select timed out"); + } else { + PCP_LOG(PCP_LOGLVL_DEBUG, "select returned %d i/o events.", + ret_count); + } +#endif + } + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return pcp_state_succeeded; +#endif // PCP_SOCKET_IS_VOIDPTR +} + +static inline void init_flow(pcp_flow_t *f, pcp_server_t *s, int lifetime, + struct sockaddr *ext_addr) { + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + if (f && s) { + struct timeval curtime; + f->ctx = s->ctx; + + switch (f->kd.operation) { + case PCP_OPCODE_MAP: + case PCP_OPCODE_PEER: + pcp_fill_in6_addr(&f->map_peer.ext_ip, &f->map_peer.ext_port, NULL, + ext_addr); + break; + default: + assert(!ext_addr); + break; + } + + gettimeofday(&curtime, NULL); + f->lifetime = lifetime; + f->timeout = curtime; + + if (s->server_state == pss_wait_io) { + f->state = pfs_send; + } else { + f->state = pfs_wait_for_server_init; + } + + s->next_timeout = curtime; + f->user_data = NULL; + + pcp_db_add_flow(f); + PCP_LOG_FLOW(f, "Added new flow"); + } + PCP_LOG_END(PCP_LOGLVL_DEBUG); +} + +struct caasi_data { + struct flow_key_data *kd; + pcp_flow_t *fprev; + pcp_flow_t *ffirst; + uint32_t lifetime; + struct sockaddr *ext_addr; + uint8_t toler_fields; + char *app_name; + void *userdata; +}; + +static int have_same_af(struct in6_addr *addr1, struct in6_addr *addr2) { + return ((IN6_IS_ADDR_V4MAPPED(addr1) && IN6_IS_ADDR_V4MAPPED(addr2)) || + (!IN6_IS_ADDR_V4MAPPED(addr1) && !IN6_IS_ADDR_V4MAPPED(addr2))); +} + +static int chain_and_assign_src_ip(pcp_server_t *s, void *data) { + struct caasi_data *d = (struct caasi_data *)data; + struct flow_key_data kd = *d->kd; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + + if (s->server_state == pss_not_working) { + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return 0; + } + + pcp_flow_t *f = NULL; + + if (IPV6_IS_ADDR_ANY(&kd.src_ip)) { + memcpy(&kd.src_ip, s->src_ip, sizeof(kd.src_ip)); + kd.scope_id = s->pcp_scope_id; + } + + // check address family + if (!have_same_af(&kd.src_ip, (struct in6_addr *)s->src_ip)) { + return 0; + } + + // check matching scope + if (kd.scope_id != s->pcp_scope_id) { + return 0; + } + + memcpy(&kd.pcp_server_ip, s->pcp_ip, sizeof(kd.pcp_server_ip)); + memcpy(&kd.nonce, &s->nonce, sizeof(kd.nonce)); + f = pcp_create_flow(s, &kd); + if (!f) { + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return 1; + } +#ifdef PCP_SADSCP + if (kd.operation == PCP_OPCODE_SADSCP) { + f->sadscp.toler_fields = d->toler_fields; + if (d->app_name) { + f->sadscp.app_name_length = strlen(d->app_name); + f->sadscp_app_name = strdup(d->app_name); + } else { + f->sadscp.app_name_length = 0; + f->sadscp_app_name = NULL; + } + } +#endif + init_flow(f, s, d->lifetime, d->ext_addr); + f->user_data = d->userdata; + if (d->fprev) { + d->fprev->next_child = f; + } else { + d->ffirst = f; + } + d->fprev = f; + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return 0; +} + +pcp_flow_t *pcp_new_flow(pcp_ctx_t *ctx, struct sockaddr *src_addr, + struct sockaddr *dst_addr, struct sockaddr *ext_addr, + uint8_t protocol, uint32_t lifetime, void *userdata) { + struct flow_key_data kd; + struct caasi_data data; + struct sockaddr_storage tmp_ext_addr; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + + memset(&kd, 0, sizeof(kd)); + + if ((!src_addr) || (!ctx)) { + return NULL; + } + pcp_fill_in6_addr(&kd.src_ip, &kd.map_peer.src_port, &kd.scope_id, + src_addr); + + kd.map_peer.protocol = protocol; + + if (dst_addr) { + switch (dst_addr->sa_family) { + case AF_INET: + if (((struct sockaddr_in *)(dst_addr))->sin_addr.s_addr == + INADDR_ANY) { + dst_addr = NULL; + } + break; + case AF_INET6: + if (IPV6_IS_ADDR_ANY( + &((struct sockaddr_in6 *)(dst_addr))->sin6_addr)) { + dst_addr = NULL; + } + break; + default: + dst_addr = NULL; + break; + } + } + + if (dst_addr) { + pcp_fill_in6_addr(&kd.map_peer.dst_ip, &kd.map_peer.dst_port, NULL, + dst_addr); + kd.operation = PCP_OPCODE_PEER; + if (src_addr->sa_family == AF_INET) { + if (S6_ADDR32(&kd.src_ip)[3] == INADDR_ANY) { + findsaddr((struct sockaddr_in *)dst_addr, &kd.src_ip); + } + } else if (IPV6_IS_ADDR_ANY(&kd.src_ip)) { + findsaddr6((struct sockaddr_in6 *)dst_addr, &kd.src_ip, + &kd.scope_id); + } else if (dst_addr->sa_family != src_addr->sa_family) { + PCP_LOG(PCP_LOGLVL_PERR, "%s", "Socket family mismatch."); + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return NULL; + } + } else { + kd.operation = PCP_OPCODE_MAP; + } + + if (!ext_addr) { + struct sockaddr_in *te4 = (struct sockaddr_in *)&tmp_ext_addr; + struct sockaddr_in6 *te6 = (struct sockaddr_in6 *)&tmp_ext_addr; + tmp_ext_addr.ss_family = src_addr->sa_family; + switch (tmp_ext_addr.ss_family) { + case AF_INET: + memset(&te4->sin_addr, 0, sizeof(te4->sin_addr)); + te4->sin_port = 0; + break; + case AF_INET6: + memset(&te6->sin6_addr, 0, sizeof(te6->sin6_addr)); + te6->sin6_port = 0; + break; + default: + PCP_LOG(PCP_LOGLVL_PERR, "%s", "Unsupported address family."); + return NULL; + } + ext_addr = (struct sockaddr *)&tmp_ext_addr; + } + + data.fprev = NULL; + data.lifetime = lifetime; + data.ext_addr = ext_addr; + data.kd = &kd; + data.ffirst = NULL; + data.userdata = userdata; + + if (pcp_db_foreach_server(ctx, chain_and_assign_src_ip, &data) != + PCP_ERR_MAX_SIZE) { // didn't iterate through each server => error + // happened + pcp_delete_flow(data.ffirst); + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return NULL; + } + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return data.ffirst; +} + +void pcp_flow_set_lifetime(pcp_flow_t *f, uint32_t lifetime) { + pcp_flow_t *fiter; + + for (fiter = f; fiter != NULL; fiter = fiter->next_child) { + fiter->lifetime = lifetime; + + pcp_flow_updated(fiter); + } +} + +void pcp_flow_set_3rd_party_opt(pcp_flow_t *f, struct sockaddr *thirdp_addr) { + pcp_flow_t *fiter; + + for (fiter = f; fiter != NULL; fiter = fiter->next_child) { + fiter->third_party_option_present = 1; + pcp_fill_in6_addr(&fiter->third_party_ip, NULL, NULL, thirdp_addr); + pcp_flow_updated(fiter); + } +} + +void pcp_flow_set_filter_opt(pcp_flow_t *f, struct sockaddr *filter_ip, + uint8_t filter_prefix) { + pcp_flow_t *fiter; + + for (fiter = f; fiter != NULL; fiter = fiter->next_child) { + if (!fiter->filter_option_present) { + fiter->filter_option_present = 1; + } + pcp_fill_in6_addr(&fiter->filter_ip, &fiter->filter_port, NULL, + filter_ip); + fiter->filter_prefix = filter_prefix; + pcp_flow_updated(fiter); + } +} + +void pcp_flow_set_prefer_failure_opt(pcp_flow_t *f) { + pcp_flow_t *fiter; + + for (fiter = f; fiter != NULL; fiter = fiter->next_child) { + if (!fiter->pfailure_option_present) { + fiter->pfailure_option_present = 1; + pcp_flow_updated(fiter); + } + } +} +#ifdef PCP_EXPERIMENTAL +int pcp_flow_set_userid(pcp_flow_t *f, pcp_userid_option_p user) { + pcp_flow_t *fiter; + + for (fiter = f; fiter; fiter = fiter->next_child) { + memcpy(&(fiter->f_userid.userid[0]), &(user->userid[0]), MAX_USER_ID); + pcp_flow_updated(fiter); + } + return 0; +} + +int pcp_flow_set_location(pcp_flow_t *f, pcp_location_option_p loc) { + pcp_flow_t *fiter; + + for (fiter = f; fiter; fiter = fiter->next_child) { + memcpy(&(fiter->f_location.location[0]), &(loc->location[0]), + MAX_GEO_STR); + pcp_flow_updated(fiter); + } + + return 0; +} + +int pcp_flow_set_deviceid(pcp_flow_t *f, pcp_deviceid_option_p dev) { + pcp_flow_t *fiter; + + for (fiter = f; fiter; fiter = fiter->next_child) { + memcpy(&(fiter->f_deviceid.deviceid[0]), &(dev->deviceid[0]), + MAX_DEVICE_ID); + pcp_flow_updated(fiter); + } + return 0; +} + +void pcp_flow_add_md(pcp_flow_t *f, uint32_t md_id, void *value, + size_t val_len) { + pcp_flow_t *fiter; + + for (fiter = f; fiter != NULL; fiter = fiter->next_child) { + pcp_db_add_md(fiter, md_id, value, val_len); + pcp_flow_updated(fiter); + } +} +#endif + +#ifdef PCP_FLOW_PRIORITY +void pcp_flow_set_flowp(pcp_flow_t *f, uint8_t dscp_up, uint8_t dscp_down) { + pcp_flow_t *fiter; + + for (fiter = f; fiter; fiter = fiter->next_child) { + uint8_t fpresent = (dscp_up != 0) || (dscp_down != 0); + if (fiter->flowp_option_present != fpresent) { + fiter->flowp_option_present = fpresent; + } + if (fpresent) { + fiter->flowp_dscp_up = dscp_up; + fiter->flowp_dscp_down = dscp_down; + } + pcp_flow_updated(fiter); + } +} +#endif + +static inline void pcp_close_flow_intern(pcp_flow_t *f) { + switch (f->state) { + case pfs_wait_for_server_init: + case pfs_idle: + case pfs_failed: + f->state = pfs_failed; + break; + default: + f->lifetime = 0; + pcp_flow_updated(f); + break; + } + if ((f->state != pfs_wait_for_server_init) && (f->state != pfs_idle) && + (f->state != pfs_failed)) { + PCP_LOG_FLOW(f, "Flow closed"); + f->lifetime = 0; + pcp_flow_updated(f); + } else { + f->state = pfs_failed; + } +} + +void pcp_close_flow(pcp_flow_t *f) { + pcp_flow_t *fiter; + + for (fiter = f; fiter; fiter = fiter->next_child) { + pcp_close_flow_intern(fiter); + } + + if (f) { + pcp_pulse(f->ctx, NULL); + } +} + +void pcp_delete_flow(pcp_flow_t *f) { + pcp_flow_t *fiter = f; + pcp_flow_t *fnext = NULL; + + while (fiter) { + fnext = fiter->next_child; + pcp_delete_flow_intern(fiter); + fiter = fnext; + } +} + +static int delete_flow_iter(pcp_flow_t *f, void *data) { + if (data) { + pcp_close_flow_intern(f); + pcp_pulse(f->ctx, NULL); + } + pcp_delete_flow_intern(f); + + return 0; +} + +void pcp_terminate(pcp_ctx_t *ctx, int close_flows) { + pcp_db_foreach_flow(ctx, delete_flow_iter, close_flows ? (void *)1 : NULL); + pcp_db_free_pcp_servers(ctx); + pcp_socket_close(ctx); +} + +pcp_flow_info_t *pcp_flow_get_info(pcp_flow_t *f, size_t *info_count) { + pcp_flow_t *fiter; + pcp_flow_info_t *info_buf; + pcp_flow_info_t *info_iter; + uint32_t cnt = 0; + + if (!info_count) { + return NULL; + } + + for (fiter = f; fiter; fiter = fiter->next_child) { + ++cnt; + } + + info_buf = (pcp_flow_info_t *)calloc(cnt, sizeof(pcp_flow_info_t)); + if (!info_buf) { + PCP_LOG(PCP_LOGLVL_DEBUG, "%s", "Error allocating memory"); + return NULL; + } + + for (fiter = f, info_iter = info_buf; fiter != NULL; + fiter = fiter->next_child, ++info_iter) { + + switch (fiter->state) { + case pfs_wait_after_short_life_error: + info_iter->result = pcp_state_short_lifetime_error; + break; + case pfs_wait_for_lifetime_renew: + info_iter->result = pcp_state_succeeded; + break; + case pfs_failed: + info_iter->result = pcp_state_failed; + break; + default: + info_iter->result = pcp_state_processing; + break; + } + + info_iter->recv_lifetime_end = fiter->recv_lifetime; + info_iter->lifetime_renew_s = fiter->lifetime; + info_iter->pcp_result_code = fiter->recv_result; + memcpy(&info_iter->int_ip, &fiter->kd.src_ip, sizeof(struct in6_addr)); + memcpy(&info_iter->pcp_server_ip, &fiter->kd.pcp_server_ip, + sizeof(info_iter->pcp_server_ip)); + info_iter->int_scope_id = fiter->kd.scope_id; + if ((fiter->kd.operation == PCP_OPCODE_MAP) || + (fiter->kd.operation == PCP_OPCODE_PEER)) { + memcpy(&info_iter->dst_ip, &fiter->kd.map_peer.dst_ip, + sizeof(info_iter->dst_ip)); + memcpy(&info_iter->ext_ip, &fiter->map_peer.ext_ip, + sizeof(info_iter->ext_ip)); + info_iter->int_port = fiter->kd.map_peer.src_port; + info_iter->dst_port = fiter->kd.map_peer.dst_port; + info_iter->ext_port = fiter->map_peer.ext_port; + info_iter->protocol = fiter->kd.map_peer.protocol; +#ifdef PCP_SADSCP + } else if (fiter->kd.operation == PCP_OPCODE_SADSCP) { + info_iter->learned_dscp = fiter->sadscp.learned_dscp; +#endif + } + } + *info_count = cnt; + + return info_buf; +} + +void pcp_flow_set_user_data(pcp_flow_t *f, void *userdata) { + pcp_flow_t *fiter = f; + + while (fiter) { + fiter->user_data = userdata; + fiter = fiter->next_child; + } +} + +void *pcp_flow_get_user_data(pcp_flow_t *f) { + return (f ? f->user_data : NULL); +} + +#ifdef PCP_SADSCP +pcp_flow_t *pcp_learn_dscp(pcp_ctx_t *ctx, uint8_t delay_tol, uint8_t loss_tol, + uint8_t jitter_tol, char *app_name) { + struct flow_key_data kd; + struct caasi_data data; + + memset(&data, 0, sizeof(data)); + memset(&kd, 0, sizeof(kd)); + + kd.operation = PCP_OPCODE_SADSCP; + + data.fprev = NULL; + data.kd = &kd; + data.ffirst = NULL; + data.lifetime = 0; + data.ext_addr = NULL; + data.toler_fields = + (delay_tol & 3) << 6 | ((loss_tol & 3) << 4) | ((jitter_tol & 3) << 2); + data.app_name = app_name; + + pcp_db_foreach_server(ctx, chain_and_assign_src_ip, &data); + + return data.ffirst; +} +#endif diff --git a/lib/libpcp/src/pcp_client_db.c b/lib/libpcpnatpmp/src/pcp_client_db.c similarity index 57% rename from lib/libpcp/src/pcp_client_db.c rename to lib/libpcpnatpmp/src/pcp_client_db.c index 15d0be064be..e1b95015339 100644 --- a/lib/libpcp/src/pcp_client_db.c +++ b/lib/libpcpnatpmp/src/pcp_client_db.c @@ -29,47 +29,46 @@ #include "default_config.h" #endif +#include "pcpnatpmp.h" + +#include "pcp_client_db.h" +#include "pcp_logger.h" +#include "pcp_utils.h" #include +#include #include #include +#include #include #include -#include -#include -#include "pcp.h" -#include "pcp_utils.h" -#include "pcp_client_db.h" -#include "pcp_logger.h" #define EMPTY 0xFFFFFFFF #define PCP_INIT_SERVER_COUNT 5 -static uint32_t compute_flow_key(struct flow_key_data *kd) -{ - uint32_t h=0; - uint8_t *k=(uint8_t*)(kd + 1); +static uint32_t compute_flow_key(struct flow_key_data *kd) { + uint32_t h = 0; + uint8_t *k = (uint8_t *)(kd + 1); - while ((void*)(k--) != (void*)kd) { - uint32_t ho=h & 0xff000000; - h=h << 8; - h=h ^ (ho >> 24); - h=h ^ *k; + while ((void *)(k--) != (void *)kd) { + uint32_t ho = h & 0xff000000; + h = h << 8; + h = h ^ (ho >> 24); + h = h ^ *k; } - h=(h * 0x9E3779B9) >> (32 - FLOW_HASH_BITS); + h = (h * 0x9E3779B9) >> (32 - FLOW_HASH_BITS); return h; } -pcp_flow_t *pcp_create_flow(pcp_server_t *s, struct flow_key_data *fkd) -{ +pcp_flow_t *pcp_create_flow(pcp_server_t *s, struct flow_key_data *fkd) { pcp_flow_t *flow; PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); assert(fkd && s); - flow=(pcp_flow_t*)calloc(1, sizeof(struct pcp_flow_s)); + flow = (pcp_flow_t *)calloc(1, sizeof(struct pcp_flow_s)); if (flow == NULL) { PCP_LOG(PCP_LOGLVL_ERR, "%s", "Malloc can't allocate enough memory for the pcp_flow."); @@ -78,29 +77,27 @@ pcp_flow_t *pcp_create_flow(pcp_server_t *s, struct flow_key_data *fkd) return NULL; } - flow->pcp_msg_len=0; - flow->pcp_server_indx=(s ? s->index : PCP_INV_SERVER); - flow->kd=*fkd; - flow->key_bucket=EMPTY; - flow->ctx=s->ctx; + flow->pcp_msg_len = 0; + flow->pcp_server_indx = (s ? s->index : PCP_INV_SERVER); + flow->kd = *fkd; + flow->key_bucket = EMPTY; + flow->ctx = s->ctx; PCP_LOG_END(PCP_LOGLVL_DEBUG); return flow; } -void pcp_flow_clear_msg_buf(pcp_flow_t *f) -{ +void pcp_flow_clear_msg_buf(pcp_flow_t *f) { if (f) { if (f->pcp_msg_buffer) { free(f->pcp_msg_buffer); - f->pcp_msg_buffer=NULL; + f->pcp_msg_buffer = NULL; } - f->pcp_msg_len=0; + f->pcp_msg_len = 0; } } -pcp_errno pcp_delete_flow_intern(pcp_flow_t *f) -{ +pcp_errno pcp_delete_flow_intern(pcp_flow_t *f) { pcp_server_t *s; assert(f); @@ -123,18 +120,17 @@ pcp_errno pcp_delete_flow_intern(pcp_flow_t *f) } #endif - if ((f->pcp_server_indx != PCP_INV_SERVER) - && ((s=get_pcp_server(f->ctx, f->pcp_server_indx)) != NULL) - && (s->ping_flow_msg == f)) { - s->ping_flow_msg=NULL; + if ((f->pcp_server_indx != PCP_INV_SERVER) && + ((s = get_pcp_server(f->ctx, f->pcp_server_indx)) != NULL) && + (s->ping_flow_msg == f)) { + s->ping_flow_msg = NULL; } free(f); return PCP_ERR_SUCCESS; } -pcp_errno pcp_db_add_flow(pcp_flow_t *f) -{ +pcp_errno pcp_db_add_flow(pcp_flow_t *f) { uint32_t indx; pcp_flow_t **fdb; pcp_ctx_t *ctx; @@ -143,16 +139,17 @@ pcp_errno pcp_db_add_flow(pcp_flow_t *f) return PCP_ERR_BAD_ARGS; } - ctx=f->ctx; + ctx = f->ctx; - f->key_bucket=indx=compute_flow_key(&f->kd); - PCP_LOG(PCP_LOGLVL_DEBUG, "Adding flow %p, key_bucket %d", - f, f->key_bucket); + f->key_bucket = indx = compute_flow_key(&f->kd); + PCP_LOG(PCP_LOGLVL_DEBUG, "Adding flow %p, key_bucket %d", f, + f->key_bucket); - for (fdb=ctx->pcp_db.flows + indx; (*fdb) != NULL; fdb=&(*fdb)->next); + for (fdb = ctx->pcp_db.flows + indx; (*fdb) != NULL; fdb = &(*fdb)->next) + ; - *fdb=f; - f->next=NULL; + *fdb = f; + f->next = NULL; ctx->pcp_db.flow_cnt++; PCP_LOG(PCP_LOGLVL_DEBUG, "total Number of flows added %zu", @@ -161,8 +158,7 @@ pcp_errno pcp_db_add_flow(pcp_flow_t *f) return PCP_ERR_SUCCESS; } -pcp_flow_t *pcp_get_flow(struct flow_key_data *fkd, pcp_server_t *s) -{ +pcp_flow_t *pcp_get_flow(struct flow_key_data *fkd, pcp_server_t *s) { pcp_flow_t **fdb; uint32_t bucket; uint32_t pcp_server_index; @@ -170,13 +166,14 @@ pcp_flow_t *pcp_get_flow(struct flow_key_data *fkd, pcp_server_t *s) if ((!fkd) || (!s) || (!s->ctx)) { return NULL; } - pcp_server_index=s->index; + pcp_server_index = s->index; - bucket=compute_flow_key(fkd); + bucket = compute_flow_key(fkd); PCP_LOG(PCP_LOGLVL_DEBUG, "Computed key_bucket %d", bucket); - for (fdb=&s->ctx->pcp_db.flows[bucket]; (*fdb) != NULL; fdb=&(*fdb)->next) { - if (((*fdb)->pcp_server_indx == pcp_server_index) - && (0 == memcmp(fkd, &(*fdb)->kd, sizeof(*fkd)))) { + for (fdb = &s->ctx->pcp_db.flows[bucket]; (*fdb) != NULL; + fdb = &(*fdb)->next) { + if (((*fdb)->pcp_server_indx == pcp_server_index) && + (0 == memcmp(fkd, &(*fdb)->kd, sizeof(*fkd)))) { return *fdb; } } @@ -184,9 +181,8 @@ pcp_flow_t *pcp_get_flow(struct flow_key_data *fkd, pcp_server_t *s) return NULL; } -pcp_errno pcp_db_rem_flow(pcp_flow_t *f) -{ - pcp_flow_t **fdb=NULL; +pcp_errno pcp_db_rem_flow(pcp_flow_t *f) { + pcp_flow_t **fdb = NULL; pcp_ctx_t *ctx; assert(f && f->ctx); @@ -195,15 +191,15 @@ pcp_errno pcp_db_rem_flow(pcp_flow_t *f) return PCP_ERR_NOT_FOUND; } - ctx=f->ctx; - PCP_LOG(PCP_LOGLVL_DEBUG, "Removing flow %p, key_bucket %d", - f, f->key_bucket); + ctx = f->ctx; + PCP_LOG(PCP_LOGLVL_DEBUG, "Removing flow %p, key_bucket %d", f, + f->key_bucket); - for (fdb=ctx->pcp_db.flows + f->key_bucket; (*fdb) != NULL; - fdb=&((*fdb)->next)) { + for (fdb = ctx->pcp_db.flows + f->key_bucket; (*fdb) != NULL; + fdb = &((*fdb)->next)) { if (*fdb == f) { - (*fdb)->key_bucket=EMPTY; - (*fdb)=(*fdb)->next; + (*fdb)->key_bucket = EMPTY; + (*fdb) = (*fdb)->next; ctx->pcp_db.flow_cnt--; return PCP_ERR_SUCCESS; } @@ -212,79 +208,76 @@ pcp_errno pcp_db_rem_flow(pcp_flow_t *f) return PCP_ERR_NOT_FOUND; } -pcp_errno pcp_db_foreach_flow(pcp_ctx_t *ctx, pcp_db_flow_iterate f, void *data) -{ - pcp_flow_t *fdb, *fdb_next=NULL; +pcp_errno pcp_db_foreach_flow(pcp_ctx_t *ctx, pcp_db_flow_iterate f, + void *data) { + pcp_flow_t *fdb, *fdb_next = NULL; uint32_t indx; assert(f && ctx); - for (indx=0; indx < FLOW_HASH_SIZE; ++indx) { - fdb=ctx->pcp_db.flows[indx]; + for (indx = 0; indx < FLOW_HASH_SIZE; ++indx) { + fdb = ctx->pcp_db.flows[indx]; while (fdb != NULL) { - fdb_next=(fdb->next); + fdb_next = (fdb->next); if ((*f)(fdb, data)) { return PCP_ERR_SUCCESS; } - fdb=fdb_next; + fdb = fdb_next; } - } return PCP_ERR_NOT_FOUND; } #ifdef PCP_EXPERIMENTAL -void pcp_db_add_md(pcp_flow_t *f, uint16_t md_id, void *val, size_t val_len) -{ +void pcp_db_add_md(pcp_flow_t *f, uint16_t md_id, void *val, size_t val_len) { md_val_t *md; uint32_t i; assert(f); - for (i=f->md_val_count, md=f->md_vals; i>0 && md!=NULL; --i, ++md) - { + for (i = f->md_val_count, md = f->md_vals; i > 0 && md != NULL; --i, ++md) { if (md->md_id == md_id) { break; } } - if (i==0) { + if (i == 0) { md = NULL; } if (!md) { - md = (md_val_t*) realloc(f->md_vals, - sizeof(f->md_vals[0])*(f->md_val_count+1)); - if (!md) { //LCOV_EXCL_START + md = (md_val_t *)realloc(f->md_vals, + sizeof(f->md_vals[0]) * (f->md_val_count + 1)); + if (!md) { // LCOV_EXCL_START return; - } //LCOV_EXCL_STOP + } // LCOV_EXCL_STOP f->md_vals = md; md = f->md_vals + f->md_val_count++; } md->md_id = md_id; - if ((val_len>0)&&(val!=NULL)) { - md->val_len = val_len > sizeof(md->val_buf) ? - sizeof(md->val_buf) : val_len; + if ((val_len > 0) && (val != NULL)) { + md->val_len = + val_len > sizeof(md->val_buf) ? sizeof(md->val_buf) : val_len; memcpy(md->val_buf, val, md->val_len); } else { - md->val_len=0; + md->val_len = 0; } } #endif -int pcp_new_server(pcp_ctx_t *ctx, struct in6_addr *ip, uint16_t port, uint32_t scope_id) -{ +int pcp_new_server(pcp_ctx_t *ctx, struct in6_addr *ip, uint16_t port, + uint32_t scope_id) { uint32_t i; - pcp_server_t *ret=NULL; + pcp_server_t *ret = NULL; PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); assert(ctx && ip); - //initialize array of pcp servers, if not already initialized + // initialize array of pcp servers, if not already initialized if (!ctx->pcp_db.pcp_servers) { - ctx->pcp_db.pcp_servers=(pcp_server_t *)calloc(PCP_INIT_SERVER_COUNT, - sizeof(*ctx->pcp_db.pcp_servers)); + ctx->pcp_db.pcp_servers = (pcp_server_t *)calloc( + PCP_INIT_SERVER_COUNT, sizeof(*ctx->pcp_db.pcp_servers)); if (!ctx->pcp_db.pcp_servers) { char buff[ERR_BUF_LEN]; pcp_strerror(errno, buff, sizeof(buff)); @@ -293,23 +286,23 @@ int pcp_new_server(pcp_ctx_t *ctx, struct in6_addr *ip, uint16_t port, uint32_t PCP_LOG_END(PCP_LOGLVL_DEBUG); return PCP_ERR_NO_MEM; } - ctx->pcp_db.pcp_servers_length=PCP_INIT_SERVER_COUNT; + ctx->pcp_db.pcp_servers_length = PCP_INIT_SERVER_COUNT; } - //find first unused record - for (i=0; i < ctx->pcp_db.pcp_servers_length; ++i) { - pcp_server_t *s=ctx->pcp_db.pcp_servers + i; + // find first unused record + for (i = 0; i < ctx->pcp_db.pcp_servers_length; ++i) { + pcp_server_t *s = ctx->pcp_db.pcp_servers + i; if (s->server_state == pss_unitialized) { - ret=s; + ret = s; break; } } // if nothing available double the size of array if (ret == NULL) { - ret=(pcp_server_t *)realloc(ctx->pcp_db.pcp_servers, - sizeof(ctx->pcp_db.pcp_servers[0]) - * (ctx->pcp_db.pcp_servers_length << 1)); + ret = (pcp_server_t *)realloc( + ctx->pcp_db.pcp_servers, sizeof(ctx->pcp_db.pcp_servers[0]) * + (ctx->pcp_db.pcp_servers_length << 1)); if (!ret) { char buff[ERR_BUF_LEN]; @@ -319,39 +312,38 @@ int pcp_new_server(pcp_ctx_t *ctx, struct in6_addr *ip, uint16_t port, uint32_t PCP_LOG_END(PCP_LOGLVL_DEBUG); return PCP_ERR_NO_MEM; } - ctx->pcp_db.pcp_servers=ret; - ret=ctx->pcp_db.pcp_servers + ctx->pcp_db.pcp_servers_length; - memset(ret, 0, sizeof(*ret)*ctx->pcp_db.pcp_servers_length); - ctx->pcp_db.pcp_servers_length<<=1; + ctx->pcp_db.pcp_servers = ret; + ret = ctx->pcp_db.pcp_servers + ctx->pcp_db.pcp_servers_length; + memset(ret, 0, sizeof(*ret) * ctx->pcp_db.pcp_servers_length); + ctx->pcp_db.pcp_servers_length <<= 1; } - ret->epoch=~0; + ret->epoch = ~0; #ifdef PCP_USE_IPV6_SOCKET ret->af = AF_INET6; #else - ret->af=IN6_IS_ADDR_V4MAPPED(ip) ? AF_INET : AF_INET6; + ret->af = IN6_IS_ADDR_V4MAPPED(ip) ? AF_INET : AF_INET6; #endif - IPV6_ADDR_COPY((struct in6_addr*)ret->pcp_ip, ip); - ret->pcp_port=port; - ret->pcp_scope_id=scope_id; - ret->ctx=ctx; - ret->server_state=pss_allocated; - ret->pcp_version=PCP_MAX_SUPPORTED_VERSION; + IPV6_ADDR_COPY((struct in6_addr *)ret->pcp_ip, ip); + ret->pcp_port = port; + ret->pcp_scope_id = IN6_IS_ADDR_LINKLOCAL(ip) ? scope_id : 0; + ret->ctx = ctx; + ret->server_state = pss_allocated; + ret->pcp_version = PCP_MAX_SUPPORTED_VERSION; createNonce(&ret->nonce); - ret->index=ret - ctx->pcp_db.pcp_servers; + ret->index = ret - ctx->pcp_db.pcp_servers; PCP_LOG_END(PCP_LOGLVL_DEBUG); return ret->index; } -pcp_server_t *get_pcp_server(pcp_ctx_t *ctx, int pcp_server_index) -{ +pcp_server_t *get_pcp_server(pcp_ctx_t *ctx, int pcp_server_index) { PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); assert(ctx); - if ((pcp_server_index < 0) - || ((unsigned)pcp_server_index >= ctx->pcp_db.pcp_servers_length)) { + if ((pcp_server_index < 0) || + ((unsigned)pcp_server_index >= ctx->pcp_db.pcp_servers_length)) { PCP_LOG(PCP_LOGLVL_WARN, "server index(%d) out of bounds(%zu)", pcp_server_index, ctx->pcp_db.pcp_servers_length); @@ -359,8 +351,8 @@ pcp_server_t *get_pcp_server(pcp_ctx_t *ctx, int pcp_server_index) return NULL; } - if (ctx->pcp_db.pcp_servers[pcp_server_index].server_state - == pss_unitialized) { + if (ctx->pcp_db.pcp_servers[pcp_server_index].server_state == + pss_unitialized) { PCP_LOG_END(PCP_LOGLVL_DEBUG); return NULL; @@ -371,74 +363,75 @@ pcp_server_t *get_pcp_server(pcp_ctx_t *ctx, int pcp_server_index) } pcp_errno pcp_db_foreach_server(pcp_ctx_t *ctx, pcp_db_server_iterate f, - void *data) -{ + void *data) { uint32_t indx; - int ret=PCP_ERR_MAX_SIZE; + int ret = PCP_ERR_MAX_SIZE; PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); assert(ctx && f); - for (indx=0; indx < ctx->pcp_db.pcp_servers_length; ++indx) { + for (indx = 0; indx < ctx->pcp_db.pcp_servers_length; ++indx) { if (ctx->pcp_db.pcp_servers[indx].server_state == pss_unitialized) { continue; } if ((*f)(ctx->pcp_db.pcp_servers + indx, data)) { - ret=PCP_ERR_SUCCESS; + ret = PCP_ERR_SUCCESS; break; } - } PCP_LOG_END(PCP_LOGLVL_DEBUG); + } + PCP_LOG_END(PCP_LOGLVL_DEBUG); return ret; } typedef struct find_data { struct in6_addr *ip; + uint32_t scope_id; pcp_server_t *found_server; } find_data_t; -static int find_ip(pcp_server_t *s, void *data) -{ - find_data_t *fd=(find_data_t *)data; +static int find_ip(pcp_server_t *s, void *data) { + find_data_t *fd = (find_data_t *)data; - if (IN6_ARE_ADDR_EQUAL(fd->ip, (struct in6_addr *) s->pcp_ip)) { + if (IN6_ARE_ADDR_EQUAL(fd->ip, (struct in6_addr *)s->pcp_ip) && + (fd->scope_id == s->pcp_scope_id)) { - fd->found_server=s; + fd->found_server = s; return 1; } return 0; } -pcp_server_t *get_pcp_server_by_ip(pcp_ctx_t *ctx, struct in6_addr *ip) -{ +pcp_server_t *get_pcp_server_by_ip(pcp_ctx_t *ctx, struct in6_addr *ip, + uint32_t scope_id) { find_data_t fdata; PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); assert(ctx && ip); - fdata.found_server=NULL; - fdata.ip=ip; + fdata.found_server = NULL; + fdata.ip = ip; + fdata.scope_id = IN6_IS_ADDR_LINKLOCAL(ip) ? scope_id : 0; pcp_db_foreach_server(ctx, find_ip, &fdata); PCP_LOG_END(PCP_LOGLVL_DEBUG); return fdata.found_server; } -void pcp_db_free_pcp_servers(pcp_ctx_t *ctx) -{ +void pcp_db_free_pcp_servers(pcp_ctx_t *ctx) { uint32_t i; assert(ctx); - for (i=0; i < ctx->pcp_db.pcp_servers_length; ++i) { - pcp_server_t *s=ctx->pcp_db.pcp_servers + i; - pcp_server_state_e state=s->server_state; + for (i = 0; i < ctx->pcp_db.pcp_servers_length; ++i) { + pcp_server_t *s = ctx->pcp_db.pcp_servers + i; + pcp_server_state_e state = s->server_state; if ((state != pss_unitialized) && (state != pss_allocated)) { run_server_state_machine(s, pcpe_terminate); } } free(ctx->pcp_db.pcp_servers); - ctx->pcp_db.pcp_servers=NULL; - ctx->pcp_db.pcp_servers_length=0; + ctx->pcp_db.pcp_servers = NULL; + ctx->pcp_db.pcp_servers_length = 0; } diff --git a/lib/libpcp/src/pcp_client_db.h b/lib/libpcpnatpmp/src/pcp_client_db.h similarity index 88% rename from lib/libpcp/src/pcp_client_db.h rename to lib/libpcpnatpmp/src/pcp_client_db.h index c2056548de9..25a8b63255c 100644 --- a/lib/libpcp/src/pcp_client_db.h +++ b/lib/libpcpnatpmp/src/pcp_client_db.h @@ -26,13 +26,14 @@ #ifndef PCP_CLIENT_DB_H_ #define PCP_CLIENT_DB_H_ -#include -#include "pcp.h" +#include "pcpnatpmp.h" + #include "pcp_event_handler.h" #include "pcp_msg_structs.h" +#include #ifdef WIN32 -#include "unp.h" #include "pcp_win_defines.h" +#include "unp.h" #endif #ifdef __cplusplus @@ -40,7 +41,8 @@ extern "C" { #endif typedef enum { - optf_3rd_party=1 << 0, optf_flowp=1 << 1, + optf_3rd_party = 1 << 0, + optf_flowp = 1 << 1, } opt_flags_e; #define PCP_INV_SERVER (~0u) @@ -55,16 +57,17 @@ typedef struct { uint16_t md_id; uint16_t val_len; uint8_t val_buf[MD_VAL_MAX_LEN]; -}md_val_t; +} md_val_t; #endif #define FLOW_HASH_BITS 5 -#define FLOW_HASH_SIZE (2< +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef WIN32 +#include "pcp_gettimeofday.h" +#include "pcp_win_defines.h" +#else +//#include +#include +#include +#include +#include +#include +#endif + +#include "pcpnatpmp.h" + +#include "pcp_event_handler.h" +#include "pcp_logger.h" +#include "pcp_msg.h" +#include "pcp_msg_structs.h" +#include "pcp_server_discovery.h" +#include "pcp_socket.h" +#include "pcp_utils.h" + +#define MIN(a, b) (a < b ? a : b) +#define MAX(a, b) (a > b ? a : b) +#define PCP_RT(rtprev) \ + ((rtprev = rtprev << 1), \ + (((8192 + (1024 - (rand() & 2047))) * \ + MIN(MAX(rtprev, PCP_RETX_IRT), PCP_RETX_MRT)) >> \ + 13)) + +static pcp_flow_event_e fhndl_send(pcp_flow_t *f, pcp_recv_msg_t *msg); +static pcp_flow_event_e fhndl_resend(pcp_flow_t *f, pcp_recv_msg_t *msg); +static pcp_flow_event_e fhndl_send_renew(pcp_flow_t *f, pcp_recv_msg_t *msg); +static pcp_flow_event_e fhndl_shortlifeerror(pcp_flow_t *f, + pcp_recv_msg_t *msg); +static pcp_flow_event_e fhndl_received_success(pcp_flow_t *f, + pcp_recv_msg_t *msg); +static pcp_flow_event_e fhndl_clear_timeouts(pcp_flow_t *f, + pcp_recv_msg_t *msg); +static pcp_flow_event_e fhndl_waitresp(pcp_flow_t *f, pcp_recv_msg_t *msg); + +static pcp_server_state_e handle_wait_io_receive_msg(pcp_server_t *s); +static pcp_server_state_e handle_server_ping(pcp_server_t *s); +static pcp_server_state_e handle_wait_ping_resp_timeout(pcp_server_t *s); +static pcp_server_state_e handle_wait_ping_resp_recv(pcp_server_t *s); +static pcp_server_state_e handle_version_negotiation(pcp_server_t *s); +static pcp_server_state_e handle_send_all_msgs(pcp_server_t *s); +static pcp_server_state_e handle_server_restart(pcp_server_t *s); +static pcp_server_state_e handle_wait_io_timeout(pcp_server_t *s); +static pcp_server_state_e handle_server_set_not_working(pcp_server_t *s); +static pcp_server_state_e handle_server_not_working(pcp_server_t *s); +static pcp_server_state_e handle_server_reping(pcp_server_t *s); +static pcp_server_state_e pcp_terminate_server(pcp_server_t *s); +static pcp_server_state_e log_unexepected_state_event(pcp_server_t *s); +static pcp_server_state_e ignore_events(pcp_server_t *s); + +#if PCP_MAX_LOG_LEVEL >= PCP_LOGLVL_DEBUG + +// LCOV_EXCL_START +static const char *dbg_get_func_name(void *f) { + if (f == fhndl_send) { + return "fhndl_send"; + } else if (f == fhndl_send_renew) { + return "fhndl_send_renew"; + } else if (f == fhndl_resend) { + return "fhndl_resend"; + } else if (f == fhndl_shortlifeerror) { + return "fhndl_shortlifeerror"; + } else if (f == fhndl_received_success) { + return "fhndl_received_success"; + } else if (f == fhndl_clear_timeouts) { + return "fhndl_clear_timeouts"; + } else if (f == fhndl_waitresp) { + return "fhndl_waitresp"; + } else if (f == handle_wait_io_receive_msg) { + return "handle_wait_io_receive_msg"; + } else if (f == handle_server_ping) { + return "handle_server_ping"; + } else if (f == handle_wait_ping_resp_timeout) { + return "handle_wait_ping_resp_timeout"; + } else if (f == handle_wait_ping_resp_recv) { + return "handle_wait_ping_resp_recv"; + } else if (f == handle_version_negotiation) { + return "handle_version_negotiation"; + } else if (f == handle_send_all_msgs) { + return "handle_send_all_msgs"; + } else if (f == handle_server_restart) { + return "handle_server_restart"; + } else if (f == handle_wait_io_timeout) { + return "handle_wait_io_timeout"; + } else if (f == handle_server_set_not_working) { + return "handle_server_set_not_working"; + } else if (f == handle_server_not_working) { + return "handle_server_not_working"; + } else if (f == handle_server_reping) { + return "handle_server_reping"; + } else if (f == pcp_terminate_server) { + return "pcp_terminate_server"; + } else if (f == log_unexepected_state_event) { + return "log_unexepected_state_event"; + } else if (f == ignore_events) { + return "ignore_events"; + } else { + return "unknown"; + } +} + +static const char *dbg_get_event_name(pcp_flow_event_e ev) { + static const char *event_names[] = { + "fev_flow_timedout", + "fev_server_initialized", + "fev_send", + "fev_msg_sent", + "fev_failed", + "fev_none", + "fev_server_restarted", + "fev_ignored", + "fev_res_success", + "fev_res_unsupp_version", + "fev_res_not_authorized", + "fev_res_malformed_request", + "fev_res_unsupp_opcode", + "fev_res_unsupp_option", + "fev_res_malformed_option", + "fev_res_network_failure", + "fev_res_no_resources", + "fev_res_unsupp_protocol", + "fev_res_user_ex_quota", + "fev_res_cant_provide_ext", + "fev_res_address_mismatch", + "fev_res_exc_remote_peers", + }; + + assert(((int)ev < sizeof(event_names) / sizeof(event_names[0]))); + + return (int)ev >= 0 ? event_names[ev] : ""; +} + +static const char *dbg_get_state_name(pcp_flow_state_e s) { + static const char *state_names[] = {"pfs_idle", + "pfs_wait_for_server_init", + "pfs_send", + "pfs_wait_resp", + "pfs_wait_after_short_life_error", + "pfs_wait_for_lifetime_renew", + "pfs_send_renew", + "pfs_failed"}; + + assert((int)s < (int)(sizeof(state_names) / sizeof(state_names[0]))); + + return s >= 0 ? state_names[s] : ""; +} + +static const char *dbg_get_sevent_name(pcp_event_e ev) { + static const char *sevent_names[] = {"pcpe_any", "pcpe_timeout", + "pcpe_io_event", "pcpe_terminate"}; + + assert((int)ev < sizeof(sevent_names) / sizeof(sevent_names[0])); + + return sevent_names[ev]; +} + +static const char *dbg_get_sstate_name(pcp_server_state_e s) { + static const char *server_state_names[] = { + "pss_unitialized", + "pss_allocated", + "pss_ping", + "pss_wait_ping_resp", + "pss_version_negotiation", + "pss_send_all_msgs", + "pss_wait_io", + "pss_wait_io_calc_nearest_timeout", + "pss_server_restart", + "pss_server_reping", + "pss_set_not_working", + "pss_not_working"}; + + assert((int)s < + (int)(sizeof(server_state_names) / sizeof(server_state_names[0]))); + + return (s >= 0) ? server_state_names[s] : ""; +} + +static const char *dbg_get_fstate_name(pcp_fstate_e s) { + static const char *flow_state_names[] = { + "pcp_state_processing", "pcp_state_succeeded", + "pcp_state_partial_result", "pcp_state_short_lifetime_error", + "pcp_state_failed"}; + + assert((int)s < + (int)sizeof(flow_state_names) / sizeof(flow_state_names[0])); + + return flow_state_names[s]; +} +// LCOV_EXCL_STOP +#endif + +//////////////////////////////////////////////////////////////////////////////// +// Flow State Machine definition + +typedef pcp_flow_event_e (*handle_flow_state_event)(pcp_flow_t *f, + pcp_recv_msg_t *msg); + +typedef struct pcp_flow_state_trans { + pcp_flow_state_e state_from; + pcp_flow_state_e state_to; + handle_flow_state_event handler; +} pcp_flow_state_trans_t; + +pcp_flow_state_trans_t flow_transitions[] = { + {pfs_any, pfs_wait_resp, fhndl_waitresp}, + {pfs_wait_resp, pfs_send, fhndl_resend}, + {pfs_any, pfs_send, fhndl_send}, + {pfs_any, pfs_wait_after_short_life_error, fhndl_shortlifeerror}, + {pfs_wait_resp, pfs_wait_for_lifetime_renew, fhndl_received_success}, + {pfs_any, pfs_send_renew, fhndl_send_renew}, + {pfs_wait_for_lifetime_renew, pfs_wait_for_lifetime_renew, + fhndl_received_success}, + {pfs_any, pfs_wait_for_server_init, fhndl_clear_timeouts}, + {pfs_any, pfs_failed, fhndl_clear_timeouts}, +}; + +#define FLOW_TRANS_COUNT (sizeof(flow_transitions) / sizeof(*flow_transitions)) + +typedef struct pcp_flow_state_event { + pcp_flow_state_e state; + pcp_flow_event_e event; + pcp_flow_state_e new_state; +} pcp_flow_state_events_t; + +pcp_flow_state_events_t flow_events_sm[] = { + {pfs_any, fev_send, pfs_send}, + {pfs_wait_for_server_init, fev_server_initialized, pfs_send}, + {pfs_wait_resp, fev_res_success, pfs_wait_for_lifetime_renew}, + {pfs_wait_resp, fev_res_unsupp_version, pfs_wait_for_server_init}, + {pfs_wait_resp, fev_res_network_failure, pfs_wait_after_short_life_error}, + {pfs_wait_resp, fev_res_no_resources, pfs_wait_after_short_life_error}, + {pfs_wait_resp, fev_res_exc_remote_peers, pfs_wait_after_short_life_error}, + {pfs_wait_resp, fev_res_user_ex_quota, pfs_wait_after_short_life_error}, + {pfs_wait_resp, fev_flow_timedout, pfs_send}, + {pfs_wait_resp, fev_server_initialized, pfs_send}, + {pfs_send, fev_server_initialized, pfs_send}, + {pfs_send, fev_msg_sent, pfs_wait_resp}, + {pfs_send, fev_flow_timedout, pfs_send}, + {pfs_wait_after_short_life_error, fev_flow_timedout, pfs_send}, + {pfs_wait_for_lifetime_renew, fev_flow_timedout, pfs_send_renew}, + {pfs_wait_for_lifetime_renew, fev_res_success, pfs_wait_for_lifetime_renew}, + {pfs_wait_for_lifetime_renew, fev_res_unsupp_version, + pfs_wait_for_server_init}, + {pfs_wait_for_lifetime_renew, fev_res_network_failure, pfs_send_renew}, + {pfs_wait_for_lifetime_renew, fev_res_no_resources, pfs_send_renew}, + {pfs_wait_for_lifetime_renew, fev_res_exc_remote_peers, pfs_send_renew}, + {pfs_wait_for_lifetime_renew, fev_failed, pfs_send}, + {pfs_wait_for_lifetime_renew, fev_res_user_ex_quota, pfs_send_renew}, + {pfs_send_renew, fev_msg_sent, pfs_wait_for_lifetime_renew}, + {pfs_send_renew, fev_flow_timedout, pfs_send_renew}, + {pfs_send_renew, fev_failed, pfs_send}, + {pfs_send, fev_ignored, pfs_wait_for_lifetime_renew}, + // { pfs_failed, fev_server_restarted, pfs_send}, + {pfs_any, fev_server_restarted, pfs_send}, + {pfs_any, fev_failed, pfs_failed}, + /////////////////////////////////////////////////////////////////////////////// + // Long lifetime Error Responses from PCP server + {pfs_wait_resp, fev_res_not_authorized, pfs_failed}, + {pfs_wait_resp, fev_res_malformed_request, pfs_failed}, + {pfs_wait_resp, fev_res_unsupp_opcode, pfs_failed}, + {pfs_wait_resp, fev_res_unsupp_option, pfs_failed}, + {pfs_wait_resp, fev_res_unsupp_protocol, pfs_failed}, + {pfs_wait_resp, fev_res_cant_provide_ext, pfs_failed}, + {pfs_wait_resp, fev_res_address_mismatch, pfs_failed}, + {pfs_wait_for_lifetime_renew, fev_res_not_authorized, pfs_failed}, + {pfs_wait_for_lifetime_renew, fev_res_malformed_request, pfs_failed}, + {pfs_wait_for_lifetime_renew, fev_res_unsupp_opcode, pfs_failed}, + {pfs_wait_for_lifetime_renew, fev_res_unsupp_option, pfs_failed}, + {pfs_wait_for_lifetime_renew, fev_res_unsupp_protocol, pfs_failed}, + {pfs_wait_for_lifetime_renew, fev_res_cant_provide_ext, pfs_failed}, + {pfs_wait_for_lifetime_renew, fev_res_address_mismatch, pfs_failed}, +}; + +#define FLOW_EVENTS_SM_COUNT (sizeof(flow_events_sm) / sizeof(*flow_events_sm)) + +static pcp_errno pcp_flow_send_msg(pcp_flow_t *flow, pcp_server_t *s) { + ssize_t ret; + size_t to_send_count; + pcp_ctx_t *ctx = s->ctx; + struct sockaddr_in6 src_saddr; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + + if ((!flow->pcp_msg_buffer) || (flow->pcp_msg_len == 0)) { + build_pcp_msg(flow); + if (flow->pcp_msg_buffer == NULL) { + PCP_LOG(PCP_LOGLVL_DEBUG, "Cannot build PCP MSG (flow bucket:%d)", + flow->key_bucket); + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return PCP_ERR_SEND_FAILED; + } + } + + pcp_fill_sockaddr((struct sockaddr *)&src_saddr, &flow->kd.src_ip, 0, 1, + s->pcp_scope_id); + to_send_count = flow->pcp_msg_len; + + while (to_send_count != 0) { + ret = flow->pcp_msg_len - to_send_count; + + ret = pcp_socket_sendto( + ctx, flow->pcp_msg_buffer + ret, flow->pcp_msg_len - ret, + MSG_DONTWAIT, &src_saddr, (struct sockaddr *)&s->pcp_server_saddr, + SA_LEN((struct sockaddr *)&s->pcp_server_saddr)); + if (ret <= 0) { + PCP_LOG(PCP_LOGLVL_WARN, + "Error occurred while sending " + "PCP packet to server %s", + s->pcp_server_paddr); + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return PCP_ERR_SEND_FAILED; + } + to_send_count -= ret; + } + + PCP_LOG(PCP_LOGLVL_INFO, "Sent PCP MSG (flow bucket:%d)", flow->key_bucket); + + pcp_flow_clear_msg_buf(flow); + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return PCP_ERR_SUCCESS; +} + +static pcp_errno read_msg(pcp_ctx_t *ctx, pcp_recv_msg_t *msg) { + ssize_t ret; + socklen_t src_len = sizeof(msg->rcvd_from_addr); + + memset(msg, 0, sizeof(*msg)); + + if ((ret = pcp_socket_recvfrom(ctx, msg->pcp_msg_buffer, + sizeof(msg->pcp_msg_buffer), MSG_DONTWAIT, + (struct sockaddr *)&msg->rcvd_from_addr, + &src_len, &msg->rcvd_to_addr)) < 0) { + return ret; + } + + msg->pcp_msg_len = ret; + + return PCP_ERR_SUCCESS; +} + +/////////////////////////////////////////////////////////////////////////////// +// Flow State Transitions Handlers + +static pcp_flow_event_e fhndl_send(pcp_flow_t *f, UNUSED pcp_recv_msg_t *msg) { + pcp_server_t *s = get_pcp_server(f->ctx, f->pcp_server_indx); + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + + if (!s) { + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return fev_failed; + } + + if (s->restart_flow_msg == f) { + return fev_ignored; + } + + if (pcp_flow_send_msg(f, s) != PCP_ERR_SUCCESS) { + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return fev_failed; + } + + f->resend_timeout = PCP_RETX_IRT; + // set timeout field + gettimeofday(&f->timeout, NULL); + f->timeout.tv_sec += f->resend_timeout / 1000; + f->timeout.tv_usec += (f->resend_timeout % 1000) * 1000; + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return fev_msg_sent; +} + +static pcp_flow_event_e fhndl_resend(pcp_flow_t *f, + UNUSED pcp_recv_msg_t *msg) { + pcp_server_t *s = get_pcp_server(f->ctx, f->pcp_server_indx); + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + + if (!s) { + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return fev_failed; + } + +#if PCP_RETX_MRC > 0 + if (++f->retry_count >= PCP_RETX_MRC) { + return fev_failed; + } +#endif + + if (pcp_flow_send_msg(f, s) != PCP_ERR_SUCCESS) { + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return fev_failed; + } + + f->resend_timeout = PCP_RT(f->resend_timeout); + +#if (PCP_RETX_MRD > 0) + { + int tdiff = (curtime - f->created_time) * 1000; + if (tdiff > PCP_RETX_MRD) { + return fev_failed; + } + if (tdiff > f->resend_timeout) { + f->resend_timeout = tdiff; + } + } +#endif + + // set timeout field + gettimeofday(&f->timeout, NULL); + f->timeout.tv_sec += f->resend_timeout / 1000; + f->timeout.tv_usec += (f->resend_timeout % 1000) * 1000; + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return fev_msg_sent; +} + +static pcp_flow_event_e fhndl_shortlifeerror(pcp_flow_t *f, + pcp_recv_msg_t *msg) { + PCP_LOG(PCP_LOGLVL_DEBUG, + "f->pcp_server_index=%d, f->state = %d, f->key_bucket=%d", + f->pcp_server_indx, f->state, f->key_bucket); + + f->recv_result = msg->recv_result; + + gettimeofday(&f->timeout, NULL); + f->timeout.tv_sec += msg->recv_lifetime; + + return fev_none; +} + +static pcp_flow_event_e fhndl_received_success(pcp_flow_t *f, + pcp_recv_msg_t *msg) { + struct timeval ctv; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + // avoid integer overflow + if (msg->recv_lifetime > (time_t)LONG_MAX - msg->received_time) { + f->recv_lifetime = LONG_MAX; + } else { + f->recv_lifetime = msg->received_time + msg->recv_lifetime; + } + if ((f->kd.operation == PCP_OPCODE_MAP) || + (f->kd.operation == PCP_OPCODE_PEER)) { + f->map_peer.ext_ip = msg->assigned_ext_ip; + f->map_peer.ext_port = msg->assigned_ext_port; +#ifdef PCP_SADSCP + } else if (f->kd.operation == PCP_OPCODE_SADSCP) { + f->sadscp.learned_dscp = msg->recv_dscp; +#endif + } + f->recv_result = msg->recv_result; + + gettimeofday(&ctv, NULL); + + if (msg->recv_lifetime == 0) { + f->timeout.tv_sec = 0; + f->timeout.tv_usec = 0; + } else { + f->timeout = ctv; + f->timeout.tv_sec += (long int)((f->recv_lifetime - ctv.tv_sec) >> 1); + } + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return fev_none; +} + +static pcp_flow_event_e fhndl_send_renew(pcp_flow_t *f, + UNUSED pcp_recv_msg_t *msg) { + pcp_server_t *s = get_pcp_server(f->ctx, f->pcp_server_indx); + long timeout_add; + + if (!s) { + return fev_failed; + } + + if (pcp_flow_send_msg(f, s) != PCP_ERR_SUCCESS) { + return fev_failed; + } + + gettimeofday(&f->timeout, NULL); + timeout_add = (long)((f->recv_lifetime - f->timeout.tv_sec) >> 1); + + if (timeout_add == 0) { + return fev_failed; + } else { + f->timeout.tv_sec += timeout_add; + } + + return fev_msg_sent; +} + +static pcp_flow_event_e fhndl_clear_timeouts(pcp_flow_t *f, + pcp_recv_msg_t *msg) { + if (msg) { + f->recv_result = msg->recv_result; + } + pcp_flow_clear_msg_buf(f); + f->timeout.tv_sec = 0; + f->timeout.tv_usec = 0; + + return fev_none; +} + +static pcp_flow_event_e fhndl_waitresp(pcp_flow_t *f, + UNUSED pcp_recv_msg_t *msg) { + struct timeval ctv; + + gettimeofday(&ctv, NULL); + if (timeval_comp(&f->timeout, &ctv) < 0) { + return fev_failed; + } + + return fev_none; +} + +static void flow_change_notify(pcp_flow_t *flow, pcp_fstate_e state); + +static pcp_flow_state_e handle_flow_event(pcp_flow_t *f, pcp_flow_event_e ev, + pcp_recv_msg_t *r) { + pcp_flow_state_e cur_state = f->state, next_state; + pcp_flow_state_events_t *esm; + pcp_flow_state_events_t *esm_end = flow_events_sm + FLOW_EVENTS_SM_COUNT; + pcp_flow_state_trans_t *trans; + pcp_flow_state_trans_t *trans_end = flow_transitions + FLOW_TRANS_COUNT; + pcp_fstate_e before, after; + struct in6_addr prev_ext_addr = f->map_peer.ext_ip; + uint16_t prev_ext_port = f->map_peer.ext_port; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + pcp_eval_flow_state(f, &before); + for (;;) { + for (esm = flow_events_sm; esm < esm_end; ++esm) { + if (((esm->state == cur_state) || (esm->state == pfs_any)) && + (esm->event == ev)) { + break; + } + } + + if (esm == esm_end) { + // TODO:log + goto end; + } + + next_state = esm->new_state; + + for (trans = flow_transitions; trans < trans_end; ++trans) { + if (((trans->state_from == cur_state) || + (trans->state_from == pfs_any)) && + (trans->state_to == next_state)) { + +#if PCP_MAX_LOG_LEVEL >= PCP_LOGLVL_DEBUG + pcp_flow_event_e prev_ev = ev; +#endif + f->state = next_state; + + PCP_LOG_DEBUG( + "Executing event handler %s\n flow \t: %d (server %d)\n" + " states\t: %s => %s\n event\t: %s", + dbg_get_func_name(trans->handler), f->key_bucket, + f->pcp_server_indx, dbg_get_state_name(cur_state), + dbg_get_state_name(next_state), + dbg_get_event_name(prev_ev)); + + ev = trans->handler(f, r); + + PCP_LOG_DEBUG( + "Return from event handler's %s \n result event: %s", + dbg_get_func_name(trans->handler), dbg_get_event_name(ev)); + + cur_state = next_state; + + if (ev == fev_none) { + goto end; + } + break; + } + } + + // no transition handler + if (trans == trans_end) { + f->state = next_state; + goto end; + } + } +end: + pcp_eval_flow_state(f, &after); + if ((before != after) || + (!IN6_ARE_ADDR_EQUAL(&prev_ext_addr, &f->map_peer.ext_ip)) || + (prev_ext_port != f->map_peer.ext_port)) { + flow_change_notify(f, after); + } + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return f->state; +} + +/////////////////////////////////////////////////////////////////////////////// +// Helper functions for server state handlers + +static pcp_flow_t *server_process_rcvd_pcp_msg(pcp_server_t *s, + pcp_recv_msg_t *msg) { + pcp_flow_t *f; +#ifndef PCP_DISABLE_NATPMP + if (msg->recv_version == 0) { + if (msg->kd.operation == NATPMP_OPCODE_ANNOUNCE) { + s->natpmp_ext_addr = S6_ADDR32(&msg->assigned_ext_ip)[3]; + if ((s->pcp_version == 0) && (s->ping_flow_msg) && + (s->ping_flow_msg->kd.operation == PCP_OPCODE_ANNOUNCE)) { + f = s->ping_flow_msg; + } else { + f = NULL; + } + } else { + S6_ADDR32(&msg->assigned_ext_ip)[3] = s->natpmp_ext_addr; + S6_ADDR32(&msg->assigned_ext_ip)[2] = htonl(0xFFFF); + S6_ADDR32(&msg->assigned_ext_ip)[1] = 0; + S6_ADDR32(&msg->assigned_ext_ip)[0] = 0; + + f = pcp_get_flow(&msg->kd, s); + } + } else { + f = pcp_get_flow(&msg->kd, s); + } +#else + f = pcp_get_flow(&msg->kd, s); +#endif + + if (!f) { + char in6[INET6_ADDRSTRLEN]; + + PCP_LOG(PCP_LOGLVL_INFO, "%s", + "Couldn't find matching flow to received PCP message."); + PCP_LOG(PCP_LOGLVL_PERR, " Operation : %u", msg->kd.operation); + if ((msg->kd.operation == PCP_OPCODE_MAP) || + (msg->kd.operation == PCP_OPCODE_PEER)) { + PCP_LOG(PCP_LOGLVL_PERR, " Protocol : %u", + msg->kd.map_peer.protocol); + PCP_LOG(PCP_LOGLVL_PERR, " Source : %s:%hu", + inet_ntop(s->af, &msg->kd.src_ip, in6, sizeof(in6)), + ntohs(msg->kd.map_peer.src_port)); + PCP_LOG( + PCP_LOGLVL_PERR, " Destination : %s:%hu", + inet_ntop(s->af, &msg->kd.map_peer.dst_ip, in6, sizeof(in6)), + ntohs(msg->kd.map_peer.dst_port)); + } else { + // TODO: add print of SADSCP params + } + return NULL; + } + + PCP_LOG(PCP_LOGLVL_INFO, "Found matching flow %d to received PCP message.", + f->key_bucket); + + handle_flow_event(f, FEV_RES_BEGIN + msg->recv_result, msg); + + return f; +} + +static int check_flow_timeout(pcp_flow_t *f, void *timeout) { + struct timeval *tout = timeout; + struct timeval ctv; + + if ((f->timeout.tv_sec == 0) && (f->timeout.tv_usec == 0)) { + return 0; + } + + gettimeofday(&ctv, NULL); + if (timeval_comp(&f->timeout, &ctv) <= 0) { + // timed out + if (f->state == pfs_wait_resp) { + PCP_LOG(PCP_LOGLVL_WARN, + "Recv of PCP response for flow %d timed out.", + f->key_bucket); + } + handle_flow_event(f, fev_flow_timedout, NULL); + } + + if ((f->timeout.tv_sec == 0) && (f->timeout.tv_usec == 0)) { + return 0; + } + + timeval_subtract(&ctv, &f->timeout, &ctv); + + if ((tout->tv_sec == 0) && (tout->tv_usec == 0)) { + *tout = ctv; + return 0; + } + + if (timeval_comp(&ctv, tout) < 0) { + *tout = ctv; + } + + return 0; +} + +struct get_first_flow_iter_data { + pcp_server_t *s; + pcp_flow_t *msg; +}; + +static int get_first_flow_iter(pcp_flow_t *f, void *data) { + struct get_first_flow_iter_data *d = + (struct get_first_flow_iter_data *)data; + + if (f->pcp_server_indx != d->s->index) { + return 0; + } + switch (f->state) { + case pfs_idle: + case pfs_wait_for_server_init: + case pfs_send: + d->msg = f; + return 1; + default: + return 0; + } +} + +#ifndef PCP_DISABLE_NATPMP +static inline pcp_flow_t *create_natpmp_ann_msg(pcp_server_t *s) { + struct flow_key_data kd; + + memset(&kd, 0, sizeof(kd)); + memcpy(&kd.src_ip, s->src_ip, sizeof(kd.src_ip)); + memcpy(&kd.pcp_server_ip, s->pcp_ip, sizeof(kd.pcp_server_ip)); + memcpy(&kd.nonce, &s->nonce, sizeof(kd.nonce)); + kd.operation = NATPMP_OPCODE_ANNOUNCE; + + s->ping_flow_msg = pcp_create_flow(s, &kd); + pcp_db_add_flow(s->ping_flow_msg); + + return s->ping_flow_msg; +} +#endif + +static inline pcp_flow_t *get_ping_msg(pcp_server_t *s) { + struct get_first_flow_iter_data find_data; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + if (!s) + return NULL; + + find_data.s = s; + find_data.msg = NULL; + + pcp_db_foreach_flow(s->ctx, get_first_flow_iter, &find_data); + + s->ping_flow_msg = find_data.msg; + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return find_data.msg; +} + +struct flow_iterator_data { + pcp_server_t *s; + pcp_flow_event_e event; +}; + +static int flow_send_event_iter(pcp_flow_t *f, void *data) { + struct flow_iterator_data *d = (struct flow_iterator_data *)data; + + if (f->pcp_server_indx == d->s->index) { + handle_flow_event(f, d->event, NULL); + check_flow_timeout(f, &d->s->next_timeout); + } + + return 0; +} + +/////////////////////////////////////////////////////////////////////////////// +// Server state machine event handlers + +static pcp_server_state_e handle_server_ping(pcp_server_t *s) { + pcp_flow_t *msg = NULL; + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + s->ping_count = 0; + + while ((msg = get_ping_msg(s)) != NULL) { + msg->retry_count = 0; + + PCP_LOG(PCP_LOGLVL_INFO, "Pinging PCP server at address %s", + s->pcp_server_paddr); + + if (handle_flow_event(msg, fev_send, NULL) != pfs_failed) { + s->next_timeout = msg->timeout; + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return pss_wait_ping_resp; + } + } + s->next_timeout.tv_sec = 0; + s->next_timeout.tv_usec = 0; + return pss_ping; +} + +static pcp_server_state_e handle_wait_ping_resp_timeout(pcp_server_t *s) { + if (++s->ping_count >= PCP_MAX_PING_COUNT) { + gettimeofday(&s->next_timeout, NULL); + return pss_set_not_working; + } + + if (!s->ping_flow_msg) { + gettimeofday(&s->next_timeout, NULL); + return pss_ping; + } + + if (handle_flow_event(s->ping_flow_msg, fev_flow_timedout, NULL) == + pfs_failed) { + gettimeofday(&s->next_timeout, NULL); + return pss_set_not_working; + } + + if (s->ping_flow_msg) { + s->next_timeout = s->ping_flow_msg->timeout; + } else { + s->next_timeout.tv_sec = 0; + s->next_timeout.tv_usec = 0; + return pss_ping; + } + return pss_wait_ping_resp; +} + +static pcp_server_state_e handle_wait_ping_resp_recv(pcp_server_t *s) { + pcp_server_state_e res = handle_wait_io_receive_msg(s); + + switch (res) { + case pss_wait_io_calc_nearest_timeout: + res = pss_send_all_msgs; + break; + case pss_wait_io: + res = pss_wait_ping_resp; + break; + default: + break; + } + return res; +} + +static pcp_server_state_e handle_version_negotiation(pcp_server_t *s) { + pcp_flow_t *ping_msg; + + if (s->next_version == s->pcp_version) { + s->next_version--; + } + + if (s->pcp_version == 0 +#if PCP_MIN_SUPPORTED_VERSION > 0 + || (s->next_version < PCP_MIN_SUPPORTED_VERSION) +#endif + ) { + PCP_LOG(PCP_LOGLVL_WARN, + "Version negotiation failed for PCP server %s. " + "Disabling sending PCP messages to this server.", + s->pcp_server_paddr); + + return pss_set_not_working; + } + + PCP_LOG(PCP_LOGLVL_INFO, + "Version %d not supported by server %s. Trying version %d.", + s->pcp_version, s->pcp_server_paddr, s->next_version); + s->pcp_version = s->next_version; + + ping_msg = s->ping_flow_msg; + +#ifndef PCP_DISABLE_NATPMP + if (s->pcp_version == 0) { + if (ping_msg) { + ping_msg->state = pfs_wait_for_server_init; + ping_msg->timeout.tv_sec = 0; + ping_msg->timeout.tv_usec = 0; + } + ping_msg = create_natpmp_ann_msg(s); + } +#endif + + if (!ping_msg) { + ping_msg = get_ping_msg(s); + if (!ping_msg) { + s->next_timeout.tv_sec = 0; + s->next_timeout.tv_usec = 0; + return pss_ping; + } + } + + ping_msg->retry_count = 0; + ping_msg->resend_timeout = 0; + + handle_flow_event(ping_msg, fev_send, NULL); + if (ping_msg->state == pfs_failed) { + return pss_set_not_working; + } + + s->next_timeout = ping_msg->timeout; + + return pss_wait_ping_resp; +} + +static pcp_server_state_e handle_send_all_msgs(pcp_server_t *s) { + struct flow_iterator_data d = {s, fev_server_initialized}; + + pcp_db_foreach_flow(s->ctx, flow_send_event_iter, &d); + gettimeofday(&s->next_timeout, NULL); + + return pss_wait_io_calc_nearest_timeout; +} + +static pcp_server_state_e handle_server_restart(pcp_server_t *s) { + struct flow_iterator_data d = {s, fev_server_restarted}; + + pcp_db_foreach_flow(s->ctx, flow_send_event_iter, &d); + s->restart_flow_msg = NULL; + gettimeofday(&s->next_timeout, NULL); + + return pss_wait_io_calc_nearest_timeout; +} + +static pcp_server_state_e handle_wait_io_receive_msg(pcp_server_t *s) { + pcp_recv_msg_t *msg = &s->ctx->msg; + pcp_flow_t *f; + + PCP_LOG(PCP_LOGLVL_INFO, + "Received PCP packet from server at %s, size %d, result_code %d, " + "epoch %d", + s->pcp_server_paddr, msg->pcp_msg_len, msg->recv_result, + msg->recv_epoch); + + switch (msg->recv_result) { + case PCP_RES_UNSUPP_VERSION: + PCP_LOG(PCP_LOGLVL_DEBUG, + "PCP server %s returned " + "result_code=Unsupported version", + s->pcp_server_paddr); + gettimeofday(&s->next_timeout, NULL); + s->next_version = msg->recv_version; + return pss_version_negotiation; + case PCP_RES_ADDRESS_MISMATCH: + PCP_LOG(PCP_LOGLVL_WARN, + "There is PCP-unaware NAT present " + "between client and PCP server %s. " + "Sending of PCP messages was disabled.", + s->pcp_server_paddr); + gettimeofday(&s->next_timeout, NULL); + return pss_set_not_working; + } + + f = server_process_rcvd_pcp_msg(s, msg); + + if (compare_epochs(msg, s)) { + s->epoch = msg->recv_epoch; + s->cepoch = msg->received_time; + gettimeofday(&s->next_timeout, NULL); + s->restart_flow_msg = f; + + return pss_server_restart; + } + + gettimeofday(&s->next_timeout, NULL); + + return pss_wait_io_calc_nearest_timeout; +} + +static pcp_server_state_e handle_wait_io_timeout(pcp_server_t *s) { + struct timeval ctv; + + s->next_timeout.tv_sec = 0; + s->next_timeout.tv_usec = 0; + + pcp_db_foreach_flow(s->ctx, check_flow_timeout, &s->next_timeout); + + if ((s->next_timeout.tv_sec != 0) || (s->next_timeout.tv_usec != 0)) { + gettimeofday(&ctv, NULL); + s->next_timeout.tv_sec += ctv.tv_sec; + s->next_timeout.tv_usec += ctv.tv_usec; + timeval_align(&s->next_timeout); + } + + return pss_wait_io; +} + +static pcp_server_state_e handle_server_set_not_working(pcp_server_t *s) { + struct flow_iterator_data d = {s, fev_failed}; + + PCP_LOG(PCP_LOGLVL_DEBUG, "Entered function %s", __FUNCTION__); + PCP_LOG(PCP_LOGLVL_WARN, + "PCP server %s failed to respond. " + "Disabling sending of PCP messages to this server for %d minutes.", + s->pcp_server_paddr, PCP_SERVER_DISCOVERY_RETRY_DELAY / 60); + + pcp_db_foreach_flow(s->ctx, flow_send_event_iter, &d); + + gettimeofday(&s->next_timeout, NULL); + s->next_timeout.tv_sec += PCP_SERVER_DISCOVERY_RETRY_DELAY; + + return pss_not_working; +} + +static pcp_server_state_e handle_server_not_working(pcp_server_t *s) { + struct timeval ctv; + + gettimeofday(&ctv, NULL); + if (timeval_comp(&ctv, &s->next_timeout) < 0) { + pcp_recv_msg_t *msg = &s->ctx->msg; + pcp_flow_t *f; + + PCP_LOG(PCP_LOGLVL_INFO, + "Received PCP packet from server at %s, size %d, result_code " + "%d, epoch %d", + s->pcp_server_paddr, msg->pcp_msg_len, msg->recv_result, + msg->recv_epoch); + + switch (msg->recv_result) { + case PCP_RES_UNSUPP_VERSION: + return pss_not_working; + case PCP_RES_ADDRESS_MISMATCH: + return pss_not_working; + } + + f = server_process_rcvd_pcp_msg(s, msg); + + s->epoch = msg->recv_epoch; + s->cepoch = msg->received_time; + gettimeofday(&s->next_timeout, NULL); + s->restart_flow_msg = f; + + return pss_server_restart; + } + + s->next_timeout = ctv; + + return pss_server_reping; +} + +static pcp_server_state_e handle_server_reping(pcp_server_t *s) { + PCP_LOG(PCP_LOGLVL_INFO, "Trying to ping PCP server %s again. ", + s->pcp_server_paddr); + + s->pcp_version = PCP_MAX_SUPPORTED_VERSION; + gettimeofday(&s->next_timeout, NULL); + + return pss_ping; +} + +static pcp_server_state_e pcp_terminate_server(pcp_server_t *s) { + s->next_timeout.tv_sec = 0; + s->next_timeout.tv_usec = 0; + + PCP_LOG(PCP_LOGLVL_INFO, "PCP server %s terminated. ", s->pcp_server_paddr); + + return pss_allocated; +} + +static pcp_server_state_e ignore_events(pcp_server_t *s) { + s->next_timeout.tv_sec = 0; + s->next_timeout.tv_usec = 0; + + return s->server_state; +} + +// LCOV_EXCL_START +static pcp_server_state_e log_unexepected_state_event(pcp_server_t *s) { + PCP_LOG(PCP_LOGLVL_PERR, + "Event happened in the state %d on PCP server %s" + " and there is no event handler defined.", + s->server_state, s->pcp_server_paddr); + + gettimeofday(&s->next_timeout, NULL); + return pss_set_not_working; +} +// LCOV_EXCL_STOP +//////////////////////////////////////////////////////////////////////////////// +// Server State Machine definition + +typedef pcp_server_state_e (*handle_server_state_event)(pcp_server_t *s); + +typedef struct pcp_server_state_machine { + pcp_server_state_e state; + pcp_event_e event; + handle_server_state_event handler; +} pcp_server_state_machine_t; + +pcp_server_state_machine_t server_sm[] = { + {pss_any, pcpe_terminate, pcp_terminate_server}, + // -> allocated + {pss_ping, pcpe_any, handle_server_ping}, + // -> wait_ping_resp | set_not_working + {pss_wait_ping_resp, pcpe_timeout, handle_wait_ping_resp_timeout}, + // -> wait_ping_resp | set_not_working + {pss_wait_ping_resp, pcpe_io_event, handle_wait_ping_resp_recv}, + // -> wait ping_resp | pss_send_waiting_msgs | set_not_working | version_neg + {pss_version_negotiation, pcpe_any, handle_version_negotiation}, + // -> wait ping_resp | set_not_working + {pss_send_all_msgs, pcpe_any, handle_send_all_msgs}, + // -> wait_io + {pss_wait_io, pcpe_io_event, handle_wait_io_receive_msg}, + // -> wait_io_calc_nearest_timeout | server_restart |version_negotiation | + // set_not_working + {pss_wait_io, pcpe_timeout, handle_wait_io_timeout}, + // -> wait_io | server_restart + {pss_wait_io_calc_nearest_timeout, pcpe_any, handle_wait_io_timeout}, + // -> wait_io + {pss_server_restart, pcpe_any, handle_server_restart}, + // -> wait_io + {pss_server_reping, pcpe_any, handle_server_reping}, + // -> ping + {pss_set_not_working, pcpe_any, handle_server_set_not_working}, + // -> not_working + {pss_not_working, pcpe_any, handle_server_not_working}, + // -> reping + {pss_allocated, pcpe_any, ignore_events}, + {pss_any, pcpe_any, log_unexepected_state_event} + // -> last_state +}; + +#define SERVER_STATE_MACHINE_COUNT (sizeof(server_sm) / sizeof(*server_sm)) + +pcp_errno run_server_state_machine(pcp_server_t *s, pcp_event_e event) { + unsigned i; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + if (!s) { + return PCP_ERR_BAD_ARGS; + } + + for (i = 0; i < SERVER_STATE_MACHINE_COUNT; ++i) { + pcp_server_state_machine_t *state_def = server_sm + i; + if ((state_def->state == s->server_state) || + (state_def->state == pss_any)) { + if ((state_def->event == pcpe_any) || (state_def->event == event)) { + PCP_LOG_DEBUG("Executing server state handler %s\n server " + "\t: %s (index %d)\n" + " state\t: %s\n" + " event\t: %s", + dbg_get_func_name(state_def->handler), + s->pcp_server_paddr, s->index, + dbg_get_sstate_name(s->server_state), + dbg_get_sevent_name(event)); + + s->server_state = state_def->handler(s); + + PCP_LOG_DEBUG("Return from server state handler's %s \n " + "result state: %s", + dbg_get_func_name(state_def->handler), + dbg_get_sstate_name(s->server_state)); + + break; + } + } + } + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return PCP_ERR_SUCCESS; +} + +struct hserver_iter_data { + struct timeval *res_timeout; + pcp_event_e ev; +}; + +static int hserver_iter(pcp_server_t *s, void *data) { + pcp_event_e ev = ((struct hserver_iter_data *)data)->ev; + struct timeval *res_timeout = + ((struct hserver_iter_data *)data)->res_timeout; + struct timeval ctv; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + if ((s == NULL) || (s->server_state == pss_unitialized) || (data == NULL)) { + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return 0; + } + + if (ev != pcpe_timeout) + run_server_state_machine(s, ev); + + while (1) { + gettimeofday(&ctv, NULL); + if (((s->next_timeout.tv_sec == 0) && (s->next_timeout.tv_usec == 0)) || + (!timeval_subtract(&ctv, &s->next_timeout, &ctv))) { + break; + } + run_server_state_machine(s, pcpe_timeout); + } + + if ((!res_timeout) || + ((s->next_timeout.tv_sec == 0) && (s->next_timeout.tv_usec == 0))) { + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return 0; + } + + if ((res_timeout->tv_sec == 0) && (res_timeout->tv_usec == 0)) { + + *res_timeout = ctv; + + } else if (timeval_comp(&ctv, res_timeout) < 0) { + + *res_timeout = ctv; + } + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return 0; +} + +//////////////////////////////////////////////////////////////////////////////// +// Exported functions + +int pcp_pulse(pcp_ctx_t *ctx, struct timeval *next_timeout) { + pcp_recv_msg_t *msg; + struct timeval tmp_timeout = {0, 0}; + + if (!ctx) { + return PCP_ERR_BAD_ARGS; + } + + msg = &ctx->msg; + + if (!next_timeout) { + next_timeout = &tmp_timeout; + } + + memset(msg, 1, sizeof(*msg)); + + if (read_msg(ctx, msg) == PCP_ERR_SUCCESS) { + struct in6_addr ip6; + uint32_t scope_id; + pcp_server_t *s; + struct hserver_iter_data param = {NULL, pcpe_io_event}; + + msg->received_time = time(NULL); + + if (!validate_pcp_msg(msg)) { + PCP_LOG(PCP_LOGLVL_PERR, "%s", "Invalid PCP msg"); + goto process_timeouts; + } + + if ((parse_response(msg)) != PCP_ERR_SUCCESS) { + PCP_LOG(PCP_LOGLVL_PERR, "%s", "Cannot parse PCP msg"); + goto process_timeouts; + } + + pcp_fill_in6_addr(&ip6, NULL, &scope_id, + (struct sockaddr *)&msg->rcvd_from_addr); + PCP_LOG(PCP_LOGLVL_DEBUG, "SCOPE_ID: %u", scope_id); + s = get_pcp_server_by_ip(ctx, &ip6, scope_id); + + if (s) { + PCP_LOG(PCP_LOGLVL_DEBUG, "Found server: %s", s->pcp_server_paddr); + msg->pcp_server_indx = s->index; + memcpy(&msg->kd.pcp_server_ip, s->pcp_ip, sizeof(struct in6_addr)); + pcp_fill_in6_addr(&msg->kd.src_ip, NULL, &msg->kd.scope_id, + (struct sockaddr *)&msg->rcvd_to_addr); + if (IPV6_IS_ADDR_ANY(&msg->kd.src_ip)) { + memcpy(&msg->kd.src_ip, s->src_ip, sizeof(struct in6_addr)); + msg->kd.scope_id = scope_id; + } + if (msg->recv_version < 2) { + memcpy(&msg->kd.nonce, &s->nonce, sizeof(struct pcp_nonce)); + } + + // process pcpe_io_event for server + hserver_iter(s, ¶m); + } + } + +process_timeouts : { + struct hserver_iter_data param = {next_timeout, pcpe_timeout}; + pcp_db_foreach_server(ctx, hserver_iter, ¶m); +} + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return (next_timeout->tv_sec * 1000) + (next_timeout->tv_usec / 1000); +} + +void pcp_flow_updated(pcp_flow_t *f) { + struct timeval curtime; + pcp_server_t *s; + + if (!f) + return; + + gettimeofday(&curtime, NULL); + s = get_pcp_server(f->ctx, f->pcp_server_indx); + if (s) { + s->next_timeout = curtime; + } + pcp_flow_clear_msg_buf(f); + f->timeout = curtime; + if ((f->state != pfs_wait_for_server_init) && (f->state != pfs_idle) && + (f->state != pfs_failed)) { + f->state = pfs_send; + } +} + +void pcp_set_flow_change_cb(pcp_ctx_t *ctx, pcp_flow_change_notify cb_fun, + void *cb_arg) { + if (ctx) { + ctx->flow_change_cb_fun = cb_fun; + ctx->flow_change_cb_arg = cb_arg; + } +} + +static void flow_change_notify(pcp_flow_t *flow, pcp_fstate_e state) { + struct sockaddr_storage src_addr, ext_addr; + pcp_ctx_t *ctx = flow->ctx; + + PCP_LOG_DEBUG("Flow's %d state changed to: %s", flow->key_bucket, + dbg_get_fstate_name(state)); + + if (ctx->flow_change_cb_fun) { + pcp_fill_sockaddr((struct sockaddr *)&src_addr, &flow->kd.src_ip, + flow->kd.map_peer.src_port, 0, flow->kd.scope_id); + if (state == pcp_state_succeeded) { + pcp_fill_sockaddr((struct sockaddr *)&ext_addr, + &flow->map_peer.ext_ip, flow->map_peer.ext_port, + 0, 0 /* scope_id */); + } else { + memset(&ext_addr, 0, sizeof(ext_addr)); + ext_addr.ss_family = AF_INET; + } + ctx->flow_change_cb_fun(flow, (struct sockaddr *)&src_addr, + (struct sockaddr *)&ext_addr, state, + ctx->flow_change_cb_arg); + } +} diff --git a/lib/libpcp/src/pcp_event_handler.h b/lib/libpcpnatpmp/src/pcp_event_handler.h similarity index 63% rename from lib/libpcp/src/pcp_event_handler.h rename to lib/libpcpnatpmp/src/pcp_event_handler.h index b4f1a6aa377..0f1768d8fd2 100644 --- a/lib/libpcp/src/pcp_event_handler.h +++ b/lib/libpcpnatpmp/src/pcp_event_handler.h @@ -29,19 +29,22 @@ #include "pcp_msg_structs.h" typedef enum { - pfs_any = -1, - pfs_idle = 0, - pfs_wait_for_server_init = 1, - pfs_send = 2, - pfs_wait_resp = 3, + pfs_any = -1, + pfs_idle = 0, + pfs_wait_for_server_init = 1, + pfs_send = 2, + pfs_wait_resp = 3, pfs_wait_after_short_life_error = 4, - pfs_wait_for_lifetime_renew = 5, - pfs_send_renew = 6, - pfs_failed = 7 + pfs_wait_for_lifetime_renew = 5, + pfs_send_renew = 6, + pfs_failed = 7 } pcp_flow_state_e; typedef enum { - pcpe_any, pcpe_timeout, pcpe_io_event, pcpe_terminate + pcpe_any, + pcpe_timeout, + pcpe_io_event, + pcpe_terminate } pcp_event_e; typedef enum { @@ -54,24 +57,24 @@ typedef enum { fev_server_restarted, fev_ignored, FEV_RES_BEGIN, - fev_res_success = FEV_RES_BEGIN + PCP_RES_SUCCESS, - fev_res_unsupp_version = FEV_RES_BEGIN + PCP_RES_UNSUPP_VERSION, - fev_res_not_authorized = FEV_RES_BEGIN + PCP_RES_NOT_AUTHORIZED, + fev_res_success = FEV_RES_BEGIN + PCP_RES_SUCCESS, + fev_res_unsupp_version = FEV_RES_BEGIN + PCP_RES_UNSUPP_VERSION, + fev_res_not_authorized = FEV_RES_BEGIN + PCP_RES_NOT_AUTHORIZED, fev_res_malformed_request = FEV_RES_BEGIN + PCP_RES_MALFORMED_REQUEST, - fev_res_unsupp_opcode = FEV_RES_BEGIN + PCP_RES_UNSUPP_OPCODE, - fev_res_unsupp_option = FEV_RES_BEGIN + PCP_RES_UNSUPP_OPTION, - fev_res_malformed_option = FEV_RES_BEGIN + PCP_RES_MALFORMED_OPTION, - fev_res_network_failure = FEV_RES_BEGIN + PCP_RES_NETWORK_FAILURE, - fev_res_no_resources = FEV_RES_BEGIN + PCP_RES_NO_RESOURCES, - fev_res_unsupp_protocol = FEV_RES_BEGIN + PCP_RES_UNSUPP_PROTOCOL, - fev_res_user_ex_quota = FEV_RES_BEGIN + PCP_RES_USER_EX_QUOTA, - fev_res_cant_provide_ext = FEV_RES_BEGIN + PCP_RES_CANNOT_PROVIDE_EXTERNAL, - fev_res_address_mismatch = FEV_RES_BEGIN + PCP_RES_ADDRESS_MISMATCH, - fev_res_exc_remote_peers = FEV_RES_BEGIN + PCP_RES_EXCESSIVE_REMOTE_PEERS, + fev_res_unsupp_opcode = FEV_RES_BEGIN + PCP_RES_UNSUPP_OPCODE, + fev_res_unsupp_option = FEV_RES_BEGIN + PCP_RES_UNSUPP_OPTION, + fev_res_malformed_option = FEV_RES_BEGIN + PCP_RES_MALFORMED_OPTION, + fev_res_network_failure = FEV_RES_BEGIN + PCP_RES_NETWORK_FAILURE, + fev_res_no_resources = FEV_RES_BEGIN + PCP_RES_NO_RESOURCES, + fev_res_unsupp_protocol = FEV_RES_BEGIN + PCP_RES_UNSUPP_PROTOCOL, + fev_res_user_ex_quota = FEV_RES_BEGIN + PCP_RES_USER_EX_QUOTA, + fev_res_cant_provide_ext = FEV_RES_BEGIN + PCP_RES_CANNOT_PROVIDE_EXTERNAL, + fev_res_address_mismatch = FEV_RES_BEGIN + PCP_RES_ADDRESS_MISMATCH, + fev_res_exc_remote_peers = FEV_RES_BEGIN + PCP_RES_EXCESSIVE_REMOTE_PEERS, } pcp_flow_event_e; typedef enum { - pss_any=-1, + pss_any = -1, pss_unitialized, pss_allocated, pss_ping, diff --git a/lib/libpcp/src/pcp_logger.c b/lib/libpcpnatpmp/src/pcp_logger.c similarity index 62% rename from lib/libpcp/src/pcp_logger.c rename to lib/libpcpnatpmp/src/pcp_logger.c index a6dd57eab7f..03cbb950fc4 100644 --- a/lib/libpcp/src/pcp_logger.c +++ b/lib/libpcpnatpmp/src/pcp_logger.c @@ -33,67 +33,66 @@ #define _CRT_SECURE_NO_WARNINGS 1 #endif +#include "pcpnatpmp.h" + +#include "pcp_logger.h" #include -#include #include +#include #include #include -#include "pcp.h" -#include "pcp_logger.h" #ifdef _MSC_VER #include "pcp_gettimeofday.h" //gettimeofday() #else #include "sys/time.h" #endif //_MSC_VER -pcp_loglvl_e pcp_log_level=PCP_MAX_LOG_LEVEL; +pcp_loglvl_e pcp_log_level = PCP_MAX_LOG_LEVEL; -void pcp_logger_init(void) -{ +void pcp_logger_init(void) { char *env, *ret; - if ((env=getenv("PCP_LOG_LEVEL"))) { - long lvl=strtol(env, &ret, 0); - if ((ret) && (!*ret) && (lvl>=0) && (lvl<=PCP_MAX_LOG_LEVEL)) { - pcp_log_level=lvl; + if ((env = getenv("PCP_LOG_LEVEL"))) { + long lvl = strtol(env, &ret, 0); + if ((ret) && (!*ret) && (lvl >= 0) && (lvl <= PCP_MAX_LOG_LEVEL)) { + pcp_log_level = lvl; } } } -static void default_logfn(pcp_loglvl_e mode, const char *msg) -{ +static void default_logfn(pcp_loglvl_e mode, const char *msg) { const char *prefix; - static struct timeval prev_timestamp={0, 0}; + static struct timeval prev_timestamp = {0, 0}; struct timeval cur_timestamp; uint64_t diff; gettimeofday(&cur_timestamp, NULL); if ((prev_timestamp.tv_sec == 0) && (prev_timestamp.tv_usec == 0)) { - prev_timestamp=cur_timestamp; - diff=0; + prev_timestamp = cur_timestamp; + diff = 0; } else { - diff=(cur_timestamp.tv_sec - prev_timestamp.tv_sec) * 1000000 - + (cur_timestamp.tv_usec - prev_timestamp.tv_usec); + diff = (cur_timestamp.tv_sec - prev_timestamp.tv_sec) * 1000000 + + (cur_timestamp.tv_usec - prev_timestamp.tv_usec); } switch (mode) { - case PCP_LOGLVL_ERR: - case PCP_LOGLVL_PERR: - prefix="ERROR"; - break; - case PCP_LOGLVL_WARN: - prefix="WARNING"; - break; - case PCP_LOGLVL_INFO: - prefix="INFO"; - break; - case PCP_LOGLVL_DEBUG: - prefix="DEBUG"; - break; - default: - prefix="UNKNOWN"; - break; + case PCP_LOGLVL_ERR: + case PCP_LOGLVL_PERR: + prefix = "ERROR"; + break; + case PCP_LOGLVL_WARN: + prefix = "WARNING"; + break; + case PCP_LOGLVL_INFO: + prefix = "INFO"; + break; + case PCP_LOGLVL_DEBUG: + prefix = "DEBUG"; + break; + default: + prefix = "UNKNOWN"; + break; } fprintf(stderr, "%3llus %03llums %03lluus %-7s: %s\n", @@ -102,17 +101,13 @@ static void default_logfn(pcp_loglvl_e mode, const char *msg) prefix, msg); } -external_logger logger=default_logfn; +external_logger logger = default_logfn; -void pcp_set_loggerfn(external_logger ext_log) -{ - logger=ext_log; -} +void pcp_set_loggerfn(external_logger ext_log) { logger = ext_log; } -void pcp_logger(pcp_loglvl_e log_level, const char *fmt, ...) -{ +void pcp_logger(pcp_loglvl_e log_level, const char *fmt, ...) { int n; - int size=256; /* Guess we need no more than 256 bytes. */ + int size = 256; /* Guess we need no more than 256 bytes. */ char *p, *np; va_list ap; @@ -120,8 +115,8 @@ void pcp_logger(pcp_loglvl_e log_level, const char *fmt, ...) return; } - if (!(p=(char*)malloc(size))) { - return; //LCOV_EXCL_LINE + if (!(p = (char *)malloc(size))) { + return; // LCOV_EXCL_LINE } while (1) { @@ -129,7 +124,7 @@ void pcp_logger(pcp_loglvl_e log_level, const char *fmt, ...) /* Try to print in the allocated space. */ va_start(ap, fmt); - n=vsnprintf(p, size, fmt, ap); + n = vsnprintf(p, size, fmt, ap); va_end(ap); /* If that worked, return the string. */ @@ -140,17 +135,17 @@ void pcp_logger(pcp_loglvl_e log_level, const char *fmt, ...) /* Else try again with more space. */ - if (n > -1) /* glibc 2.1 */ - size=n + 1; /* precisely what is needed */ + if (n > -1) /* glibc 2.1 */ + size = n + 1; /* precisely what is needed */ else /* glibc 2.0 */ - size*=2; /* twice the old size */ //LCOV_EXCL_LINE + size *= 2; /* twice the old size */ // LCOV_EXCL_LINE - if (!(np=(char*)realloc(p, size))) { - free(p); //LCOV_EXCL_LINE - return; //LCOV_EXCL_LINE + if (!(np = (char *)realloc(p, size))) { + free(p); // LCOV_EXCL_LINE + return; // LCOV_EXCL_LINE } - p=np; + p = np; } if (logger) @@ -160,8 +155,7 @@ void pcp_logger(pcp_loglvl_e log_level, const char *fmt, ...) return; } -void pcp_strerror(int errnum, char *buf, size_t buflen) -{ +void pcp_strerror(int errnum, char *buf, size_t buflen) { memset(buf, 0, buflen); @@ -169,8 +163,7 @@ void pcp_strerror(int errnum, char *buf, size_t buflen) strerror_s(buf, buflen, errnum); -#else //WIN32 +#else // WIN32 strerror_r(errnum, buf, buflen); -#endif //WIN32 +#endif // WIN32 } - diff --git a/lib/libpcpnatpmp/src/pcp_logger.h b/lib/libpcpnatpmp/src/pcp_logger.h new file mode 100644 index 00000000000..71c2527d9cf --- /dev/null +++ b/lib/libpcpnatpmp/src/pcp_logger.h @@ -0,0 +1,126 @@ +/* + Copyright (c) 2014 by Cisco Systems, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PCP_LOGGER_H_ +#define PCP_LOGGER_H_ + +#define ERR_BUF_LEN 256 + +#include "pcpnatpmp.h" + +#ifdef NDEBUG +#undef DEBUG +#endif + +void pcp_logger_init(void); + +#ifdef WIN32 +void pcp_logger(pcp_loglvl_e log_level, const char *fmt, ...); +#else +void pcp_logger(pcp_loglvl_e log_level, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); +#endif + +#ifdef DEBUG + +#ifndef PCP_MAX_LOG_LEVEL +// Maximal log level for compile-time check +#define PCP_MAX_LOG_LEVEL PCP_LOGLVL_DEBUG +#endif + +#define PCP_LOG(level, fmt, ...) \ + { \ + if (level <= PCP_MAX_LOG_LEVEL) \ + pcp_logger(level, "FILE: %s:%d; Func: %s:\n " fmt, __FILE__, \ + __LINE__, __FUNCTION__, __VA_ARGS__); \ + } + +#define PCP_LOG_END(level) \ + { \ + if (level <= PCP_MAX_LOG_LEVEL) \ + pcp_logger(level, "FILE: %s:%d; Func: %s: END \n ", __FILE__, \ + __LINE__, __FUNCTION__); \ + } + +#define PCP_LOG_BEGIN(level) \ + { \ + if (level <= PCP_MAX_LOG_LEVEL) \ + pcp_logger(level, "FILE: %s:%d; Func: %s: BEGIN \n ", \ + __FILE__, __LINE__, __FUNCTION__); \ + } + +#else // DEBUG +#ifndef PCP_MAX_LOG_LEVEL +// Maximal log level for compile-time check +#define PCP_MAX_LOG_LEVEL PCP_LOGLVL_INFO +#endif + +#define PCP_LOG(level, fmt, ...) \ + { \ + if (level <= PCP_MAX_LOG_LEVEL) \ + pcp_logger(level, fmt, __VA_ARGS__); \ + } + +#define PCP_LOG_END(level) + +#define PCP_LOG_BEGIN(level) + +#endif // DEBUG +#if PCP_MAX_LOG_LEVEL >= PCP_LOGLVL_DEBUG +#define PCP_LOG_DEBUG(fmt, ...) PCP_LOG(PCP_LOGLVL_DEBUG, fmt, __VA_ARGS__) +#else +#define PCP_LOG_DEBUG(fmt, ...) +#endif + +#if PCP_MAX_LOG_LEVEL >= PCP_LOGLVL_INFO +#define PCP_LOG_FLOW(f, msg) \ + do { \ + if (pcp_log_level >= PCP_LOGLVL_INFO) { \ + char src_buf[INET6_ADDRSTRLEN] = "Unknown"; \ + char dst_buf[INET6_ADDRSTRLEN] = "Unknown"; \ + char pcp_buf[INET6_ADDRSTRLEN] = "Unknown"; \ + \ + inet_ntop(AF_INET6, &f->kd.src_ip, src_buf, sizeof(src_buf)); \ + inet_ntop(AF_INET6, &f->kd.map_peer.dst_ip, dst_buf, \ + sizeof(dst_buf)); \ + inet_ntop(AF_INET6, &f->kd.pcp_server_ip, pcp_buf, \ + sizeof(pcp_buf)); \ + PCP_LOG(PCP_LOGLVL_INFO, \ + "%s(PCP server: %s; Int. addr: [%s]:%d; ScopeId: %u; " \ + "Dest. addr: [%s]:%d; Key bucket: %d)", \ + msg, pcp_buf, src_buf, ntohs(f->kd.map_peer.src_port), \ + f->kd.scope_id, dst_buf, ntohs(f->kd.map_peer.dst_port), \ + f->key_bucket); \ + } \ + } while (0) +#else +#define PCP_LOG_FLOW(f, msg) \ + do { \ + } while (0) +#endif + +void pcp_strerror(int errnum, char *buf, size_t buflen); + +#endif /* PCP_LOGGER_H_ */ diff --git a/lib/libpcpnatpmp/src/pcp_msg.c b/lib/libpcpnatpmp/src/pcp_msg.c new file mode 100644 index 00000000000..54b83456aaa --- /dev/null +++ b/lib/libpcpnatpmp/src/pcp_msg.c @@ -0,0 +1,696 @@ +/* + Copyright (c) 2014 by Cisco Systems, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#else +#include "default_config.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#ifdef WIN32 +#include "pcp_win_defines.h" +#else +#include +#include +#include +#endif // WIN32 +#include "pcpnatpmp.h" + +#include "pcp_logger.h" +#include "pcp_msg.h" +#include "pcp_msg_structs.h" +#include "pcp_utils.h" + +static void *add_filter_option(pcp_flow_t *f, void *cur) { + pcp_filter_option_t *filter_op = (pcp_filter_option_t *)cur; + + filter_op->option = PCP_OPTION_FILTER; + filter_op->reserved = 0; + filter_op->len = + htons(sizeof(pcp_filter_option_t) - sizeof(pcp_options_hdr_t)); + filter_op->reserved2 = 0; + filter_op->filter_prefix = f->filter_prefix; + filter_op->filter_peer_port = f->filter_port; + memcpy(&filter_op->filter_peer_ip, &f->filter_ip, + sizeof(filter_op->filter_peer_ip)); + cur = filter_op->next_data; + + return cur; +} + +static void *add_prefer_failure_option(void *cur) { + pcp_prefer_fail_option_t *pfailure_op = (pcp_prefer_fail_option_t *)cur; + + pfailure_op->option = PCP_OPTION_PREF_FAIL; + pfailure_op->reserved = 0; + pfailure_op->len = + htons(sizeof(pcp_prefer_fail_option_t) - sizeof(pcp_options_hdr_t)); + cur = pfailure_op->next_data; + + return cur; +} + +static void *add_third_party_option(pcp_flow_t *f, void *cur) { + pcp_3rd_party_option_t *tp_op = (pcp_3rd_party_option_t *)cur; + + tp_op->option = PCP_OPTION_3RD_PARTY; + tp_op->reserved = 0; + memcpy(tp_op->ip, &f->third_party_ip, sizeof(f->third_party_ip)); + tp_op->len = htons(sizeof(*tp_op) - sizeof(pcp_options_hdr_t)); + cur = tp_op->next_data; + + return cur; +} + +#ifdef PCP_EXPERIMENTAL +static void *add_userid_option(pcp_flow_t *f, void *cur) { + pcp_userid_option_t *userid_op = (pcp_userid_option_t *)cur; + + userid_op->option = PCP_OPTION_USERID; + userid_op->len = + htons(sizeof(pcp_userid_option_t) - sizeof(pcp_options_hdr_t)); + memcpy(&(userid_op->userid[0]), &(f->f_userid.userid[0]), MAX_USER_ID); + cur = userid_op + 1; + + return cur; +} + +static void *add_location_option(pcp_flow_t *f, void *cur) { + pcp_location_option_t *location_op = (pcp_location_option_t *)cur; + + location_op->option = PCP_OPTION_LOCATION; + location_op->len = + htons(sizeof(pcp_location_option_t) - sizeof(pcp_options_hdr_t)); + memcpy(&(location_op->location[0]), &(f->f_location.location[0]), + MAX_GEO_STR); + cur = location_op + 1; + + return cur; +} + +static void *add_deviceid_option(pcp_flow_t *f, void *cur) { + pcp_deviceid_option_t *deviceid_op = (pcp_deviceid_option_t *)cur; + + deviceid_op->option = PCP_OPTION_DEVICEID; + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + deviceid_op->len = + htons(sizeof(pcp_deviceid_option_t) - sizeof(pcp_options_hdr_t)); + memcpy(&(deviceid_op->deviceid[0]), &(f->f_deviceid.deviceid[0]), + MAX_DEVICE_ID); + cur = deviceid_op + 1; + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return cur; +} +#endif + +#ifdef PCP_FLOW_PRIORITY +static void *add_flowp_option(pcp_flow_t *f, void *cur) { + pcp_flow_priority_option_t *flowp_op = (pcp_flow_priority_option_t *)cur; + + flowp_op->option = PCP_OPTION_FLOW_PRIORITY; + flowp_op->len = + htons(sizeof(pcp_flow_priority_option_t) - sizeof(pcp_options_hdr_t)); + flowp_op->dscp_up = f->flowp_dscp_up; + flowp_op->dscp_down = f->flowp_dscp_down; + cur = flowp_op->next_data; + + return cur; +} +#endif + +#ifdef PCP_EXPERIMENTAL +static inline pcp_metadata_option_t * +add_md_option(pcp_flow_t *f, pcp_metadata_option_t *md_opt, md_val_t *md) { + size_t len_md = md->val_len; + uint32_t padding = (4 - (len_md % 4)) % 4; + size_t pcp_msg_len = ((const char *)md_opt) - f->pcp_msg_buffer; + + if ((pcp_msg_len + (sizeof(pcp_metadata_option_t) + len_md + padding)) > + PCP_MAX_LEN) { + return md_opt; + } + + md_opt->option = PCP_OPTION_METADATA; + md_opt->metadata_id = htonl(md->md_id); + memcpy(md_opt->metadata, md->val_buf, len_md); + md_opt->len = + htons(sizeof(*md_opt) - sizeof(pcp_options_hdr_t) + len_md + padding); + + return (pcp_metadata_option_t *)(((uint8_t *)(md_opt + 1)) + len_md + + padding); +} + +static void *add_md_options(pcp_flow_t *f, void *cur) { + uint32_t i; + md_val_t *md; + pcp_metadata_option_t *md_opt = (pcp_metadata_option_t *)cur; + + for (i = f->md_val_count, md = f->md_vals; i > 0 && md != NULL; --i, ++md) { + if (md->val_len) { + md_opt = add_md_option(f, md_opt, md); + } + } + return md_opt; +} +#endif + +static pcp_errno build_pcp_options(pcp_flow_t *flow, void *cur) { +#ifdef PCP_FLOW_PRIORITY + if (flow->flowp_option_present) { + cur = add_flowp_option(flow, cur); + } +#endif + if (flow->filter_option_present) { + cur = add_filter_option(flow, cur); + } + + if (flow->pfailure_option_present) { + cur = add_prefer_failure_option(cur); + } + if (flow->third_party_option_present) { + cur = add_third_party_option(flow, cur); + } +#ifdef PCP_EXPERIMENTAL + if (flow->f_deviceid.deviceid[0] != '\0') { + cur = add_deviceid_option(flow, cur); + } + + if (flow->f_userid.userid[0] != '\0') { + cur = add_userid_option(flow, cur); + } + + if (flow->f_location.location[0] != '\0') { + cur = add_location_option(flow, cur); + } + + if (flow->md_val_count > 0) { + cur = add_md_options(flow, cur); + } +#endif + + flow->pcp_msg_len = ((char *)cur) - flow->pcp_msg_buffer; + + // TODO: implement building all pcp options into msg + return PCP_ERR_SUCCESS; +} + +static pcp_errno build_pcp_peer(pcp_server_t *server, pcp_flow_t *flow, + void *peer_loc) { + void *next = NULL; + + if (server->pcp_version == 1) { + pcp_peer_v1_t *peer_info = (pcp_peer_v1_t *)peer_loc; + + peer_info->protocol = flow->kd.map_peer.protocol; + peer_info->int_port = flow->kd.map_peer.src_port; + peer_info->ext_port = flow->map_peer.ext_port; + peer_info->peer_port = flow->kd.map_peer.dst_port; + memcpy(peer_info->ext_ip, &flow->map_peer.ext_ip, + sizeof(peer_info->ext_ip)); + memcpy(peer_info->peer_ip, &flow->kd.map_peer.dst_ip, + sizeof(peer_info->peer_ip)); + next = peer_info + 1; + } else if (server->pcp_version == 2) { + pcp_peer_v2_t *peer_info = (pcp_peer_v2_t *)peer_loc; + + peer_info->protocol = flow->kd.map_peer.protocol; + peer_info->int_port = flow->kd.map_peer.src_port; + peer_info->ext_port = flow->map_peer.ext_port; + peer_info->peer_port = flow->kd.map_peer.dst_port; + memcpy(peer_info->ext_ip, &flow->map_peer.ext_ip, + sizeof(peer_info->ext_ip)); + memcpy(peer_info->peer_ip, &flow->kd.map_peer.dst_ip, + sizeof(peer_info->peer_ip)); + peer_info->nonce = flow->kd.nonce; + next = peer_info + 1; + } else { + return PCP_ERR_UNSUP_VERSION; + } + return build_pcp_options(flow, next); +} + +static pcp_errno build_pcp_map(pcp_server_t *server, pcp_flow_t *flow, + void *map_loc) { + void *next = NULL; + + if (server->pcp_version == 1) { + pcp_map_v1_t *map_info = (pcp_map_v1_t *)map_loc; + + map_info->protocol = flow->kd.map_peer.protocol; + map_info->int_port = flow->kd.map_peer.src_port; + map_info->ext_port = flow->map_peer.ext_port; + memcpy(map_info->ext_ip, &flow->map_peer.ext_ip, + sizeof(map_info->ext_ip)); + next = map_info + 1; + } else if (server->pcp_version == 2) { + pcp_map_v2_t *map_info = (pcp_map_v2_t *)map_loc; + + map_info->protocol = flow->kd.map_peer.protocol; + map_info->int_port = flow->kd.map_peer.src_port; + map_info->ext_port = flow->map_peer.ext_port; + memcpy(map_info->ext_ip, &flow->map_peer.ext_ip, + sizeof(map_info->ext_ip)); + map_info->nonce = flow->kd.nonce; + next = map_info + 1; + } else { + return PCP_ERR_UNSUP_VERSION; + } + + return build_pcp_options(flow, next); +} + +#ifdef PCP_SADSCP +static pcp_errno build_pcp_sadscp(pcp_server_t *server, pcp_flow_t *flow, + void *sadscp_loc) { + void *next = NULL; + + if (server->pcp_version == 1) { + return PCP_ERR_UNSUP_VERSION; + } else if (server->pcp_version == 2) { + size_t fill_len; + pcp_sadscp_req_t *sadscp = (pcp_sadscp_req_t *)sadscp_loc; + + sadscp->nonce = flow->kd.nonce; + sadscp->tolerance_fields = flow->sadscp.toler_fields; + + // app name fill size to multiple of 4 + fill_len = (4 - ((flow->sadscp.app_name_length + 2) % 4)) % 4; + + sadscp->app_name_length = flow->sadscp.app_name_length + fill_len; + if (flow->sadscp_app_name) { + memcpy(sadscp->app_name, flow->sadscp_app_name, + flow->sadscp.app_name_length); + } else { + memset(sadscp->app_name, 0, flow->sadscp.app_name_length); + } + + next = ((uint8_t *)sadscp_loc) + sizeof(pcp_sadscp_req_t) + + sadscp->app_name_length; + } else { + return PCP_ERR_UNSUP_VERSION; + } + + return build_pcp_options(flow, next); +} +#endif + +#ifndef PCP_DISABLE_NATPMP +static pcp_errno build_natpmp_msg(pcp_flow_t *flow) { + nat_pmp_announce_req_t *ann_msg; + nat_pmp_map_req_t *map_info; + + switch (flow->kd.operation) { + case PCP_OPCODE_ANNOUNCE: + ann_msg = (nat_pmp_announce_req_t *)flow->pcp_msg_buffer; + ann_msg->ver = 0; + ann_msg->opcode = NATPMP_OPCODE_ANNOUNCE; + flow->pcp_msg_len = sizeof(*ann_msg); + return PCP_RES_SUCCESS; + + case PCP_OPCODE_MAP: + map_info = (nat_pmp_map_req_t *)flow->pcp_msg_buffer; + switch (flow->kd.map_peer.protocol) { + case IPPROTO_TCP: + map_info->opcode = NATPMP_OPCODE_MAP_TCP; + break; + case IPPROTO_UDP: + map_info->opcode = NATPMP_OPCODE_MAP_UDP; + break; + default: + return PCP_RES_UNSUPP_PROTOCOL; + } + map_info->ver = 0; + map_info->lifetime = htonl(flow->lifetime); + map_info->int_port = flow->kd.map_peer.src_port; + map_info->ext_port = flow->map_peer.ext_port; + flow->pcp_msg_len = sizeof(*map_info); + return PCP_RES_SUCCESS; + + default: + return PCP_RES_UNSUPP_OPCODE; + } +} +#endif + +void *build_pcp_msg(pcp_flow_t *flow) { + ssize_t ret = -1; + pcp_server_t *pcp_server = NULL; + pcp_request_t *req; + // pointer used for referencing next data structure in linked list + void *next_data = NULL; + + PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); + + if (!flow) { + return NULL; + } + + pcp_server = get_pcp_server(flow->ctx, flow->pcp_server_indx); + + if (!pcp_server) { + return NULL; + } + + if (!flow->pcp_msg_buffer) { + flow->pcp_msg_buffer = (char *)calloc(1, PCP_MAX_LEN); + if (flow->pcp_msg_buffer == NULL) { + PCP_LOG(PCP_LOGLVL_ERR, "%s", + "Malloc can't allocate enough memory for the pcp_flow."); + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return NULL; + } + } + + req = (pcp_request_t *)flow->pcp_msg_buffer; + + if (pcp_server->pcp_version == 0) { + // NATPMP +#ifndef PCP_DISABLE_NATPMP + ret = build_natpmp_msg(flow); +#endif + } else { + + req->ver = pcp_server->pcp_version; + + req->r_opcode |= (uint8_t)(flow->kd.operation & 0x7f); // set opcode + req->req_lifetime = htonl((uint32_t)flow->lifetime); + + memcpy(&req->ip, &flow->kd.src_ip, 16); + // next data in the packet + next_data = req->next_data; + flow->pcp_msg_len = (uint8_t *)next_data - (uint8_t *)req; + + switch (flow->kd.operation) { + case PCP_OPCODE_PEER: + ret = build_pcp_peer(pcp_server, flow, next_data); + break; + case PCP_OPCODE_MAP: + ret = build_pcp_map(pcp_server, flow, next_data); + break; +#ifdef PCP_SADSCP + case PCP_OPCODE_SADSCP: + ret = build_pcp_sadscp(pcp_server, flow, next_data); + break; +#endif + case PCP_OPCODE_ANNOUNCE: + ret = 0; + break; + } + } + + if (ret < 0) { + PCP_LOG(PCP_LOGLVL_ERR, "%s", "Unsupported operation."); + free(flow->pcp_msg_buffer); + flow->pcp_msg_buffer = NULL; + flow->pcp_msg_len = 0; + req = NULL; + } + + PCP_LOG_END(PCP_LOGLVL_DEBUG); + return req; +} + +int validate_pcp_msg(pcp_recv_msg_t *f) { + pcp_response_t *resp; + + // check size + if (((f->pcp_msg_len & 3) != 0) || (f->pcp_msg_len < 4) || + (f->pcp_msg_len > PCP_MAX_LEN)) { + PCP_LOG(PCP_LOGLVL_WARN, "Received packet with invalid size %d)", + f->pcp_msg_len); + return 0; + } + + resp = (pcp_response_t *)f->pcp_msg_buffer; + if ((resp->ver) && !(resp->r_opcode & 0x80)) { + PCP_LOG(PCP_LOGLVL_WARN, "%s", + "Received packet without response bit set"); + return 0; + } + + if (resp->ver > PCP_MAX_SUPPORTED_VERSION) { + PCP_LOG(PCP_LOGLVL_WARN, + "Received PCP msg using unsupported PCP version %d", resp->ver); + return 0; + } + + return 1; +} + +static pcp_errno parse_options(UNUSED pcp_recv_msg_t *f, UNUSED void *r) { + // TODO: implement parsing of pcp options + return PCP_ERR_SUCCESS; +} + +static pcp_errno parse_v1_map(pcp_recv_msg_t *f, void *r) { + pcp_map_v1_t *m; + size_t rest_size = f->pcp_msg_len - (((char *)r) - f->pcp_msg_buffer); + + if (rest_size < sizeof(pcp_map_v1_t)) { + return PCP_ERR_RECV_FAILED; + } + + m = (pcp_map_v1_t *)r; + f->kd.map_peer.src_port = m->int_port; + f->kd.map_peer.protocol = m->protocol; + f->assigned_ext_port = m->ext_port; + memcpy(&f->assigned_ext_ip, m->ext_ip, sizeof(f->assigned_ext_ip)); + + if (rest_size > sizeof(pcp_map_v1_t)) { + return parse_options(f, m + 1); + } + return PCP_ERR_SUCCESS; +} + +static pcp_errno parse_v2_map(pcp_recv_msg_t *f, void *r) { + pcp_map_v2_t *m; + size_t rest_size = f->pcp_msg_len - (((char *)r) - f->pcp_msg_buffer); + + if (rest_size < sizeof(pcp_map_v2_t)) { + return PCP_ERR_RECV_FAILED; + } + + m = (pcp_map_v2_t *)r; + f->kd.nonce = m->nonce; + f->kd.map_peer.src_port = m->int_port; + f->kd.map_peer.protocol = m->protocol; + f->assigned_ext_port = m->ext_port; + memcpy(&f->assigned_ext_ip, m->ext_ip, sizeof(f->assigned_ext_ip)); + + if (rest_size > sizeof(pcp_map_v2_t)) { + return parse_options(f, m + 1); + } + return PCP_ERR_SUCCESS; +} + +static pcp_errno parse_v1_peer(pcp_recv_msg_t *f, void *r) { + pcp_peer_v1_t *m; + size_t rest_size = f->pcp_msg_len - (((char *)r) - f->pcp_msg_buffer); + + if (rest_size < sizeof(pcp_peer_v1_t)) { + return PCP_ERR_RECV_FAILED; + } + + m = (pcp_peer_v1_t *)r; + f->kd.map_peer.src_port = m->int_port; + f->kd.map_peer.protocol = m->protocol; + f->kd.map_peer.dst_port = m->peer_port; + f->assigned_ext_port = m->ext_port; + memcpy(&f->kd.map_peer.dst_ip, m->peer_ip, sizeof(f->kd.map_peer.dst_ip)); + memcpy(&f->assigned_ext_ip, m->ext_ip, sizeof(f->assigned_ext_ip)); + + if (rest_size > sizeof(pcp_peer_v1_t)) { + return parse_options(f, m + 1); + } + return PCP_ERR_SUCCESS; +} + +static pcp_errno parse_v2_peer(pcp_recv_msg_t *f, void *r) { + pcp_peer_v2_t *m; + size_t rest_size = f->pcp_msg_len - (((char *)r) - f->pcp_msg_buffer); + + if (rest_size < sizeof(pcp_peer_v2_t)) { + return PCP_ERR_RECV_FAILED; + } + + m = (pcp_peer_v2_t *)r; + f->kd.nonce = m->nonce; + f->kd.map_peer.src_port = m->int_port; + f->kd.map_peer.protocol = m->protocol; + f->kd.map_peer.dst_port = m->peer_port; + f->assigned_ext_port = m->ext_port; + memcpy(&f->kd.map_peer.dst_ip, m->peer_ip, sizeof(f->kd.map_peer.dst_ip)); + memcpy(&f->assigned_ext_ip, m->ext_ip, sizeof(f->assigned_ext_ip)); + + if (rest_size > sizeof(pcp_peer_v2_t)) { + return parse_options(f, m + 1); + } + return PCP_ERR_SUCCESS; +} + +#ifdef PCP_SADSCP +static pcp_errno parse_sadscp(pcp_recv_msg_t *f, void *r) { + pcp_sadscp_resp_t *d; + size_t rest_size = f->pcp_msg_len - (((char *)r) - f->pcp_msg_buffer); + + if (rest_size < sizeof(pcp_sadscp_resp_t)) { + return PCP_ERR_RECV_FAILED; + } + d = (pcp_sadscp_resp_t *)r; + f->kd.nonce = d->nonce; + f->recv_dscp = d->a_r_dscp & (0x3f); // mask 6 lower bits + + return PCP_ERR_SUCCESS; +} +#endif + +#ifndef PCP_DISABLE_NATPMP +static pcp_errno parse_v0_resp(pcp_recv_msg_t *f, pcp_response_t *resp) { + switch (f->kd.operation) { + case PCP_OPCODE_ANNOUNCE: + if (f->pcp_msg_len == sizeof(nat_pmp_announce_resp_t)) { + nat_pmp_announce_resp_t *r = (nat_pmp_announce_resp_t *)resp; + + f->recv_epoch = ntohl(r->epoch); + S6_ADDR32(&f->assigned_ext_ip)[0] = 0; + S6_ADDR32(&f->assigned_ext_ip)[1] = 0; + S6_ADDR32(&f->assigned_ext_ip)[2] = htonl(0xFFFF); + S6_ADDR32(&f->assigned_ext_ip)[3] = r->ext_ip; + + return PCP_ERR_SUCCESS; + } + break; + case NATPMP_OPCODE_MAP_TCP: + case NATPMP_OPCODE_MAP_UDP: + if (f->pcp_msg_len == sizeof(nat_pmp_map_resp_t)) { + nat_pmp_map_resp_t *r = (nat_pmp_map_resp_t *)resp; + + f->assigned_ext_port = r->ext_port; + f->kd.map_peer.src_port = r->int_port; + f->recv_epoch = ntohl(r->epoch); + f->recv_lifetime = ntohl(r->lifetime); + f->recv_result = ntohs(r->result); + f->kd.map_peer.protocol = f->kd.operation == NATPMP_OPCODE_MAP_TCP + ? IPPROTO_TCP + : IPPROTO_UDP; + f->kd.operation = PCP_OPCODE_MAP; + return PCP_ERR_SUCCESS; + } + break; + default: + break; + } + + if (f->pcp_msg_len == sizeof(nat_pmp_inv_version_resp_t)) { + nat_pmp_inv_version_resp_t *r = (nat_pmp_inv_version_resp_t *)resp; + + f->recv_result = ntohs(r->result); + f->recv_epoch = ntohl(r->epoch); + return PCP_ERR_SUCCESS; + } + + return PCP_ERR_RECV_FAILED; +} +#endif + +static pcp_errno parse_v1_resp(pcp_recv_msg_t *f, pcp_response_t *resp) { + if (f->pcp_msg_len < sizeof(pcp_response_t)) { + return PCP_ERR_RECV_FAILED; + } + + f->recv_lifetime = ntohl(resp->lifetime); + f->recv_epoch = ntohl(resp->epochtime); + + switch (f->kd.operation) { + case PCP_OPCODE_ANNOUNCE: + return PCP_ERR_SUCCESS; + case PCP_OPCODE_MAP: + return parse_v1_map(f, resp->next_data); + case PCP_OPCODE_PEER: + return parse_v1_peer(f, resp->next_data); + default: + return PCP_ERR_RECV_FAILED; + } +} + +static pcp_errno parse_v2_resp(pcp_recv_msg_t *f, pcp_response_t *resp) { + if (f->pcp_msg_len < sizeof(pcp_response_t)) { + return PCP_ERR_RECV_FAILED; + } + + f->recv_lifetime = ntohl(resp->lifetime); + f->recv_epoch = ntohl(resp->epochtime); + + switch (f->kd.operation) { + case PCP_OPCODE_ANNOUNCE: + return PCP_ERR_SUCCESS; + case PCP_OPCODE_MAP: + return parse_v2_map(f, resp->next_data); + case PCP_OPCODE_PEER: + return parse_v2_peer(f, resp->next_data); +#ifdef PCP_SADSCP + case PCP_OPCODE_SADSCP: + return parse_sadscp(f, resp->next_data); +#endif + default: + return PCP_ERR_RECV_FAILED; + } +} + +pcp_errno parse_response(pcp_recv_msg_t *f) { + pcp_response_t *resp = (pcp_response_t *)f->pcp_msg_buffer; + + f->recv_version = resp->ver; + f->recv_result = resp->result_code; + memset(&f->kd, 0, sizeof(f->kd)); + + f->kd.operation = resp->r_opcode & 0x7f; + + PCP_LOG(PCP_LOGLVL_DEBUG, "parse_response: version: %d", f->recv_version); + PCP_LOG(PCP_LOGLVL_DEBUG, "parse_response: result: %d", f->recv_result); + + switch (f->recv_version) { +#ifndef PCP_DISABLE_NATPMP + case 0: + return parse_v0_resp(f, resp); + break; +#endif + case 1: + return parse_v1_resp(f, resp); + break; + case 2: + return parse_v2_resp(f, resp); + break; + } + return PCP_ERR_UNSUP_VERSION; +} diff --git a/lib/libpcp/src/pcp_msg.h b/lib/libpcpnatpmp/src/pcp_msg.h similarity index 97% rename from lib/libpcp/src/pcp_msg.h rename to lib/libpcpnatpmp/src/pcp_msg.h index 73b586912c6..9db2b43d067 100644 --- a/lib/libpcp/src/pcp_msg.h +++ b/lib/libpcpnatpmp/src/pcp_msg.h @@ -29,15 +29,16 @@ #ifdef WIN32 //#include #include "stdint.h" -#else //WIN32 -#include +#else // WIN32 #include +#include #endif -#include "pcp.h" -#include "pcp_utils.h" +#include "pcpnatpmp.h" + #include "pcp_client_db.h" #include "pcp_msg_structs.h" +#include "pcp_utils.h" void *build_pcp_msg(struct pcp_flow_s *flow); diff --git a/lib/libpcp/src/pcp_msg_structs.h b/lib/libpcpnatpmp/src/pcp_msg_structs.h similarity index 81% rename from lib/libpcp/src/pcp_msg_structs.h rename to lib/libpcpnatpmp/src/pcp_msg_structs.h index ff4f5f7b5f5..d7f4295ab4f 100644 --- a/lib/libpcp/src/pcp_msg_structs.h +++ b/lib/libpcpnatpmp/src/pcp_msg_structs.h @@ -26,47 +26,47 @@ #ifndef PCP_MSG_STRUCTS_H_ #define PCP_MSG_STRUCTS_H_ -#ifdef WIN32 -#pragma warning (push) -#pragma warning (disable:4200) -#endif // WIN32 -#define PCP_MAX_LEN 1100 -#define PCP_OPCODE_ANNOUNCE 0 -#define PCP_OPCODE_MAP 1 -#define PCP_OPCODE_PEER 2 -#define PCP_OPCODE_SADSCP 3 -#define NATPMP_OPCODE_ANNOUNCE 0 -#define NATPMP_OPCODE_MAP_UDP 1 -#define NATPMP_OPCODE_MAP_TCP 2 +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable : 4200) +#endif // _MSC_VER +#define PCP_MAX_LEN 1100 +#define PCP_OPCODE_ANNOUNCE 0 +#define PCP_OPCODE_MAP 1 +#define PCP_OPCODE_PEER 2 +#define PCP_OPCODE_SADSCP 3 +#define NATPMP_OPCODE_ANNOUNCE 0 +#define NATPMP_OPCODE_MAP_UDP 1 +#define NATPMP_OPCODE_MAP_TCP 2 /* Possible response codes sent by server, as a result of client request*/ -#define PCP_RES_SUCCESS 0 -#define PCP_RES_UNSUPP_VERSION 1 -#define PCP_RES_NOT_AUTHORIZED 2 -#define PCP_RES_MALFORMED_REQUEST 3 -#define PCP_RES_UNSUPP_OPCODE 4 -#define PCP_RES_UNSUPP_OPTION 5 -#define PCP_RES_MALFORMED_OPTION 6 -#define PCP_RES_NETWORK_FAILURE 7 -#define PCP_RES_NO_RESOURCES 8 -#define PCP_RES_UNSUPP_PROTOCOL 9 -#define PCP_RES_USER_EX_QUOTA 10 -#define PCP_RES_CANNOT_PROVIDE_EXTERNAL 11 -#define PCP_RES_ADDRESS_MISMATCH 12 -#define PCP_RES_EXCESSIVE_REMOTE_PEERS 13 +#define PCP_RES_SUCCESS 0 +#define PCP_RES_UNSUPP_VERSION 1 +#define PCP_RES_NOT_AUTHORIZED 2 +#define PCP_RES_MALFORMED_REQUEST 3 +#define PCP_RES_UNSUPP_OPCODE 4 +#define PCP_RES_UNSUPP_OPTION 5 +#define PCP_RES_MALFORMED_OPTION 6 +#define PCP_RES_NETWORK_FAILURE 7 +#define PCP_RES_NO_RESOURCES 8 +#define PCP_RES_UNSUPP_PROTOCOL 9 +#define PCP_RES_USER_EX_QUOTA 10 +#define PCP_RES_CANNOT_PROVIDE_EXTERNAL 11 +#define PCP_RES_ADDRESS_MISMATCH 12 +#define PCP_RES_EXCESSIVE_REMOTE_PEERS 13 typedef enum pcp_options { - PCP_OPTION_3RD_PARTY=1, - PCP_OPTION_PREF_FAIL=2, - PCP_OPTION_FILTER=3, - PCP_OPTION_DEVICEID=96, /*private range */ - PCP_OPTION_LOCATION=97, - PCP_OPTION_USERID=98, - PCP_OPTION_FLOW_PRIORITY=99, - PCP_OPTION_METADATA=100 + PCP_OPTION_3RD_PARTY = 1, + PCP_OPTION_PREF_FAIL = 2, + PCP_OPTION_FILTER = 3, + PCP_OPTION_DEVICEID = 96, /*private range */ + PCP_OPTION_LOCATION = 97, + PCP_OPTION_USERID = 98, + PCP_OPTION_FLOW_PRIORITY = 99, + PCP_OPTION_METADATA = 100 } pcp_options_t; -#pragma pack(push,1) +#pragma pack(push, 1) #ifndef MAX_USER_ID #define MAX_USER_ID 512 @@ -228,8 +228,8 @@ typedef struct pcp_location_option { uint8_t option; uint8_t reserved; uint16_t len; - //float latitude; - //float longitude; + // float latitude; + // float longitude; char location[MAX_GEO_STR]; } pcp_location_option_t; @@ -248,17 +248,15 @@ typedef struct pcp_deviceid_option { char deviceid[MAX_DEVICE_ID]; } pcp_deviceid_option_t; -#define FOREACH_DEVICE(DEVICE) \ - DEVICE(smartphone) \ - DEVICE(iphone) \ - DEVICE(unknown) +#define FOREACH_DEVICE(DEVICE) \ + DEVICE(smartphone) \ + DEVICE(iphone) \ + DEVICE(unknown) #define GENERATE_ENUM(ENUM) ENUM, -#define GENERATE_STRING(STRING) #STRING , +#define GENERATE_STRING(STRING) #STRING, -typedef enum DEVICE_ENUM { - FOREACH_DEVICE(GENERATE_ENUM) -} device_enum_e; +typedef enum DEVICE_ENUM { FOREACH_DEVICE(GENERATE_ENUM) } device_enum_e; typedef struct pcp_prefer_fail_option { uint8_t option; @@ -292,11 +290,11 @@ typedef struct pcp_flow_priority_option { uint16_t len; uint8_t dscp_up; uint8_t dscp_down; -#define PCP_DSCP_MASK ((1<<6)-1) +#define PCP_DSCP_MASK ((1 << 6) - 1) uint8_t reserved2; /* most significant bit is used for response */ uint8_t response_bit; -//#define PCP_FLOW_OPTION_RESP_P (1<<7) + //#define PCP_FLOW_OPTION_RESP_P (1<<7) uint8_t next_data[0]; } pcp_flow_priority_option_t; @@ -310,7 +308,7 @@ typedef struct pcp_metadata_option { #pragma pack(pop) -#ifdef WIN32 -#pragma warning (pop) -#endif // WIN32 +#ifdef _MSC_VER +#pragma warning(pop) +#endif // _MSC_VER #endif /* PCP_MSG_STRUCTS_H_ */ diff --git a/lib/libpcp/src/pcp_server_discovery.c b/lib/libpcpnatpmp/src/pcp_server_discovery.c similarity index 55% rename from lib/libpcp/src/pcp_server_discovery.c rename to lib/libpcpnatpmp/src/pcp_server_discovery.c index 4dfb6c76671..4edc909fa29 100644 --- a/lib/libpcp/src/pcp_server_discovery.c +++ b/lib/libpcpnatpmp/src/pcp_server_discovery.c @@ -29,34 +29,35 @@ #include "default_config.h" #endif +#include #include #include #include #include -#include #ifdef WIN32 #include "pcp_win_defines.h" #else +#include +#include #include #include -#include -#include -#endif //WIN32 -#include "pcp.h" -#include "pcp_utils.h" -#include "pcp_server_discovery.h" -#include "pcp_event_handler.h" +#endif // WIN32 +#include "findsaddr.h" #include "gateway.h" -#include "pcp_msg.h" +#include "pcpnatpmp.h" + +#include "pcp_event_handler.h" #include "pcp_logger.h" -#include "findsaddr.h" +#include "pcp_msg.h" +#include "pcp_server_discovery.h" #include "pcp_socket.h" +#include "pcp_utils.h" -static pcp_errno psd_fill_pcp_server_src(pcp_server_t *s) -{ +static pcp_errno psd_fill_pcp_server_src(pcp_server_t *s) { struct in6_addr src_ip; - const char *err=NULL; + uint32_t src_scope_id = 0; + const char *err = NULL; PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); @@ -69,30 +70,32 @@ static pcp_errno psd_fill_pcp_server_src(pcp_server_t *s) memset(&src_ip, 0, sizeof(src_ip)); #ifndef PCP_USE_IPV6_SOCKET - s->pcp_server_saddr.ss_family=AF_INET; + s->pcp_server_saddr.ss_family = AF_INET; if (s->af == AF_INET) { - ((struct sockaddr_in *)&s->pcp_server_saddr)->sin_addr.s_addr= - s->pcp_ip[3]; - ((struct sockaddr_in *)&s->pcp_server_saddr)->sin_port=s->pcp_port; + ((struct sockaddr_in *)&s->pcp_server_saddr)->sin_addr.s_addr = + s->pcp_ip[3]; + ((struct sockaddr_in *)&s->pcp_server_saddr)->sin_port = s->pcp_port; SET_SA_LEN(&s->pcp_server_saddr, sizeof(struct sockaddr_in)); - inet_ntop(AF_INET, - (void *)&((struct sockaddr_in *)&s->pcp_server_saddr)->sin_addr, - s->pcp_server_paddr, sizeof(s->pcp_server_paddr)); + inet_ntop( + AF_INET, + (void *)&((struct sockaddr_in *)&s->pcp_server_saddr)->sin_addr, + s->pcp_server_paddr, sizeof(s->pcp_server_paddr)); - err=findsaddr((struct sockaddr_in *)&s->pcp_server_saddr, &src_ip); + err = findsaddr((struct sockaddr_in *)&s->pcp_server_saddr, &src_ip); if (err) { PCP_LOG(PCP_LOGLVL_WARN, "Error (%s) occurred while registering a new " - "PCP server %s", err, s->pcp_server_paddr); + "PCP server %s", + err, s->pcp_server_paddr); PCP_LOG_END(PCP_LOGLVL_DEBUG); return PCP_ERR_UNKNOWN; } - s->src_ip[0]=0; + s->src_ip[0] = 0; - s->src_ip[1]=0; - s->src_ip[2]=htonl(0xFFFF); - s->src_ip[3]=S6_ADDR32(&src_ip)[3]; + s->src_ip[1] = 0; + s->src_ip[2] = htonl(0xFFFF); + s->src_ip[3] = S6_ADDR32(&src_ip)[3]; } else { PCP_LOG(PCP_LOGLVL_WARN, "%s", "IPv6 is disabled and IPv6 address of PCP server occurred"); @@ -100,73 +103,82 @@ static pcp_errno psd_fill_pcp_server_src(pcp_server_t *s) PCP_LOG_END(PCP_LOGLVL_DEBUG); return PCP_ERR_BAD_AFINET; } -#else //PCP_USE_IPV6_SOCKET - s->pcp_server_saddr.ss_family=AF_INET6; +#else // PCP_USE_IPV6_SOCKET + s->pcp_server_saddr.ss_family = AF_INET6; if (s->af == AF_INET) { - return PCP_ERR_BAD_AFINET; //should never happen + return PCP_ERR_BAD_AFINET; // should never happen } pcp_fill_sockaddr((struct sockaddr *)&s->pcp_server_saddr, - (struct in6_addr *)&s->pcp_ip, s->pcp_port, 1, s->pcp_scope_id); + (struct in6_addr *)&s->pcp_ip, s->pcp_port, 1, + s->pcp_scope_id); inet_ntop(AF_INET6, - (void *)&((struct sockaddr_in6*) &s->pcp_server_saddr)->sin6_addr, - s->pcp_server_paddr, sizeof(s->pcp_server_paddr)); + (void *)&((struct sockaddr_in6 *)&s->pcp_server_saddr)->sin6_addr, + s->pcp_server_paddr, sizeof(s->pcp_server_paddr)); - err=findsaddr6((struct sockaddr_in6*)&s->pcp_server_saddr, &src_ip); + err = findsaddr6((struct sockaddr_in6 *)&s->pcp_server_saddr, &src_ip, + &src_scope_id); if (err) { PCP_LOG(PCP_LOGLVL_WARN, "Error (%s) occurred while registering a new " - "PCP server %s", err, s->pcp_server_paddr); + "PCP server %s", + err, s->pcp_server_paddr); PCP_LOG_END(PCP_LOGLVL_DEBUG); return PCP_ERR_UNKNOWN; } - s->src_ip[0]=S6_ADDR32(&src_ip)[0]; - s->src_ip[1]=S6_ADDR32(&src_ip)[1]; - s->src_ip[2]=S6_ADDR32(&src_ip)[2]; - s->src_ip[3]=S6_ADDR32(&src_ip)[3]; -#endif //PCP_USE_IPV6_SOCKET - s->server_state=pss_ping; - s->next_timeout.tv_sec=0; - s->next_timeout.tv_usec=0; + s->src_ip[0] = S6_ADDR32(&src_ip)[0]; + s->src_ip[1] = S6_ADDR32(&src_ip)[1]; + s->src_ip[2] = S6_ADDR32(&src_ip)[2]; + s->src_ip[3] = S6_ADDR32(&src_ip)[3]; + + if ((s->pcp_scope_id == 0) && (IN6_IS_ADDR_LINKLOCAL(&src_ip))) { + s->pcp_scope_id = src_scope_id; + } +#endif // PCP_USE_IPV6_SOCKET + s->server_state = pss_ping; + s->next_timeout.tv_sec = 0; + s->next_timeout.tv_usec = 0; PCP_LOG_END(PCP_LOGLVL_DEBUG); return PCP_ERR_SUCCESS; } -void psd_add_gws(pcp_ctx_t *ctx) -{ - struct sockaddr_in6 *gws=NULL, *gw; - int rcount=getgateways(&gws); +void psd_add_gws(pcp_ctx_t *ctx) { + struct sockaddr_in6 *gws = NULL, *gw; + int rcount = getgateways(&gws); - gw=gws; + gw = gws; for (; rcount > 0; rcount--, gw++) { int pcps_indx; - if ((IN6_IS_ADDR_V4MAPPED(&gw->sin6_addr)) && (S6_ADDR32(&gw->sin6_addr)[3] == INADDR_ANY)) + if ((IN6_IS_ADDR_V4MAPPED(&gw->sin6_addr)) && + (S6_ADDR32(&gw->sin6_addr)[3] == INADDR_ANY)) continue; - if (IN6_IS_ADDR_UNSPECIFIED(&gw->sin6_addr)) + if (IPV6_IS_ADDR_ANY(&gw->sin6_addr)) continue; - if (get_pcp_server_by_ip(ctx, &gw->sin6_addr)) + if (get_pcp_server_by_ip(ctx, &gw->sin6_addr, gw->sin6_scope_id)) continue; - pcps_indx=pcp_new_server(ctx, &gw->sin6_addr, ntohs(PCP_SERVER_PORT), gw->sin6_scope_id); + pcps_indx = pcp_new_server(ctx, &gw->sin6_addr, ntohs(PCP_SERVER_PORT), + gw->sin6_scope_id); if (pcps_indx >= 0) { - pcp_server_t *s=get_pcp_server(ctx, pcps_indx); + pcp_server_t *s = get_pcp_server(ctx, pcps_indx); if (!s) continue; if (psd_fill_pcp_server_src(s)) { PCP_LOG(PCP_LOGLVL_ERR, "Failed to initialize gateway %s as a PCP server.", - s?s->pcp_server_paddr:"NULL pointer!!!"); + s ? s->pcp_server_paddr : "NULL pointer!!!"); } else { - PCP_LOG(PCP_LOGLVL_INFO, "Found gateway %s. " - "Added as possible PCP server.", - s?s->pcp_server_paddr:"NULL pointer!!!"); + PCP_LOG(PCP_LOGLVL_INFO, + "Found gateway %s. " + "Added as possible PCP server.", + s ? s->pcp_server_paddr : "NULL pointer!!!"); } } } @@ -174,37 +186,36 @@ void psd_add_gws(pcp_ctx_t *ctx) } pcp_errno psd_add_pcp_server(pcp_ctx_t *ctx, struct sockaddr *sa, - uint8_t version) -{ - struct in6_addr pcp_ip=IN6ADDR_ANY_INIT; + uint8_t version) { + struct in6_addr pcp_ip = IN6ADDR_ANY_INIT; uint16_t pcp_port; - uint32_t scope_id=0; - pcp_server_t *pcps=NULL; + uint32_t scope_id = 0; + pcp_server_t *pcps = NULL; PCP_LOG_BEGIN(PCP_LOGLVL_DEBUG); if (sa->sa_family == AF_INET) { - S6_ADDR32(&pcp_ip)[0]=0; - S6_ADDR32(&pcp_ip)[1]=0; - S6_ADDR32(&pcp_ip)[2]=htonl(0xFFFF); - S6_ADDR32(&pcp_ip)[3]=((struct sockaddr_in *)sa)->sin_addr.s_addr; - pcp_port=((struct sockaddr_in *)sa)->sin_port; + S6_ADDR32(&pcp_ip)[0] = 0; + S6_ADDR32(&pcp_ip)[1] = 0; + S6_ADDR32(&pcp_ip)[2] = htonl(0xFFFF); + S6_ADDR32(&pcp_ip)[3] = ((struct sockaddr_in *)sa)->sin_addr.s_addr; + pcp_port = ((struct sockaddr_in *)sa)->sin_port; } else { - IPV6_ADDR_COPY(&pcp_ip, &((struct sockaddr_in6*)sa)->sin6_addr); - pcp_port=((struct sockaddr_in6 *)sa)->sin6_port; - scope_id=((struct sockaddr_in6 *)sa)->sin6_scope_id; + IPV6_ADDR_COPY(&pcp_ip, &((struct sockaddr_in6 *)sa)->sin6_addr); + pcp_port = ((struct sockaddr_in6 *)sa)->sin6_port; + scope_id = ((struct sockaddr_in6 *)sa)->sin6_scope_id; } if (!pcp_port) { - pcp_port=ntohs(PCP_SERVER_PORT); + pcp_port = ntohs(PCP_SERVER_PORT); } - pcps=get_pcp_server_by_ip(ctx, (struct in6_addr *)&pcp_ip); + pcps = get_pcp_server_by_ip(ctx, (struct in6_addr *)&pcp_ip, scope_id); if (!pcps) { - int pcps_indx=pcp_new_server(ctx, &pcp_ip, pcp_port, scope_id); + int pcps_indx = pcp_new_server(ctx, &pcp_ip, pcp_port, scope_id); if (pcps_indx >= 0) { - pcps=get_pcp_server(ctx, pcps_indx); + pcps = get_pcp_server(ctx, pcps_indx); } if (pcps == NULL) { @@ -213,14 +224,14 @@ pcp_errno psd_add_pcp_server(pcp_ctx_t *ctx, struct sockaddr *sa, return PCP_ERR_UNKNOWN; } } else { - pcps->pcp_port=pcp_port; + pcps->pcp_port = pcp_port; } - pcps->pcp_version=version; - pcps->server_state=pss_allocated; + pcps->pcp_version = version; + pcps->server_state = pss_allocated; if (psd_fill_pcp_server_src(pcps)) { - pcps->server_state=pss_unitialized; + pcps->server_state = pss_unitialized; PCP_LOG(PCP_LOGLVL_INFO, "Failed to add PCP server %s", pcps->pcp_server_paddr); diff --git a/lib/libpcp/src/pcp_server_discovery.h b/lib/libpcpnatpmp/src/pcp_server_discovery.h similarity index 97% rename from lib/libpcp/src/pcp_server_discovery.h rename to lib/libpcpnatpmp/src/pcp_server_discovery.h index 36000950760..05a97da8cfe 100644 --- a/lib/libpcp/src/pcp_server_discovery.h +++ b/lib/libpcpnatpmp/src/pcp_server_discovery.h @@ -30,6 +30,6 @@ void psd_add_gws(pcp_ctx_t *ctx); pcp_errno psd_add_pcp_server(pcp_ctx_t *ctx, struct sockaddr *sa, - uint8_t version); + uint8_t version); #endif /* PCP_SERVER_DISCOVERY_H_ */ diff --git a/lib/libpcpnatpmp/src/pcp_utils.h b/lib/libpcpnatpmp/src/pcp_utils.h new file mode 100644 index 00000000000..2ed709b1967 --- /dev/null +++ b/lib/libpcpnatpmp/src/pcp_utils.h @@ -0,0 +1,252 @@ +/* + Copyright (c) 2014 by Cisco Systems, Inc. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef PCP_UTILS_H_ +#define PCP_UTILS_H_ + +#include "pcp_client_db.h" +#include "pcp_logger.h" +#include +#include +#include + +#ifndef max +#define max(a, b) \ + ({ \ + typeof(a) _a = (a); \ + typeof(b) _b = (b); \ + _a > _b ? _a : _b; \ + }) +#endif + +#ifndef min +#define min(a, b) \ + ({ \ + typeof(a) _a = (a); \ + typeof(b) _b = (b); \ + _a > _b ? _b : _a; \ + }) +#endif + +#ifdef __GNUC__ +#define UNUSED __attribute__((unused)) +#else +#define UNUSED +#endif + +#ifdef _MSC_VER +/* variable num of arguments*/ +#define DUPPRINT(fp, fmt, ...) \ + do { \ + printf(fmt, __VA_ARGS__); \ + if (fp != NULL) { \ + fprintf(fp, fmt, __VA_ARGS__); \ + } \ + } while (0) +#else /*WIN32*/ +#define DUPPRINT(fp, fmt...) \ + do { \ + printf(fmt); \ + if (fp != NULL) { \ + fprintf(fp, fmt); \ + } \ + } while (0) +#endif /*WIN32*/ + +#define log_err(STR) \ + do { \ + printf("%s:%d " #STR ": %s \n", __FUNCTION__, __LINE__, \ + strerror(errno)); \ + } while (0) + +#define log_debug_scr(STR) \ + do { \ + printf("%s:%d %s \n", __FUNCTION__, __LINE__, STR); \ + } while (0) + +#define log_debug(STR) \ + do { \ + printf("%s:%d " #STR " \n", __FUNCTION__, __LINE__); \ + } while (0) + +#define CHECK_RET_EXIT(func) \ + do { \ + if (func < 0) { \ + log_err(""); \ + exit(EXIT_FAILURE); \ + } \ + } while (0) + +#define CHECK_NULL_EXIT(func) \ + do { \ + if (func == NULL) { \ + log_err(""); \ + exit(EXIT_FAILURE); \ + } \ + } while (0) + +#define CHECK_RET(func) \ + do { \ + if (func < 0) { \ + log_err(""); \ + } \ + } while (0) + +#define CHECK_RET_GOTO_ERROR(func) \ + do { \ + if (func < 0) { \ + log_err(""); \ + goto ERROR; \ + } \ + } while (0) + +#define OSDEP(x) (void)(x) + +#ifdef s6_addr32 +#define S6_ADDR32(sa6) (sa6)->s6_addr32 +#else +#define S6_ADDR32(sa6) ((uint32_t *)((sa6)->s6_addr)) +#endif + +#define IPV6_IS_ADDR_ANY(a) \ + (IN6_IS_ADDR_UNSPECIFIED(a) || \ + (IN6_IS_ADDR_V4MAPPED(a) && (a)->s6_addr[12] == 0 && \ + (a)->s6_addr[13] == 0 && (a)->s6_addr[14] == 0 && \ + (a)->s6_addr[15] == 0)) + +#define IPV6_ADDR_COPY(dest, src) \ + do { \ + (S6_ADDR32(dest))[0] = (S6_ADDR32(src))[0]; \ + (S6_ADDR32(dest))[1] = (S6_ADDR32(src))[1]; \ + (S6_ADDR32(dest))[2] = (S6_ADDR32(src))[2]; \ + (S6_ADDR32(dest))[3] = (S6_ADDR32(src))[3]; \ + } while (0) + +#include "pcp_msg.h" +static inline int compare_epochs(pcp_recv_msg_t *f, pcp_server_t *s) { + uint32_t c_delta; + uint32_t s_delta; + + if (s->epoch == ~0u) { + s->epoch = f->recv_epoch; + s->cepoch = f->received_time; + } + c_delta = (uint32_t)(f->received_time - s->cepoch); + s_delta = f->recv_epoch - s->epoch; + + PCP_LOG(PCP_LOGLVL_DEBUG, "Epoch - client delta = %u, server delta = %u", + c_delta, s_delta); + + return (c_delta + 2 < s_delta - (s_delta >> 4)) || + (s_delta + 2 < c_delta - (c_delta >> 4)); +} + +inline static void timeval_align(struct timeval *x) { + x->tv_sec += x->tv_usec / 1000000; + x->tv_usec = x->tv_usec % 1000000; + if (x->tv_usec < 0) { + x->tv_usec = 1000000 + x->tv_usec; + x->tv_sec -= 1; + } +} + +inline static int timeval_comp(struct timeval *x, struct timeval *y) { + timeval_align(x); + timeval_align(y); + if (x->tv_sec < y->tv_sec) { + return -1; + } else if (x->tv_sec > y->tv_sec) { + return 1; + } else if (x->tv_usec < y->tv_usec) { + return -1; + } else if (x->tv_usec > y->tv_usec) { + return 1; + } else { + return 0; + } +} + +inline static int timeval_subtract(struct timeval *result, struct timeval *x, + struct timeval *y) { + int ret = timeval_comp(x, y); + + if (ret <= 0) { + result->tv_sec = 0; + result->tv_usec = 0; + return 1; + } + + // in case that tv_usec is unsigned -> perform the carry + if (x->tv_usec < y->tv_usec) { + int nsec = (y->tv_usec - x->tv_usec) / 1000000 + 1; + y->tv_usec -= 1000000 * nsec; + y->tv_sec += nsec; + } + + /* Compute the time remaining to wait. + tv_usec is certainly positive. */ + result->tv_sec = x->tv_sec - y->tv_sec; + result->tv_usec = x->tv_usec - y->tv_usec; + timeval_align(result); + + /* Return 1 if result is negative. */ + return ret <= 0; +} + +/* Nonce is part of the MAP and PEER requests/responses + as of version 2 of the PCP protocol */ +static inline void createNonce(struct pcp_nonce *nonce_field) { + int i; + for (i = 2; i >= 0; --i) +#ifdef WIN32 + nonce_field->n[i] = htonl(rand()); +#else // WIN32 + nonce_field->n[i] = htonl(random()); +#endif // WIN32 +} + +#ifndef HAVE_STRNDUP +static inline char *pcp_strndup(const char *s, size_t size) { + char *ret; + char *end = memchr(s, 0, size); + + if (end) { + /* Length + 1 */ + size = end - s + 1; + } else { + size++; + } + ret = malloc(size); + + if (ret) { + memcpy(ret, s, size); + ret[size - 1] = '\0'; + } + return ret; +} +#define strndup pcp_strndup +#endif + +#endif /* PCP_UTILS_H_ */ diff --git a/lib/libpcp/src/windows/pcp_gettimeofday.c b/lib/libpcpnatpmp/src/windows/pcp_gettimeofday.c similarity index 74% rename from lib/libpcp/src/windows/pcp_gettimeofday.c rename to lib/libpcpnatpmp/src/windows/pcp_gettimeofday.c index 01c6d894161..229905b536e 100644 --- a/lib/libpcp/src/windows/pcp_gettimeofday.c +++ b/lib/libpcpnatpmp/src/windows/pcp_gettimeofday.c @@ -30,12 +30,12 @@ #endif #ifndef HAVE_GETTIMEOFDAY -#include #include +#include #if defined(_MSC_VER) || defined(_MSC_EXTENSIONS) -#define DELTA_EPOCH_IN_MICROSECS 11644473600000000Ui64 +#define DELTA_EPOCH_IN_MICROSECS 11644473600000000Ui64 #else /* defined(_MSC_VER) || defined(_MSC_EXTENSIONS)*/ -#define DELTA_EPOCH_IN_MICROSECS 11644473600000000ULL +#define DELTA_EPOCH_IN_MICROSECS 11644473600000000ULL #endif /* defined(_MSC_VER) || defined(_MSC_EXTENSIONS)*/ /* custom implementation of the gettimeofday function @@ -43,29 +43,28 @@ struct timezone { int tz_minuteswest; /* minutes W of Greenwich */ - int tz_dsttime; /* type of dst correction */ + int tz_dsttime; /* type of dst correction */ }; -int gettimeofday(struct timeval *tv, struct timezone *tz) -{ +int gettimeofday(struct timeval *tv, struct timezone *tz) { FILETIME ft; - unsigned __int64 tmpres=0; - static int tzflag=0; - int tz_seconds=0; - int tz_daylight=0; + unsigned __int64 tmpres = 0; + static int tzflag = 0; + int tz_seconds = 0; + int tz_daylight = 0; if (NULL != tv) { GetSystemTimeAsFileTime(&ft); - tmpres|=ft.dwHighDateTime; - tmpres<<=32; - tmpres|=ft.dwLowDateTime; + tmpres |= ft.dwHighDateTime; + tmpres <<= 32; + tmpres |= ft.dwLowDateTime; - tmpres/=10; /*convert into microseconds*/ + tmpres /= 10; /*convert into microseconds*/ /*converting file time to unix epoch*/ - tmpres-=DELTA_EPOCH_IN_MICROSECS; - tv->tv_sec=(long)(tmpres / 1000000UL); - tv->tv_usec=(long)(tmpres % 1000000UL); + tmpres -= DELTA_EPOCH_IN_MICROSECS; + tv->tv_sec = (long)(tmpres / 1000000UL); + tv->tv_usec = (long)(tmpres % 1000000UL); } if (tz) { @@ -79,11 +78,11 @@ int gettimeofday(struct timeval *tv, struct timezone *tz) if (_get_daylight(&tz_daylight)) { return -1; } - tz->tz_minuteswest=tz_seconds / 60; - tz->tz_dsttime=tz_daylight; + tz->tz_minuteswest = tz_seconds / 60; + tz->tz_dsttime = tz_daylight; } return 0; } -#endif //HAVE_GETTIMEOFDAY +#endif // HAVE_GETTIMEOFDAY diff --git a/lib/libpcp/src/windows/pcp_gettimeofday.h b/lib/libpcpnatpmp/src/windows/pcp_gettimeofday.h similarity index 100% rename from lib/libpcp/src/windows/pcp_gettimeofday.h rename to lib/libpcpnatpmp/src/windows/pcp_gettimeofday.h diff --git a/lib/libpcp/src/windows/pcp_win_defines.h b/lib/libpcpnatpmp/src/windows/pcp_win_defines.h similarity index 80% rename from lib/libpcp/src/windows/pcp_win_defines.h rename to lib/libpcpnatpmp/src/windows/pcp_win_defines.h index 2a6df06259a..3ea66b923b4 100644 --- a/lib/libpcp/src/windows/pcp_win_defines.h +++ b/lib/libpcpnatpmp/src/windows/pcp_win_defines.h @@ -27,42 +27,48 @@ #define PCP_WIN_DEFINES #include + #include + #include + #include + #include /*GetCurrentProcessId*/ /*link with kernel32.dll*/ -#include "stdint.h" + +#include /* windows uses Sleep(miliseconds) method, instead of UNIX sleep(seconds) */ -#define sleep(x) Sleep((x) * 1000) +#define sleep(x) Sleep((x)*1000) + #ifdef _MSC_VER -#define inline __inline /*In Visual Studio inline keyword only available in C++ */ +#define inline \ + __inline /*In Visual Studio inline keyword only available in C++ */ #endif typedef uint16_t in_port_t; -#if 1 //WINVERsin_addr), src, sizeof(sa4->sin_addr)); - slen=sizeof(struct sockaddr_in); + slen = sizeof(struct sockaddr_in); } else if (af == AF_INET6) { - struct sockaddr_in6 *sa6=(struct sockaddr_in6 *)&srcaddr; + struct sockaddr_in6 *sa6 = (struct sockaddr_in6 *)&srcaddr; memset(sa6, 0, sizeof(struct sockaddr_in6)); memcpy(&(sa6->sin6_addr), src, sizeof(sa6->sin6_addr)); - slen=sizeof(struct sockaddr_in6); + slen = sizeof(struct sockaddr_in6); } else { return NULL; } - srcaddr.ss_family=af; + srcaddr.ss_family = af; if (WSAAddressToString((struct sockaddr *)&srcaddr, (DWORD)slen, 0, dst, - (LPDWORD) & cnt) != 0) { + (LPDWORD)&cnt) != 0) { return NULL; } return dst; @@ -75,10 +81,8 @@ static inline const char *pcp_inet_ntop(int af, const void *src, char *dst, #define getpid GetCurrentProcessId -#define snprintf _snprintf - int gettimeofday(struct timeval *tv, struct timezone *tz); -#define MSG_DONTWAIT 0x0 +#define MSG_DONTWAIT 0x0 #endif /*PCP_WIN_DEFINES*/ From 9e618f6632ecf5e0916bfbe5da7d1546f55d276c Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Fri, 22 Aug 2025 04:19:15 -0700 Subject: [PATCH 395/466] Particle effect hooks for subsystem destruction (#6937) * add particle hook * partial bugfixing * bugfixing * appeasement * syntax fix --- code/model/model.h | 1 + code/particle/ParticleSource.cpp | 1 + code/ship/ship.cpp | 17 +++++++++++++++ code/ship/ship.h | 1 + code/ship/shipfx.cpp | 25 +++++++++++++--------- code/ship/shipfx.h | 2 +- code/ship/shiphit.cpp | 36 ++++++++++++++++++++++++++++---- 7 files changed, 68 insertions(+), 15 deletions(-) diff --git a/code/model/model.h b/code/model/model.h index 0d528036fdd..d0471f08c22 100644 --- a/code/model/model.h +++ b/code/model/model.h @@ -294,6 +294,7 @@ class model_subsystem { /* contains rotation rate info */ float density; + particle::ParticleEffectHandle death_effect; particle::ParticleEffectHandle debris_flame_particles; particle::ParticleEffectHandle shrapnel_flame_particles; diff --git a/code/particle/ParticleSource.cpp b/code/particle/ParticleSource.cpp index a7d337337a3..1023b315440 100644 --- a/code/particle/ParticleSource.cpp +++ b/code/particle/ParticleSource.cpp @@ -85,6 +85,7 @@ bool ParticleSource::process() { } void ParticleSource::setNormal(const vec3d& normal) { + Assertion(vm_vec_is_normalized(&normal), "Particle source normal must be normalized!"); m_normal = normal; } diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 979da1d538a..bbf73bd3769 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -1352,6 +1352,8 @@ void ship_info::clone(const ship_info& other) ship_passive_arcs = other.ship_passive_arcs; glowpoint_bank_override_map = other.glowpoint_bank_override_map; + + default_subsys_death_effect = other.default_subsys_death_effect; } void ship_info::move(ship_info&& other) @@ -1698,6 +1700,8 @@ void ship_info::move(ship_info&& other) animations = std::move(other.animations); cockpit_animations = std::move(other.cockpit_animations); + + default_subsys_death_effect = other.default_subsys_death_effect; } ship_info &ship_info::operator= (ship_info&& other) noexcept @@ -2091,6 +2095,8 @@ ship_info::ship_info() glowpoint_bank_override_map.clear(); ship_passive_arcs.clear(); + + default_subsys_death_effect = particle::ParticleEffectHandle::invalid(); } ship_info::~ship_info() @@ -5488,6 +5494,10 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool required_string("$end_custom_strings"); } + if (optional_string("$Default Subsystem Death Effect:")) { + sip->default_subsys_death_effect = particle::util::parseEffect(sip->name); + } + if(optional_string("$Default Subsystem Debris Flame Effect:")) { sip->default_subsys_debris_flame_particles = particle::util::parseEffect(sip->name); @@ -5595,8 +5605,11 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool sp->turret_max_bomb_ownage = -1; sp->turret_max_target_ownage = -1; sp->density = 1.0f; + + sp->death_effect = particle::ParticleEffectHandle::invalid(); sp->debris_flame_particles = particle::ParticleEffectHandle::invalid(); sp->shrapnel_flame_particles = particle::ParticleEffectHandle::invalid(); + } sfo_return = stuff_float_optional(&percentage_of_hits); if(sfo_return==2) @@ -5783,6 +5796,10 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool } } + if (optional_string("$Subsystem Death Effect:")) { + sp->death_effect = particle::util::parseEffect(sip->name); + } + if (optional_string("$Debris Density:")) { stuff_float(&sp->density); } diff --git a/code/ship/ship.h b/code/ship/ship.h index d3bfa0b62e3..aae7f67e487 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -1262,6 +1262,7 @@ class ship_info // subsystem information int n_subsystems; // this number comes from ships.tbl model_subsystem *subsystems; // see model.h for structure definition + particle::ParticleEffectHandle default_subsys_death_effect; // Energy Transfer System fields float power_output; // power output of ships reactor (EU/s) diff --git a/code/ship/shipfx.cpp b/code/ship/shipfx.cpp index 33359200f73..caa91a11bd3 100644 --- a/code/ship/shipfx.cpp +++ b/code/ship/shipfx.cpp @@ -90,7 +90,7 @@ void model_get_rotating_submodel_axis(vec3d *model_axis, vec3d *world_axis, cons * * DKA: 5/26/99 make velocity of debris scale according to size of debris subobject (at least for large subobjects) */ -static void shipfx_subsystem_maybe_create_live_debris(object *ship_objp, const ship *ship_p, const ship_subsys *subsys, const vec3d *exp_center, float exp_mag) +static void shipfx_subsystem_maybe_create_live_debris(object *ship_objp, const ship *ship_p, const ship_subsys *subsys, const vec3d *exp_center, float exp_mag, bool no_fireballs = false) { // initializations ship *shipp = &Ships[ship_objp->instance]; @@ -146,8 +146,11 @@ static void shipfx_subsystem_maybe_create_live_debris(object *ship_objp, const s if(fireball_type < 0) { fireball_type = FIREBALL_EXPLOSION_MEDIUM; } - // create fireball here. - fireball_create(&end_world_pos, fireball_type, FIREBALL_MEDIUM_EXPLOSION, OBJ_INDEX(ship_objp), pm->submodel[live_debris_submodel].rad); + + if (!no_fireballs) { + // create fireball here. + fireball_create(&end_world_pos, fireball_type, FIREBALL_MEDIUM_EXPLOSION, OBJ_INDEX(ship_objp), pm->submodel[live_debris_submodel].rad); + } // create debris live_debris_obj = debris_create(ship_objp, pm->id, live_debris_submodel, &end_world_pos, exp_center, 1, exp_mag, subsys); @@ -269,7 +272,7 @@ static void shipfx_maybe_create_live_debris_at_ship_death( object *ship_objp ) } } -void shipfx_blow_off_subsystem(object *ship_objp, ship *ship_p, const ship_subsys *subsys, const vec3d *exp_center, bool no_explosion) +void shipfx_blow_off_subsystem(object *ship_objp, ship *ship_p, const ship_subsys *subsys, const vec3d *exp_center, bool no_explosion, bool no_fireballs) { vec3d subobj_pos; @@ -286,14 +289,16 @@ void shipfx_blow_off_subsystem(object *ship_objp, ship *ship_p, const ship_subsy // create live debris objects, if any // TODO: some MULTIPLAYER implcations here!! - shipfx_subsystem_maybe_create_live_debris(ship_objp, ship_p, subsys, exp_center, 1.0f); + shipfx_subsystem_maybe_create_live_debris(ship_objp, ship_p, subsys, exp_center, 1.0f, no_fireballs); - int fireball_type = fireball_ship_explosion_type(&Ship_info[ship_p->ship_info_index]); - if(fireball_type < 0) { - fireball_type = FIREBALL_EXPLOSION_MEDIUM; + if (!no_fireballs) { + int fireball_type = fireball_ship_explosion_type(&Ship_info[ship_p->ship_info_index]); + if(fireball_type < 0) { + fireball_type = FIREBALL_EXPLOSION_MEDIUM; + } + // create first fireball + fireball_create( &subobj_pos, fireball_type, FIREBALL_MEDIUM_EXPLOSION, OBJ_INDEX(ship_objp), psub->radius ); } - // create first fireball - fireball_create( &subobj_pos, fireball_type, FIREBALL_MEDIUM_EXPLOSION, OBJ_INDEX(ship_objp), psub->radius ); } } diff --git a/code/ship/shipfx.h b/code/ship/shipfx.h index 905afde0260..06db056443b 100644 --- a/code/ship/shipfx.h +++ b/code/ship/shipfx.h @@ -32,7 +32,7 @@ struct matrix; void shipfx_emit_spark( int n, int sn ); // Does the special effects to blow a subsystem off a ship -extern void shipfx_blow_off_subsystem(object *ship_obj, ship *ship_p, const ship_subsys *subsys, const vec3d *exp_center, bool no_explosion = false); +extern void shipfx_blow_off_subsystem(object *ship_obj, ship *ship_p, const ship_subsys *subsys, const vec3d *exp_center, bool no_explosion = false, bool no_fireballs = false); // Creates "ndebris" pieces of debris on random verts of the "submodel" in the // ship's model. diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 505912c943b..71f7a2d4598 100755 --- a/code/ship/shiphit.cpp +++ b/code/ship/shiphit.cpp @@ -157,7 +157,34 @@ void do_subobj_destroyed_stuff( ship *ship_p, ship_subsys *subsys, const vec3d* // create fireballs when subsys destroy for large ships. if (!(subsys->flags[Ship::Subsystem_Flags::Vanished, Ship::Subsystem_Flags::No_disappear]) && !no_explosion) { - if (ship_objp->radius > 100.0f) { + vec3d center_to_subsys; + vm_vec_sub(¢er_to_subsys, &g_subobj_pos, &ship_objp->pos); + + particle::ParticleEffectHandle death_effect; + + if (psub->death_effect.isValid()) { + death_effect = psub->death_effect; + } else { + death_effect = sip->default_subsys_death_effect; + } + + if (death_effect.isValid()) { + vec3d subsys_local_pos; + if (psub->subobj_num >= 0) { + // the vmd_zero_vector here should probably be psub->pnt instead, but this matches the behavior of get_subsystem_world_pos + model_instance_local_to_global_point(&subsys_local_pos, &vmd_zero_vector, ship_p->model_instance_num, psub->subobj_num); + } else { + subsys_local_pos = psub->pnt; + } + vec3d normalized_center_to_subsys = center_to_subsys; + vm_vec_normalize(&normalized_center_to_subsys); + // spawn particle effect + auto source = particle::ParticleManager::get()->createSource(death_effect); + source->setHost(make_unique(ship_objp, subsys_local_pos, vmd_identity_matrix)); + source->setTriggerRadius(psub->radius); + source->setNormal(normalized_center_to_subsys); + source->finishCreation(); + } else if (ship_objp->radius > 100.0f) { // number of fireballs determined by radius of subsys int num_fireballs; if ( psub->radius < 3 ) { @@ -166,8 +193,7 @@ void do_subobj_destroyed_stuff( ship *ship_p, ship_subsys *subsys, const vec3d* num_fireballs = 5; } - vec3d temp_vec, center_to_subsys, rand_vec; - vm_vec_sub(¢er_to_subsys, &g_subobj_pos, &ship_objp->pos); + vec3d temp_vec, rand_vec; for (i=0; ideath_effect.isValid() || sip->default_subsys_death_effect.isValid(); + if (!(subsys->flags[Ship::Subsystem_Flags::No_disappear])) { if (psub->subobj_num > -1) { - shipfx_blow_off_subsystem(ship_objp, ship_p, subsys, &g_subobj_pos, no_explosion); + shipfx_blow_off_subsystem(ship_objp, ship_p, subsys, &g_subobj_pos, no_explosion, no_fireballs); subsys->submodel_instance_1->blown_off = true; } From 84897636cdc0f196e567bf43244a62eb590968a3 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 22 Aug 2025 09:14:07 -0400 Subject: [PATCH 396/466] Hopefully last of clang warnings --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 4 ++-- qtfred/src/mission/dialogs/VariableDialogModel.h | 4 ++-- qtfred/src/ui/dialogs/VariableDialog.h | 8 ++------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 8794dbf0fd1..3cf1171a9a1 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -325,7 +325,7 @@ void VariableDialogModel::initializeData() _containerItems.clear(); for (int i = 0; i < MAX_SEXP_VARIABLES; ++i){ - if (!(Sexp_variables[i].type & SEXP_VARIABLE_NOT_USED)) { + if (!(Sexp_variables[i].type & SEXP_VARIABLE_NOT_USED)) { // NOLINT(modernize-loop-convert) _variableItems.emplace_back(); auto& item = _variableItems.back(); item.name = Sexp_variables[i].variable_name; @@ -2184,7 +2184,7 @@ SCP_vector> VariableDialogModel::getContainerNames() } if (item.list){ - type += listPrefix + type + listPostscript; + type.append(listPrefix + type + listPostscript); } else { diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h index 0839fe2e779..4bf7ccb8527 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.h +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -74,7 +74,7 @@ class VariableDialogModel : public AbstractDialogModel { // returns whether it succeeded bool removeVariable(int index, bool toDelete); bool safeToAlterVariable(int index); - bool safeToAlterVariable(const variableInfo& variableItem); + static bool safeToAlterVariable(const variableInfo& variableItem); // Container Section @@ -148,7 +148,7 @@ class VariableDialogModel : public AbstractDialogModel { int _deleteWarningCount; - sexp_container createContainer(const containerInfo& infoIn); + static sexp_container createContainer(const containerInfo& infoIn); void sortMap(int index); bool atMaxVariables(); diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h index 16667b91aba..837efc4b65c 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.h +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -5,9 +5,7 @@ #include #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class VariableEditorDialog; @@ -101,6 +99,4 @@ class VariableDialog : public QDialog { -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace From fa80fc3278e7f131e6dae4eebb855009a61eed56 Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 22 Aug 2025 09:36:19 -0400 Subject: [PATCH 397/466] APPEND THIS --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index 3cf1171a9a1..d6781d564c5 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -2184,7 +2184,7 @@ SCP_vector> VariableDialogModel::getContainerNames() } if (item.list){ - type.append(listPrefix + type + listPostscript); + type.append(listPrefix.append(type.append(listPostscript))); } else { From 89219df6af14c966bab0bb73c421574fe0b64c2b Mon Sep 17 00:00:00 2001 From: John Fernandez Date: Fri, 22 Aug 2025 09:40:49 -0400 Subject: [PATCH 398/466] And move this --- qtfred/src/mission/dialogs/VariableDialogModel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp index d6781d564c5..d9ed801ad0d 100644 --- a/qtfred/src/mission/dialogs/VariableDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -324,8 +324,8 @@ void VariableDialogModel::initializeData() _variableItems.clear(); _containerItems.clear(); - for (int i = 0; i < MAX_SEXP_VARIABLES; ++i){ - if (!(Sexp_variables[i].type & SEXP_VARIABLE_NOT_USED)) { // NOLINT(modernize-loop-convert) + for (int i = 0; i < MAX_SEXP_VARIABLES; ++i){ // NOLINT(modernize-loop-convert) + if (!(Sexp_variables[i].type & SEXP_VARIABLE_NOT_USED)) { _variableItems.emplace_back(); auto& item = _variableItems.back(); item.name = Sexp_variables[i].variable_name; From c39d6b290fc9837772d398313348e1c177fd73d0 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 22 Aug 2025 11:31:19 -0500 Subject: [PATCH 399/466] convert tostdstring to const data --- qtfred/src/ui/dialogs/VariableDialog.cpp | 64 +++++++++++++----------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 6e855917899..00783773f35 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -280,7 +280,7 @@ void VariableDialog::onVariablesTableUpdated() } auto item = ui->variablesTable->item(currentRow, 0); - SCP_string itemText = item->text().toStdString(); + SCP_string itemText = item->text().toUtf8().constData(); bool apply = false; // This will only be true if the user is trying to add a new variable. @@ -327,7 +327,7 @@ void VariableDialog::onVariablesTableUpdated() auto ret = _model->changeVariableName(item->row(), itemText); // we put something in the cell, but the model couldn't process it. - if (strlen(item->text().toStdString().c_str()) && ret.empty()){ + if (strlen(item->text().toUtf8().constData()) && ret.empty()) { // update of variable name failed, resync UI apply = true; @@ -341,7 +341,7 @@ void VariableDialog::onVariablesTableUpdated() // now work on the variable data cell item = ui->variablesTable->item(currentRow, 1); - itemText = item->text().toStdString(); + itemText = item->text().toUtf8().constData(); // check if data column was altered if (itemText != _currentVariableData) { @@ -360,7 +360,7 @@ void VariableDialog::onVariablesTableUpdated() // Variable is a number } else { - SCP_string source = item->text().toStdString(); + SCP_string source = item->text().toUtf8().constData(); SCP_string temp = _model->trimIntegerString(source); try { @@ -404,13 +404,13 @@ void VariableDialog::onVariablesSelectionChanged() auto item = ui->variablesTable->item(row, 0); if (item){ - newVariableName = item->text().toStdString(); + newVariableName = item->text().toUtf8().constData(); } item = ui->variablesTable->item(row, 1); if (item){ - _currentVariableData = item->text().toStdString(); + _currentVariableData = item->text().toUtf8().constData(); } if (newVariableName != _currentVariable){ @@ -439,7 +439,7 @@ void VariableDialog::onContainersTableUpdated() // Are they adding a new container? if (row == ui->containersTable->rowCount() - 1){ if (ui->containersTable->item(row, 0)) { - SCP_string newString = ui->containersTable->item(row, 0)->text().toStdString(); + SCP_string newString = ui->containersTable->item(row, 0)->text().toUtf8().constData(); if (!newString.empty() && newString != "Add Container ..."){ _model->addContainer(newString); _currentContainer = newString; @@ -454,7 +454,7 @@ void VariableDialog::onContainersTableUpdated() // are they editing an existing container name? } else if (ui->containersTable->item(row, 0)){ - SCP_string newName = ui->containersTable->item(row,0)->text().toStdString(); + SCP_string newName = ui->containersTable->item(row,0)->text().toUtf8().constData(); // Restoring a deleted container? if (_currentContainer.empty()){ @@ -484,7 +484,7 @@ void VariableDialog::onContainersSelectionChanged() } // guaranteed not to be null, since getCurrentContainerRow already checked. - _currentContainer = ui->containersTable->item(row, 0)->text().toStdString(); + _currentContainer = ui->containersTable->item(row, 0)->text().toUtf8().constData(); applyModel(); } @@ -512,7 +512,7 @@ void VariableDialog::onContainerContentsTableUpdated() SCP_string newString; if (ui->containerContentsTable->item(row, 0)) { - newString = ui->containerContentsTable->item(row, 0)->text().toStdString(); + newString = ui->containerContentsTable->item(row, 0)->text().toUtf8().constData(); if (!newString.empty() && newString != "Add item ..."){ @@ -532,7 +532,7 @@ void VariableDialog::onContainerContentsTableUpdated() } if (ui->containerContentsTable->item(row, 1)) { - newString = ui->containerContentsTable->item(row, 1)->text().toStdString(); + newString = ui->containerContentsTable->item(row, 1)->text().toUtf8().constData(); if (!newString.empty() && newString != "Add item ..."){ @@ -554,7 +554,7 @@ void VariableDialog::onContainerContentsTableUpdated() // are they editing an existing container item column 1? } else if (ui->containerContentsTable->item(row, 0)){ - SCP_string newText = ui->containerContentsTable->item(row, 0)->text().toStdString(); + SCP_string newText = ui->containerContentsTable->item(row, 0)->text().toUtf8().constData(); if (_model->getContainerListOrMap(containerRow)){ @@ -581,7 +581,7 @@ void VariableDialog::onContainerContentsTableUpdated() // if we're here, nothing has changed so far. So let's attempt column 2 if (ui->containerContentsTable->item(row, 1) && !_model->getContainerListOrMap(containerRow)){ - SCP_string newText = ui->containerContentsTable->item(row, 1)->text().toStdString(); + SCP_string newText = ui->containerContentsTable->item(row, 1)->text().toUtf8().constData(); if(newText != _currentContainerItemCol2){ @@ -624,9 +624,9 @@ void VariableDialog::onContainerContentsSelectionChanged() return; } - newContainerItemName = item->text().toStdString(); + newContainerItemName = item->text().toUtf8().constData(); item = ui->containerContentsTable->item(row, 1); - SCP_string newContainerDataText = (item) ? item->text().toStdString() : ""; + SCP_string newContainerDataText = (item) ? item->text().toUtf8().constData() : ""; if (newContainerItemName != _currentContainerItemCol1 || _currentContainerItemCol2 != newContainerDataText){ _currentContainerItemCol1 = newContainerItemName; @@ -676,7 +676,7 @@ void VariableDialog::onDeleteVariableButtonPressed() } // Because of the text update we'll need, this needs an applyModel, whether it fails or not. - if (ui->deleteVariableButton->text().toStdString() == "Restore") { + if (ui->deleteVariableButton->text().toUtf8().constData() == "Restore") { _model->removeVariable(currentRow, false); applyModel(); } else { @@ -853,7 +853,7 @@ void VariableDialog::onDeleteContainerButtonPressed() } // Because of the text update we'll need, this needs an applyModel, whether it fails or not. - if (ui->deleteContainerButton->text().toStdString() == "Restore"){ + if (ui->deleteContainerButton->text().toUtf8().constData() == "Restore"){ _model->removeContainer(row, false); } else { _model->removeContainer(row, true); @@ -1248,12 +1248,13 @@ void VariableDialog::applyModel() } if (_currentVariable.empty() || selectedRow < 0){ - if (ui->variablesTable->item(0, 0) && !ui->variablesTable->item(0, 0)->text().toStdString().empty()){ - _currentVariable = ui->variablesTable->item(0, 0)->text().toStdString(); + SCP_string text = ui->variablesTable->item(0, 0)->text().toUtf8().constData(); + if (ui->variablesTable->item(0, 0) && !text.empty()) { + _currentVariable = text; } if (ui->variablesTable->item(0, 1)) { - _currentVariableData = ui->variablesTable->item(0, 1)->text().toStdString(); + _currentVariableData = ui->variablesTable->item(0, 1)->text().toUtf8().constData(); } } @@ -1294,11 +1295,13 @@ void VariableDialog::applyModel() } // do we need to switch the delete button to a restore button? - if (selectedRow > -1 && ui->containersTable->item(selectedRow, 2) && ui->containersTable->item(selectedRow, 2)->text().toStdString() == "To Be Deleted") { + SCP_string var = selectedRow > -1 ? ui->containersTable->item(selectedRow, 2)->text().toUtf8().constData() : ""; + if (selectedRow > -1 && ui->containersTable->item(selectedRow, 2) && var == "To Be Deleted") { ui->deleteContainerButton->setText("Restore"); // We can't restore empty container names. - if (ui->containersTable->item(selectedRow, 0) && ui->containersTable->item(selectedRow, 0)->text().toStdString().empty()){ + SCP_string text = ui->containersTable->item(selectedRow, 0)->text().toUtf8().constData(); + if (ui->containersTable->item(selectedRow, 0) && text.empty()){ ui->deleteContainerButton->setEnabled(false); } else { ui->deleteContainerButton->setEnabled(true); @@ -1338,7 +1341,7 @@ void VariableDialog::applyModel() if (selectedRow < 0 && ui->containersTable->rowCount() > 1) { if (ui->containersTable->item(0, 0)){ - _currentContainer = ui->containersTable->item(0, 0)->text().toStdString(); + _currentContainer = ui->containersTable->item(0, 0)->text().toUtf8().constData(); ui->containersTable->clearSelection(); ui->containersTable->item(0, 0)->setSelected(true); } @@ -1396,11 +1399,13 @@ void VariableDialog::updateVariableOptions(bool safeToAlter) ui->setVariableAsNumberRadio->setChecked(!string); // do we need to switch the delete button to a restore button? - if (ui->variablesTable->item(row, 2) && ui->variablesTable->item(row, 2)->text().toStdString() == "To Be Deleted"){ + SCP_string var = ui->variablesTable->item(row, 2) ? ui->variablesTable->item(row, 2)->text().toUtf8().constData() : ""; + if (ui->variablesTable->item(row, 2) && var == "To Be Deleted"){ ui->deleteVariableButton->setText("Restore"); // We can't restore empty variable names. - if (ui->variablesTable->item(row, 0) && ui->variablesTable->item(row, 0)->text().toStdString().empty()){ + SCP_string text = ui->variablesTable->item(row, 0)->text().toUtf8().constData(); + if (ui->variablesTable->item(row, 0) && text.empty()){ ui->deleteVariableButton->setEnabled(false); } else { ui->deleteVariableButton->setEnabled(true); @@ -1823,7 +1828,8 @@ int VariableDialog::getCurrentVariableRow() // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { - if (item && item->column() == 0 && item->text().toStdString() != "Add Variable ...") { + SCP_string var = item->text().toUtf8().constData(); + if (item && item->column() == 0 && var != "Add Variable ...") { return item->row(); } } @@ -1837,7 +1843,8 @@ int VariableDialog::getCurrentContainerRow() // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { - if (item && item->column() == 0 && item->text().toStdString() != "Add Container ...") { + SCP_string var = item->text().toUtf8().constData(); + if (item && item->column() == 0 && var != "Add Container ...") { return item->row(); } } @@ -1851,7 +1858,8 @@ int VariableDialog::getCurrentContainerItemRow() // yes, selected items returns a list, but we really should only have one item because multiselect will be off. for (const auto& item : items) { - if (item && ((item->column() == 0 && item->text().toStdString() != "Add item ...") || (item->column() == 1 && item->text().toStdString() != "Add item ..."))) { + SCP_string var = item->text().toUtf8().constData(); + if (item && ((item->column() == 0 && var != "Add item ...") || (item->column() == 1 && var != "Add item ..."))) { return item->row(); } } From eb36b56f1dc7cc727215415b54054778fc75c76c Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 22 Aug 2025 12:10:52 -0500 Subject: [PATCH 400/466] fix --- qtfred/src/ui/dialogs/VariableDialog.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp index 00783773f35..217fa2317c6 100644 --- a/qtfred/src/ui/dialogs/VariableDialog.cpp +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -676,7 +676,8 @@ void VariableDialog::onDeleteVariableButtonPressed() } // Because of the text update we'll need, this needs an applyModel, whether it fails or not. - if (ui->deleteVariableButton->text().toUtf8().constData() == "Restore") { + SCP_string btn_text = ui->deleteVariableButton->text().toUtf8().constData(); + if (btn_text == "Restore") { _model->removeVariable(currentRow, false); applyModel(); } else { @@ -853,7 +854,8 @@ void VariableDialog::onDeleteContainerButtonPressed() } // Because of the text update we'll need, this needs an applyModel, whether it fails or not. - if (ui->deleteContainerButton->text().toUtf8().constData() == "Restore"){ + SCP_string btn_text = ui->deleteContainerButton->text().toUtf8().constData(); + if (btn_text == "Restore"){ _model->removeContainer(row, false); } else { _model->removeContainer(row, true); From 9ba88ca1630ca2e42bd9f65e27457dc2d6bbf097 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 23 Aug 2025 09:37:47 -0400 Subject: [PATCH 401/466] fix error in error reporting routine (#6969) The `_s` functions throw an exception if the string's length is exceeded. But in an error reporting routine, we are far more interested in the string than the exception, and we'd rather get a partial error message than a crash. Plus this bit of code is already set up to truncate the string anyway. --- code/globalincs/mspdb_callstack.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/globalincs/mspdb_callstack.cpp b/code/globalincs/mspdb_callstack.cpp index ced5563f9d1..3a8ec6736e1 100644 --- a/code/globalincs/mspdb_callstack.cpp +++ b/code/globalincs/mspdb_callstack.cpp @@ -105,7 +105,7 @@ BOOL SCP_mspdbcs_ResolveSymbol( HANDLE hProcess, UINT_PTR dwAddress, SCP_mspdbcs if ( siSymbol.dwOffset != 0 ) { - sprintf_s( szWithOffset, SCP_MSPDBCS_MAX_SYMBOL_LENGTH, "%s + %lld bytes", pszSymbol, siSymbol.dwOffset ); + snprintf( szWithOffset, SCP_MSPDBCS_MAX_SYMBOL_LENGTH, "%s + %lld bytes", pszSymbol, siSymbol.dwOffset ); szWithOffset[ SCP_MSPDBCS_MAX_SYMBOL_LENGTH - 1 ] = '\0'; /* Because sprintf doesn't guarantee NULL terminating */ pszSymbol = szWithOffset; } From 080aa65ec8fc6cfa0d2ef6c042947c37fa7ec332 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 23 Aug 2025 09:57:18 -0500 Subject: [PATCH 402/466] QtFRED Object Orientation Editor Dialog (#6932) * refactor and cleanup the qtfred object dialog * clang is being pedantic * BRAAAACE FOR IMPACT * clang * switch statement --- .../dialogs/ObjectOrientEditorDialogModel.cpp | 482 ++++++++++---- .../dialogs/ObjectOrientEditorDialogModel.h | 130 ++-- .../ui/dialogs/ObjectOrientEditorDialog.cpp | 309 +++++---- .../src/ui/dialogs/ObjectOrientEditorDialog.h | 71 +- qtfred/ui/ObjectOrientationDialog.ui | 605 ++++++++++-------- 5 files changed, 1032 insertions(+), 565 deletions(-) diff --git a/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp index aa9ab8b329c..457580ffed5 100644 --- a/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.cpp @@ -1,7 +1,3 @@ -// -// - - #include "ObjectOrientEditorDialogModel.h" #include @@ -9,197 +5,439 @@ #include #include -const float PREC = 0.0001f; - -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { -ObjectOrientEditorDialogModel::ObjectOrientEditorDialogModel(QObject* parent, EditorViewport* viewport) : - AbstractDialogModel(parent, viewport) { +ObjectOrientEditorDialogModel::ObjectOrientEditorDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ vm_vec_make(&_location, 0.f, 0.f, 0.f); Assert(query_valid_object(_editor->currentObject)); _position = Objects[_editor->currentObject].pos; + angles ang{}; + vm_extract_angles_matrix(&ang, &Objects[_editor->currentObject].orient); + _orientationDeg.xyz.x = normalize_degrees(fl_degrees(ang.p)); + _orientationDeg.xyz.y = normalize_degrees(fl_degrees(ang.b)); + _orientationDeg.xyz.z = normalize_degrees(fl_degrees(ang.h)); + initializeData(); } -bool ObjectOrientEditorDialogModel::apply() { - vec3d delta; - object *ptr; - - vm_vec_sub(&delta, &_position, &Objects[_editor->currentObject].pos); + +void ObjectOrientEditorDialogModel::initializeData() +{ + char text[80]; + int type; + object* ptr; + + _pointToObjectList.clear(); ptr = GET_FIRST(&obj_used_list); while (ptr != END_OF_LIST(&obj_used_list)) { - if (ptr->flags[Object::Object_Flags::Marked]) { - vm_vec_add2(&ptr->pos, &delta); - update_object(ptr); + if (_editor->getNumMarked() != 1 || OBJ_INDEX(ptr) != _editor->currentObject) { + switch (ptr->type) { + case OBJ_START: + case OBJ_SHIP: + _pointToObjectList.emplace_back(ObjectEntry(Ships[ptr->instance].ship_name, OBJ_INDEX(ptr))); + break; + case OBJ_WAYPOINT: { + int waypoint_num; + waypoint_list* wp_list = find_waypoint_list_with_instance(ptr->instance, &waypoint_num); + Assertion(wp_list != nullptr, "Waypoint list was nullptr!"); + sprintf(text, "%s:%d", wp_list->get_name(), waypoint_num + 1); - _editor->missionChanged(); + _pointToObjectList.emplace_back(ObjectEntry(text, OBJ_INDEX(ptr))); + break; + } + case OBJ_POINT: + case OBJ_JUMP_NODE: + break; + default: + Assertion(false, "Unknown object type in Object Orient Dialog!"); // unknown object type + } } ptr = GET_NEXT(ptr); } - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (ptr->flags[Object::Object_Flags::Marked]) - object_moved(ptr); - - ptr = GET_NEXT(ptr); + type = Objects[_editor->currentObject].type; + if (_editor->getNumMarked() == 1 && (type == OBJ_WAYPOINT || type == OBJ_JUMP_NODE)) { + _orientationEnabledForType = false; + _selectedPointToObjectIndex = -1; + } else { + _selectedPointToObjectIndex = _pointToObjectList.empty() ? -1 : _pointToObjectList[0].objIndex; } - return true; + modelChanged(); } -void ObjectOrientEditorDialogModel::update_object(object *ptr) +void ObjectOrientEditorDialogModel::updateObject(object* ptr) { - if (ptr->type != OBJ_WAYPOINT && _point_to) { + if (ptr->type != OBJ_WAYPOINT && _pointTo) { vec3d v; matrix m; memset(&v, 0, sizeof(vec3d)); if (_pointMode == PointToMode::Object) { - if (_selectedObjectNum >= 0) { - v = Objects[_selectedObjectNum].pos; + if (_selectedPointToObjectIndex >= 0) { + v = Objects[_selectedPointToObjectIndex].pos; vm_vec_sub2(&v, &ptr->pos); } - } - else if (_pointMode == PointToMode::Location) { + } else if (_pointMode == PointToMode::Location) { vm_vec_sub(&v, &_location, &ptr->pos); - } - else { - Assert(0); // neither radio button is checked. + } else { + Assertion(false, "Unknown Point To mode in Object Orient Dialog!"); // neither radio button is checked. } if (!v.xyz.x && !v.xyz.y && !v.xyz.z) { - return; // can't point to itself. + return; // can't point to itself. } - vm_vector_2_matrix(&m, &v, NULL, NULL); + vm_vector_2_matrix(&m, &v, nullptr, nullptr); ptr->orient = m; } } -void ObjectOrientEditorDialogModel::reject() { +// Also in objectorient.cpp in FRED. TODO Would be nice if this were somewhere common +float ObjectOrientEditorDialogModel::normalize_degrees(float deg) +{ + while (deg < -180.0f) + deg += 360.0f; + while (deg > 180.0f) + deg -= 360.0f; + // collapse negative zero + if (deg == -0.0f) + deg = 0.0f; + return deg; +} + +float ObjectOrientEditorDialogModel::round1(float v) +{ + return std::round(v * 10.0f) / 10.0f; } -void ObjectOrientEditorDialogModel::initializeData() { - char text[80]; - int type; - object *ptr; +ObjectOrientEditorDialogModel::ObjectEntry::ObjectEntry(SCP_string in_name, int in_objIndex) + : name(std::move(in_name)), objIndex(in_objIndex) +{ +} - total = 0; - _entries.clear(); +bool ObjectOrientEditorDialogModel::apply() +{ + object* origin_objp = nullptr; - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (_editor->getNumMarked() != 1 || OBJ_INDEX(ptr) != _editor->currentObject) { - if ((ptr->type == OBJ_START) || (ptr->type == OBJ_SHIP)) { - _entries.push_back(ObjectEntry(Ships[ptr->instance].ship_name, OBJ_INDEX(ptr))); - } else if (ptr->type == OBJ_WAYPOINT) { - int waypoint_num; - waypoint_list *wp_list = find_waypoint_list_with_instance(ptr->instance, &waypoint_num); - Assert(wp_list != NULL); - sprintf(text, "%s:%d", wp_list->get_name(), waypoint_num + 1); - - _entries.push_back(ObjectEntry(text, OBJ_INDEX(ptr))); - } else if ((ptr->type == OBJ_POINT) || (ptr->type == OBJ_JUMP_NODE)) { + // Build translation delta and orientation matrix from UI values + vec3d delta = vmd_zero_vector; + matrix desired_orient = vmd_identity_matrix; + bool change_pos = false, change_orient = false; + + auto& obj = Objects[_editor->currentObject]; + + // ----- Position ----- + // If Relative: _position is a local space delta; unrotate into world + // If Absolute: delta = _position - obj.pos + { + const vec3d& refPos = (_setMode == SetMode::Relative) ? vmd_zero_vector : obj.pos; + if (!is_close(refPos.xyz.x, _position.xyz.x) || !is_close(refPos.xyz.y, _position.xyz.y) || + !is_close(refPos.xyz.z, _position.xyz.z)) { + + if (_setMode == SetMode::Relative) { + vm_vec_unrotate(&delta, &_position, &obj.orient); } else { - Assert(0); // unknown object type + vm_vec_sub(&delta, &_position, &refPos); } + change_pos = true; } + } - ptr = GET_NEXT(ptr); + // ----- Orientation ----- + { + angles object_ang{}; + vm_extract_angles_matrix(&object_ang, &obj.orient); + + vec3d refDeg = (_setMode == SetMode::Relative) ? vmd_zero_vector + : vec3d{{{normalize_degrees(fl_degrees(object_ang.p)), + normalize_degrees(fl_degrees(object_ang.b)), + normalize_degrees(fl_degrees(object_ang.h))}}}; + + if (!is_close(refDeg.xyz.x, normalize_degrees(_orientationDeg.xyz.x)) || + !is_close(refDeg.xyz.y, normalize_degrees(_orientationDeg.xyz.y)) || + !is_close(refDeg.xyz.z, normalize_degrees(_orientationDeg.xyz.z))) { + + angles ang{}; + ang.p = fl_radians(_orientationDeg.xyz.x); + ang.b = fl_radians(_orientationDeg.xyz.y); + ang.h = fl_radians(_orientationDeg.xyz.z); + + if (_setMode == SetMode::Relative) { + ang.p = object_ang.p + ang.p; + ang.b = object_ang.b + ang.b; + ang.h = object_ang.h + ang.h; + } + vm_angles_2_matrix(&desired_orient, &ang); + change_orient = true; + } } - type = Objects[_editor->currentObject].type; - if (_editor->getNumMarked() == 1 && type == OBJ_WAYPOINT) { - _enabled = false; - _selectedObjectNum = -1; - } else { - _selectedObjectNum = _entries.empty() ? -1 : _entries[0].objIndex; + // ----- Transform mode ----- + // If multiple marked and using Relative to Origin, move/rotate the origin first, then + // bring everyone else along by the origin’s delta rotation and position. + matrix origin_rotation = vmd_identity_matrix; + vec3d origin_prev_pos = vmd_zero_vector; + + const bool manyMarked = (_editor->getNumMarked() > 1); + const bool relativeToOrigin = (manyMarked && _transformMode == TransformMode::Relative); + + if (relativeToOrigin) { + origin_objp = &obj; + origin_prev_pos = origin_objp->pos; + matrix saved_orient = origin_objp->orient; + + // Move the origin first + if (change_pos) { + vm_vec_add2(&origin_objp->pos, &delta); + _editor->missionChanged(); + } + if (_pointTo) { + updateObject(origin_objp); + _editor->missionChanged(); + } else if (change_orient) { + origin_objp->orient = desired_orient; + _editor->missionChanged(); + } + + if (origin_objp->type != OBJ_WAYPOINT) { + vm_transpose(&saved_orient); + origin_rotation = saved_orient * origin_objp->orient; + } } - modelChanged(); + // Apply to all marked objects + for (auto ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (!ptr->flags[Object::Object_Flags::Marked]) + continue; + + // Skip the origin in the second pass + if (relativeToOrigin && ptr == origin_objp) + continue; + + if (relativeToOrigin) { + // Transform relative to new origin pose + vec3d relative_pos, transformed_pos; + vm_vec_sub(&relative_pos, &ptr->pos, &origin_prev_pos); + vm_vec_unrotate(&transformed_pos, &relative_pos, &origin_rotation); + vm_vec_add(&ptr->pos, &transformed_pos, &origin_objp->pos); + + ptr->orient = ptr->orient * origin_rotation; + _editor->missionChanged(); + } else { + // Independent transform of each marked object + if (change_pos) { + vm_vec_add2(&ptr->pos, &delta); + _editor->missionChanged(); + } + if (_pointTo) { + updateObject(ptr); + _editor->missionChanged(); + } else if (change_orient) { + ptr->orient = desired_orient; + _editor->missionChanged(); + } + } + } + + // Notify the engine about moved objects + if (change_pos || relativeToOrigin) { + for (auto ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->flags[Object::Object_Flags::Marked]) { + object_moved(ptr); + } + } + } + + return true; } +void ObjectOrientEditorDialogModel::reject() { + // Do nothing +} -bool ObjectOrientEditorDialogModel::query_modified() +void ObjectOrientEditorDialogModel::setPositionX(float x) { - float dif; + if (!is_close(_position.xyz.x, x)) { + modify(_position.xyz.x, round1(x)); + } +} - dif = Objects[_editor->currentObject].pos.xyz.x - _position.xyz.x; - if ((dif > PREC) || (dif < -PREC)) - return true; - dif = Objects[_editor->currentObject].pos.xyz.y - _position.xyz.y; - if ((dif > PREC) || (dif < -PREC)) - return true; - dif = Objects[_editor->currentObject].pos.xyz.z - _position.xyz.z; - if ((dif > PREC) || (dif < -PREC)) - return true; +void ObjectOrientEditorDialogModel::setPositionY(float y) +{ + if (!is_close(_position.xyz.y, y)) { + modify(_position.xyz.y, round1(y)); + } +} +void ObjectOrientEditorDialogModel::setPositionZ(float z) +{ + if (!is_close(_position.xyz.z, z)) { + modify(_position.xyz.z, round1(z)); + } +} - if (_point_to) - return true; +ObjectPosition ObjectOrientEditorDialogModel::getPosition() const +{ + return {_position.xyz.x, _position.xyz.y, _position.xyz.z}; +} - return false; +void ObjectOrientEditorDialogModel::setOrientationP(float deg) +{ + float val = normalize_degrees(round1(deg)); + if (!is_close(_orientationDeg.xyz.x, deg)) { + modify(_orientationDeg.xyz.x, val); + } } -int ObjectOrientEditorDialogModel::getObjectIndex() const { - return _selectedObjectNum; + +void ObjectOrientEditorDialogModel::setOrientationB(float deg) +{ + float val = normalize_degrees(round1(deg)); + if (!is_close(_orientationDeg.xyz.y, deg)) { + modify(_orientationDeg.xyz.y, val); + } } -bool ObjectOrientEditorDialogModel::isPointTo() const { - return _point_to; + +void ObjectOrientEditorDialogModel::setOrientationH(float deg) +{ + float val = normalize_degrees(round1(deg)); + if (!is_close(_orientationDeg.xyz.z, deg)) { + modify(_orientationDeg.xyz.z, val); + } } -const vec3d& ObjectOrientEditorDialogModel::getPosition() const { - return _position; + +ObjectOrientation ObjectOrientEditorDialogModel::getOrientation() const +{ + return {normalize_degrees(_orientationDeg.xyz.x), + normalize_degrees(_orientationDeg.xyz.y), + normalize_degrees(_orientationDeg.xyz.z)}; } -const vec3d& ObjectOrientEditorDialogModel::getLocation() const { - return _location; + +void ObjectOrientEditorDialogModel::setSetMode(SetMode mode) +{ + if (_setMode == mode) { + return; + } + + // Current object pose for capturing baseline when entering Relative + const object& obj = Objects[_editor->currentObject]; + + angles objAng{}; + vm_extract_angles_matrix(&objAng, &obj.orient); + + vec3d objAngDeg; + objAngDeg.xyz.x = normalize_degrees(fl_degrees(objAng.p)); + objAngDeg.xyz.y = normalize_degrees(fl_degrees(objAng.b)); + objAngDeg.xyz.z = normalize_degrees(fl_degrees(objAng.h)); + + if (mode == SetMode::Relative) { + // Capture baseline once when switching to Relative + _rebaseRefPos = obj.pos; + _rebaseRefAnglesDeg = objAngDeg; + + // Absolute to Relative: subtract the captured baseline + _position.xyz.x -= _rebaseRefPos.xyz.x; + _position.xyz.y -= _rebaseRefPos.xyz.y; + _position.xyz.z -= _rebaseRefPos.xyz.z; + + _orientationDeg.xyz.x = normalize_degrees(_orientationDeg.xyz.x - _rebaseRefAnglesDeg.xyz.x); + _orientationDeg.xyz.y = normalize_degrees(_orientationDeg.xyz.y - _rebaseRefAnglesDeg.xyz.y); + _orientationDeg.xyz.z = normalize_degrees(_orientationDeg.xyz.z - _rebaseRefAnglesDeg.xyz.z); + } else { + // Relative to Absolute: add the same captured baseline + _position.xyz.x += _rebaseRefPos.xyz.x; + _position.xyz.y += _rebaseRefPos.xyz.y; + _position.xyz.z += _rebaseRefPos.xyz.z; + + _orientationDeg.xyz.x = normalize_degrees(_orientationDeg.xyz.x + _rebaseRefAnglesDeg.xyz.x); + _orientationDeg.xyz.y = normalize_degrees(_orientationDeg.xyz.y + _rebaseRefAnglesDeg.xyz.y); + _orientationDeg.xyz.z = normalize_degrees(_orientationDeg.xyz.z + _rebaseRefAnglesDeg.xyz.z); + } + + // Round to one decimal and normalize angles + _position.xyz.x = round1(_position.xyz.x); + _position.xyz.y = round1(_position.xyz.y); + _position.xyz.z = round1(_position.xyz.z); + + _orientationDeg.xyz.x = normalize_degrees(round1(_orientationDeg.xyz.x)); + _orientationDeg.xyz.y = normalize_degrees(round1(_orientationDeg.xyz.y)); + _orientationDeg.xyz.z = normalize_degrees(round1(_orientationDeg.xyz.z)); + + modify(_setMode, mode); +} + +ObjectOrientEditorDialogModel::SetMode ObjectOrientEditorDialogModel::getSetMode() const +{ + return _setMode; +} + +void ObjectOrientEditorDialogModel::setTransformMode(TransformMode mode) +{ + modify(_transformMode, mode); +} + +ObjectOrientEditorDialogModel::TransformMode ObjectOrientEditorDialogModel::getTransformMode() const +{ + return _transformMode; } -bool ObjectOrientEditorDialogModel::isEnabled() const { - return _enabled; + +void ObjectOrientEditorDialogModel::setPointTo(bool point_to) +{ + modify(_pointTo, point_to); } -const SCP_vector& -ObjectOrientEditorDialogModel::getEntries() const { - return _entries; + +bool ObjectOrientEditorDialogModel::getPointTo() const +{ + return _pointTo; } -ObjectOrientEditorDialogModel::PointToMode ObjectOrientEditorDialogModel::getPointMode() const { + +void ObjectOrientEditorDialogModel::setPointMode(ObjectOrientEditorDialogModel::PointToMode pointMode) +{ + modify(_pointMode, pointMode); +} + +ObjectOrientEditorDialogModel::PointToMode ObjectOrientEditorDialogModel::getPointMode() const +{ return _pointMode; } -void ObjectOrientEditorDialogModel::setSelectedObjectNum(int selectedObjectNum) { - if (_selectedObjectNum != selectedObjectNum) { - _selectedObjectNum = selectedObjectNum; - modelChanged(); - } + +void ObjectOrientEditorDialogModel::setPointToObjectIndex(int selectedObjectNum) +{ + modify(_selectedPointToObjectIndex, selectedObjectNum); } -void ObjectOrientEditorDialogModel::setPointTo(bool point_to) { - if (_point_to != point_to) { - _point_to = point_to; - modelChanged(); - } + +int ObjectOrientEditorDialogModel::getPointToObjectIndex() const +{ + return _selectedPointToObjectIndex; } -void ObjectOrientEditorDialogModel::setPosition(const vec3d& position) { - if (_position != position) { - _position = position; - modelChanged(); + +void ObjectOrientEditorDialogModel::setLocationX(float x) +{ + if (!is_close(_location.xyz.x, x)) { + modify(_location.xyz.x, round1(x)); } } -void ObjectOrientEditorDialogModel::setLocation(const vec3d& location) { - if (_location != location) { - _location = location; - modelChanged(); + +void ObjectOrientEditorDialogModel::setLocationY(float y) +{ + if (!is_close(_location.xyz.y, y)) { + modify(_location.xyz.y, round1(y)); } } -void ObjectOrientEditorDialogModel::setPointMode(ObjectOrientEditorDialogModel::PointToMode pointMode) { - if (_pointMode != pointMode) { - _pointMode = pointMode; - modelChanged(); + +void ObjectOrientEditorDialogModel::setLocationZ(float z) +{ + if (!is_close(_location.xyz.z, z)) { + modify(_location.xyz.z, round1(z)); } } -ObjectOrientEditorDialogModel::ObjectEntry::ObjectEntry(const SCP_string& in_name, int in_objIndex) : - name(in_name), objIndex(in_objIndex) { -} -} -} +ObjectPosition ObjectOrientEditorDialogModel::getLocation() const +{ + return {_location.xyz.x, _location.xyz.y, _location.xyz.z}; } + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.h b/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.h index 90026a58ae5..1add6cfb436 100644 --- a/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/ObjectOrientEditorDialogModel.h @@ -1,69 +1,111 @@ #pragma once +#include "AbstractDialogModel.h" +namespace fso::fred::dialogs { -#include "AbstractDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +struct ObjectPosition { + float x = 0.0f; + float y = 0.0f; + float z = 0.0f; +}; +struct ObjectOrientation { + float p = 0.0f; + float b = 0.0f; + float h = 0.0f; +}; + +class ObjectOrientEditorDialogModel : public AbstractDialogModel { + public: + ObjectOrientEditorDialogModel(QObject* parent, EditorViewport* viewport); -class ObjectOrientEditorDialogModel: public AbstractDialogModel { - public: struct ObjectEntry { SCP_string name; int objIndex = -1; - - ObjectEntry(const SCP_string& name, int objIndex); + ObjectEntry(SCP_string name, int objIndex); }; enum class PointToMode { Object, Location }; + enum class SetMode { + Absolute, + Relative + }; + enum class TransformMode { + Independent, + Relative + }; - private: + bool apply() override; + void reject() override; + + bool isOrientationEnabledForType() const {return _orientationEnabledForType;}; + const SCP_vector& getPointToObjectList() const {return _pointToObjectList;}; + int getNumObjectsMarked() const {return _editor->getNumMarked();} + + // Position + void setPositionX(float x); + void setPositionY(float y); + void setPositionZ(float z); + ObjectPosition getPosition() const; + + // Orientation + void setOrientationP(float deg); + void setOrientationB(float deg); + void setOrientationH(float deg); + ObjectOrientation getOrientation() const; + + // Settings + void setSetMode(SetMode mode); + SetMode getSetMode() const; + void setTransformMode(TransformMode mode); + TransformMode getTransformMode() const; + + // Point to + void setPointTo(bool point_to); + bool getPointTo() const; + void setPointMode(PointToMode pointMode); + PointToMode getPointMode() const; + void setPointToObjectIndex(int selectedObjectNum); + int getPointToObjectIndex() const; + void setLocationX(float x); + void setLocationY(float y); + void setLocationZ(float z); + ObjectPosition getLocation() const; + + private: void initializeData(); + void updateObject(object* ptr); - int _selectedObjectNum = -1; - bool _point_to = false; + int _selectedPointToObjectIndex = -1; + bool _pointTo = false; - vec3d _position; - vec3d _location; + vec3d _position; // UI fields: X/Y/Z + vec3d _orientationDeg; // UI fields: Pitch/Bank/Heading in degrees + vec3d _location; // Point to location X/Y/Z - bool _enabled = true; + vec3d _rebaseRefPos = vmd_zero_vector; + vec3d _rebaseRefAnglesDeg = vmd_zero_vector; - int total = 0; + bool _orientationEnabledForType = true; - SCP_vector _entries; + SCP_vector _pointToObjectList; PointToMode _pointMode = PointToMode::Object; - - void update_object(object* ptr); - public: - ObjectOrientEditorDialogModel(QObject* parent, EditorViewport* viewport); - - bool apply() override; - - void reject() override; - - int getObjectIndex() const; - bool isPointTo() const; - const vec3d& getPosition() const; - const vec3d& getLocation() const; - bool isEnabled() const; - const SCP_vector& getEntries() const; - PointToMode getPointMode() const; - - void setSelectedObjectNum(int selectedObjectNum); - void setPointTo(bool point_to); - void setPosition(const vec3d& position); - void setLocation(const vec3d& location); - void setPointMode(PointToMode pointMode); - - bool query_modified(); + SetMode _setMode = SetMode::Absolute; + TransformMode _transformMode = TransformMode::Independent; + + // Helpers + static constexpr float INPUT_THRESHOLD = 0.01f; // Same as FRED in orienteditor.cpp TODO would be nice if this was stored somewhere common + static float normalize_degrees(float deg); + static bool is_close(float a, float b) + { + return fabsf(a - b) < INPUT_THRESHOLD; + } + + static float round1(float v); }; -} -} -} - +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.cpp b/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.cpp index 9e8c0e2b56b..72d4d2bccc6 100644 --- a/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.cpp @@ -9,169 +9,248 @@ #include "mission/util.h" #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { ObjectOrientEditorDialog::ObjectOrientEditorDialog(FredView* parent, EditorViewport* viewport) : QDialog(parent), ui(new Ui::ObjectOrientEditorDialog()), _model(new ObjectOrientEditorDialogModel(this, viewport)), _viewport(viewport) { + this->setFocus(); ui->setupUi(this); - connect(this, &QDialog::accepted, _model.get(), &ObjectOrientEditorDialogModel::apply); - connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ObjectOrientEditorDialog::rejectHandler); - - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &ObjectOrientEditorDialog::updateUI); - - connect(ui->objectComboBox, - static_cast(&QComboBox::currentIndexChanged), - this, - &ObjectOrientEditorDialog::objectSelectionChanged); - - connect(ui->objectRadio, &QRadioButton::toggled, this, &ObjectOrientEditorDialog::objectRadioToggled); - connect(ui->locationRadio, &QRadioButton::toggled, this, &ObjectOrientEditorDialog::locationRadioToggled); - - connect(ui->pointToCheck, &QCheckBox::toggled, this, &ObjectOrientEditorDialog::pointToChecked); - - connect(ui->position_x, - static_cast(&QDoubleSpinBox::valueChanged), - this, - &ObjectOrientEditorDialog::positionValueChangedX); - connect(ui->position_y, - static_cast(&QDoubleSpinBox::valueChanged), - this, - &ObjectOrientEditorDialog::positionValueChangedY); - connect(ui->position_z, - static_cast(&QDoubleSpinBox::valueChanged), - this, - &ObjectOrientEditorDialog::positionValueChangedZ); - - connect(ui->location_x, - static_cast(&QDoubleSpinBox::valueChanged), - this, - &ObjectOrientEditorDialog::locationValueChangedX); - connect(ui->location_y, - static_cast(&QDoubleSpinBox::valueChanged), - this, - &ObjectOrientEditorDialog::locationValueChangedY); - connect(ui->location_z, - static_cast(&QDoubleSpinBox::valueChanged), - this, - &ObjectOrientEditorDialog::locationValueChangedZ); - - updateUI(); + // set our internal values, update the UI + initializeUi(); + updateUi(); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); } -ObjectOrientEditorDialog::~ObjectOrientEditorDialog() { +ObjectOrientEditorDialog::~ObjectOrientEditorDialog() = default; + +void ObjectOrientEditorDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void ObjectOrientEditorDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close } -void ObjectOrientEditorDialog::closeEvent(QCloseEvent* e) { - if (!rejectOrCloseHandler(this, _model.get(), _viewport)) { - e->ignore(); - }; +void ObjectOrientEditorDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window } -void ObjectOrientEditorDialog::rejectHandler() +void ObjectOrientEditorDialog::initializeUi() { - this->close(); + updateComboBox(); + + if (_model->getPointToObjectList().empty()) { + _model->setPointMode(ObjectOrientEditorDialogModel::PointToMode::Location); + } } -void ObjectOrientEditorDialog::updateUI() { +void ObjectOrientEditorDialog::updateUi() +{ util::SignalBlockers blockers(this); - ui->position_x->setValue(_model->getPosition().xyz.x); - ui->position_y->setValue(_model->getPosition().xyz.y); - ui->position_z->setValue(_model->getPosition().xyz.z); + updatePosition(); + updateOrientation(); + updatePointTo(); + updateLocation(); - ui->location_x->setValue(_model->getLocation().xyz.x); - ui->location_y->setValue(_model->getLocation().xyz.y); - ui->location_z->setValue(_model->getLocation().xyz.z); + enableOrDisableControls(); +} - ui->pointToCheck->setChecked(_model->isPointTo()); +void ObjectOrientEditorDialog::enableOrDisableControls() +{ + ui->orientationGroupBox->setEnabled(!_model->getPointTo() && _model->isOrientationEnabledForType()); + ui->pointToGroupBox->setEnabled(_model->getPointTo() && _model->isOrientationEnabledForType()); + ui->transformSettingsGroupBox->setEnabled(_model->getNumObjectsMarked() > 1 && _model->isOrientationEnabledForType()); - ui->objectRadio->setChecked(_model->getPointMode() == ObjectOrientEditorDialogModel::PointToMode::Object); - ui->locationRadio->setChecked(_model->getPointMode() == ObjectOrientEditorDialogModel::PointToMode::Location); + bool enableLocation = _model->getPointTo() && _model->getPointMode() == ObjectOrientEditorDialogModel::PointToMode::Location; + bool noEntries = _model->getPointToObjectList().empty(); + bool enableObject = _model->getPointTo() && !noEntries && _model->getPointMode() == ObjectOrientEditorDialogModel::PointToMode::Object; - updateComboBox(); + ui->objectRadioButton->setEnabled(!noEntries); + ui->objectComboBox->setEnabled(enableObject); + ui->locationXSpinBox->setEnabled(enableLocation); + ui->locationYSpinBox->setEnabled(enableLocation); + ui->locationZSpinBox->setEnabled(enableLocation); +} - ui->position_x->setEnabled(_model->isEnabled()); - ui->position_y->setEnabled(_model->isEnabled()); - ui->position_z->setEnabled(_model->isEnabled()); +void ObjectOrientEditorDialog::updatePosition() +{ + util::SignalBlockers blockers(this); - ui->location_x->setEnabled(_model->isEnabled()); - ui->location_y->setEnabled(_model->isEnabled()); - ui->location_z->setEnabled(_model->isEnabled()); + ui->positionXSpinBox->setValue(_model->getPosition().x); + ui->positionYSpinBox->setValue(_model->getPosition().y); + ui->positionZSpinBox->setValue(_model->getPosition().z); +} - ui->pointToCheck->setEnabled(_model->isEnabled()); +void ObjectOrientEditorDialog::updateOrientation() +{ + util::SignalBlockers blockers(this); - ui->objectRadio->setEnabled(_model->isEnabled()); + ui->orientationPSpinBox->setValue(_model->getOrientation().p); + ui->orientationBSpinBox->setValue(_model->getOrientation().b); + ui->orientationHSpinBox->setValue(_model->getOrientation().h); +} - ui->objectComboBox->setEnabled(_model->isEnabled()); +void ObjectOrientEditorDialog::updatePointTo() +{ + util::SignalBlockers blockers(this); - ui->locationRadio->setEnabled(_model->isEnabled()); - ui->location_x->setEnabled(_model->isEnabled()); - ui->location_y->setEnabled(_model->isEnabled()); - ui->location_z->setEnabled(_model->isEnabled()); + ui->pointToCheckBox->setChecked(_model->getPointTo()); + ui->objectRadioButton->setChecked(_model->getPointMode() == ObjectOrientEditorDialogModel::PointToMode::Object); + ui->locationRadioButton->setChecked(_model->getPointMode() == ObjectOrientEditorDialogModel::PointToMode::Location); } -void ObjectOrientEditorDialog::updateComboBox() { + +void ObjectOrientEditorDialog::updateComboBox() +{ + util::SignalBlockers blockers(this); + ui->objectComboBox->clear(); - for (auto& entry : _model->getEntries()) { + for (auto& entry : _model->getPointToObjectList()) { ui->objectComboBox->addItem(QString::fromStdString(entry.name), QVariant(entry.objIndex)); } - ui->objectComboBox->setCurrentIndex(ui->objectComboBox->findData(_model->getObjectIndex())); + ui->objectComboBox->setCurrentIndex(ui->objectComboBox->findData(_model->getPointToObjectIndex())); } -void ObjectOrientEditorDialog::objectSelectionChanged(int index) { - auto objNum = ui->objectComboBox->itemData(index).value(); - _model->setSelectedObjectNum(objNum); + +void ObjectOrientEditorDialog::updateLocation() +{ + util::SignalBlockers blockers(this); + + ui->locationXSpinBox->setValue(_model->getLocation().x); + ui->locationYSpinBox->setValue(_model->getLocation().y); + ui->locationZSpinBox->setValue(_model->getLocation().z); } -void ObjectOrientEditorDialog::objectRadioToggled(bool enabled) { - if (enabled) { - _model->setPointMode(ObjectOrientEditorDialogModel::PointToMode::Object); - } + +void ObjectOrientEditorDialog::on_okAndCancelButtons_accepted() +{ + accept(); } -void ObjectOrientEditorDialog::locationRadioToggled(bool enabled) { - if (enabled) { - _model->setPointMode(ObjectOrientEditorDialogModel::PointToMode::Location); + +void ObjectOrientEditorDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void ObjectOrientEditorDialog::on_positionXSpinBox_valueChanged(double value) +{ + _model->setPositionX(static_cast(value)); +} + +void ObjectOrientEditorDialog::on_positionYSpinBox_valueChanged(double value) +{ + _model->setPositionY(static_cast(value)); +} + +void ObjectOrientEditorDialog::on_positionZSpinBox_valueChanged(double value) +{ + _model->setPositionZ(static_cast(value)); +} + +void ObjectOrientEditorDialog::on_orientationPSpinBox_valueChanged(double value) +{ + _model->setOrientationP(static_cast(value)); +} + +void ObjectOrientEditorDialog::on_orientationBSpinBox_valueChanged(double value) +{ + _model->setOrientationB(static_cast(value)); +} + +void ObjectOrientEditorDialog::on_orientationHSpinBox_valueChanged(double value) +{ + _model->setOrientationH(static_cast(value)); +} + +void ObjectOrientEditorDialog::on_setAbsoluteRadioButton_toggled(bool checked) +{ + if (checked) { + _model->setSetMode(ObjectOrientEditorDialogModel::SetMode::Absolute); + updateUi(); } } -void ObjectOrientEditorDialog::pointToChecked(bool checked) { - _model->setPointTo(checked); + +void ObjectOrientEditorDialog::on_setRelativeRadioButton_toggled(bool checked) +{ + if (checked) { + _model->setSetMode(ObjectOrientEditorDialogModel::SetMode::Relative); + updateUi(); + } } -void ObjectOrientEditorDialog::positionValueChangedX(double value) { - auto oldVal = _model->getPosition(); - oldVal.xyz.x = (float) value; - _model->setPosition(oldVal); + +void ObjectOrientEditorDialog::on_transformIndependentlyRadioButton_toggled(bool checked) +{ + if (checked) { + _model->setTransformMode(ObjectOrientEditorDialogModel::TransformMode::Independent); + updateUi(); + } } -void ObjectOrientEditorDialog::positionValueChangedY(double value) { - auto oldVal = _model->getPosition(); - oldVal.xyz.y = (float) value; - _model->setPosition(oldVal); + +void ObjectOrientEditorDialog::on_transformRelativelyRadioButton_toggled(bool checked) +{ + if (checked) { + _model->setTransformMode(ObjectOrientEditorDialogModel::TransformMode::Relative); + updateUi(); + } } -void ObjectOrientEditorDialog::positionValueChangedZ(double value) { - auto oldVal = _model->getPosition(); - oldVal.xyz.z = (float) value; - _model->setPosition(oldVal); + +void ObjectOrientEditorDialog::on_pointToCheckBox_toggled(bool checked) +{ + _model->setPointTo(checked); + updateUi(); } -void ObjectOrientEditorDialog::locationValueChangedX(double value) { - auto oldVal = _model->getLocation(); - oldVal.xyz.x = (float) value; - _model->setLocation(oldVal); +void ObjectOrientEditorDialog::on_objectRadioButton_toggled(bool checked) +{ + if (checked) { + _model->setPointMode(ObjectOrientEditorDialogModel::PointToMode::Object); + updateUi(); + } } -void ObjectOrientEditorDialog::locationValueChangedY(double value) { - auto oldVal = _model->getLocation(); - oldVal.xyz.y = (float) value; - _model->setLocation(oldVal); + +void ObjectOrientEditorDialog::on_objectComboBox_currentIndexChanged(int index) +{ + auto objNum = ui->objectComboBox->itemData(index).value(); + _model->setPointToObjectIndex(objNum); } -void ObjectOrientEditorDialog::locationValueChangedZ(double value) { - auto oldVal = _model->getLocation(); - oldVal.xyz.z = (float) value; - _model->setLocation(oldVal); + +void ObjectOrientEditorDialog::on_locationRadioButton_toggled(bool checked) +{ + if (checked) { + _model->setPointMode(ObjectOrientEditorDialogModel::PointToMode::Location); + updateUi(); + } } +void ObjectOrientEditorDialog::on_locationXSpinBox_valueChanged(double value) +{ + _model->setLocationX(static_cast(value)); } + +void ObjectOrientEditorDialog::on_locationYSpinBox_valueChanged(double value) +{ + _model->setLocationY(static_cast(value)); } + +void ObjectOrientEditorDialog::on_locationZSpinBox_valueChanged(double value) +{ + _model->setLocationZ(static_cast(value)); } + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.h b/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.h index b50a10057f8..0276b5795cb 100644 --- a/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.h +++ b/qtfred/src/ui/dialogs/ObjectOrientEditorDialog.h @@ -6,49 +6,68 @@ #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class ObjectOrientEditorDialog; } class ObjectOrientEditorDialog : public QDialog { + Q_OBJECT public: ObjectOrientEditorDialog(FredView* parent, EditorViewport* viewport); ~ObjectOrientEditorDialog() override; + void accept() override; + void reject() override; protected: - void closeEvent(QCloseEvent*) override; - void rejectHandler(); - -private: + void closeEvent(QCloseEvent* e) override; // funnel all Window X presses through reject() + +private slots: + // dialog controls + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + // Position + void on_positionXSpinBox_valueChanged(double value); + void on_positionYSpinBox_valueChanged(double value); + void on_positionZSpinBox_valueChanged(double value); + // Orientation + void on_orientationPSpinBox_valueChanged(double value); + void on_orientationBSpinBox_valueChanged(double value); + void on_orientationHSpinBox_valueChanged(double value); + // Settings + void on_setAbsoluteRadioButton_toggled(bool checked); + void on_setRelativeRadioButton_toggled(bool checked); + void on_transformIndependentlyRadioButton_toggled(bool checked); + void on_transformRelativelyRadioButton_toggled(bool checked); + // Point to + void on_pointToCheckBox_toggled(bool checked); + void on_objectRadioButton_toggled(bool checked); + void on_objectComboBox_currentIndexChanged(int index); + void on_locationRadioButton_toggled(bool checked); + void on_locationXSpinBox_valueChanged(double value); + void on_locationYSpinBox_valueChanged(double value); + void on_locationZSpinBox_valueChanged(double value); + + +private: // NOLINT(readability-redundant-access-specifiers) + void initializeUi(); + void updateUi(); + void enableOrDisableControls(); + + // Boilerplate std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; - void updateUI(); + // Group updates + void updatePosition(); + void updateOrientation(); + void updatePointTo(); void updateComboBox(); - - void objectSelectionChanged(int index); - - void objectRadioToggled(bool enabled); - void locationRadioToggled(bool enabled); - - void pointToChecked(bool checked); - - void positionValueChangedX(double value); - void positionValueChangedY(double value); - void positionValueChangedZ(double value); - - void locationValueChangedX(double value); - void locationValueChangedY(double value); - void locationValueChangedZ(double value); + void updateLocation(); }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/ObjectOrientationDialog.ui b/qtfred/ui/ObjectOrientationDialog.ui index 5b3bd355d34..b3d68d53005 100644 --- a/qtfred/ui/ObjectOrientationDialog.ui +++ b/qtfred/ui/ObjectOrientationDialog.ui @@ -22,275 +22,364 @@ true - + QLayout::SetFixedSize - - + + - - - - 0 - 0 - - - - Position - - - - - - -9999.000000000000000 - - - 9999.000000000000000 - - - 0.000000000000000 - - - - - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - - - &Y: - - - position_y - - - - - - - &X: - - - position_x - - - - - - - &Z: - - - position_z - - - - - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - + + + + + + + + 0 + 0 + + + + Position + + + + + + &X: + + + positionXSpinBox + + + + + + + 2 + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + + + + + &Y: + + + positionYSpinBox + + + + + + + 2 + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + 0.000000000000000 + + + + + + + &Z: + + + positionZSpinBox + + + + + + + 2 + + + -9999.000000000000000 + + + 9999.000000000000000 + + + + + + + + + + Orientation + + + + + + P: + + + + + + + B: + + + + + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + + + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + + + + + H: + + + + + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + + + + + + + + + + + + + + + Set Absolute + + + true + + + + + + + Set Relative + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + Transform Objects Independently + + + true + + + + + + + Transform Objects Relative to Origin Object + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + - - - Qt::Vertical - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - false - - + + + + + Point to: + + + + + + + + 10 + + + + + Object: + + + true + + + + + + + + + + Location: + + + + + + + + + X: + + + + + + + 2 + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + + + + + Y: + + + + + + + 2 + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + + + + + Z: + + + + + + + 2 + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + + + + + + + - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - - 0 - 0 - - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - - - - 0 - 0 - - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - - - - 0 - 0 - - - - -9999.000000000000000 - - - 9999.000000000000000 - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - - - - Loca&tion - - - - - - - Ob&ject: - - - - - - - Point to: - - - - - - - X: - - - location_x - - - - - - - Y: - - - location_y - - - - - - - Z: - - - location_z - - - - + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + false + + - - - buttonBox - accepted() - fso::fred::dialogs::ObjectOrientEditorDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - + From e9d4f322d0276e68d4b82283bd2d5ff24600c396 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 23 Aug 2025 09:57:35 -0500 Subject: [PATCH 403/466] QtFRED Waypoint Path Dialog (#6935) * cleanup qtfred waypoint dialog * range based loops * typo --- .../dialogs/WaypointEditorDialogModel.cpp | 425 ++++++------------ .../dialogs/WaypointEditorDialogModel.h | 54 +-- .../src/ui/dialogs/WaypointEditorDialog.cpp | 115 ++--- qtfred/src/ui/dialogs/WaypointEditorDialog.h | 39 +- qtfred/ui/WaypointEditorDialog.ui | 84 ++-- 5 files changed, 276 insertions(+), 441 deletions(-) diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp index f7787051b0a..42d8589c83c 100644 --- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.cpp @@ -1,365 +1,218 @@ -#include #include #include #include #include #include "mission/dialogs/WaypointEditorDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { WaypointEditorDialogModel::WaypointEditorDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { connect(viewport->editor, &Editor::currentObjectChanged, this, &WaypointEditorDialogModel::onSelectedObjectChanged); - connect(viewport->editor, - &Editor::objectMarkingChanged, - this, - &WaypointEditorDialogModel::onSelectedObjectMarkingChanged); - connect(viewport->editor, &Editor::missionChanged, this, &WaypointEditorDialogModel::missionChanged); + connect(viewport->editor, &Editor::objectMarkingChanged, this, &WaypointEditorDialogModel::onSelectedObjectMarkingChanged); + connect(viewport->editor, &Editor::missionChanged, this, &WaypointEditorDialogModel::onMissionChanged); initializeData(); } -bool WaypointEditorDialogModel::showErrorDialog(const SCP_string& message, const SCP_string& title) { - if (bypass_errors) { - return true; - } - bypass_errors = 1; - auto z = _viewport->dialogProvider->showButtonDialog(DialogType::Error, - title, - message, - { DialogButton::Ok, DialogButton::Cancel }); +bool WaypointEditorDialogModel::apply() +{ + if (!validateData()) { + return false; + } - if (z == DialogButton::Cancel) { - return true; + // apply name + char old_name[255]; + strcpy_s(old_name, _editor->cur_waypoint_list->get_name()); + const char* str = _currentName.c_str(); + _editor->cur_waypoint_list->set_name(str); + if (strcmp(old_name, str) != 0) { + update_sexp_references(old_name, str); + _editor->ai_update_goal_references(sexp_ref_type::WAYPOINT, old_name, str); + _editor->update_texture_replacements(old_name, str); // ?? Uh really? Check that FRED does this also } - return false; + _editor->missionChanged(); + return true; +} + +void WaypointEditorDialogModel::reject() +{ + // do nothing } -bool WaypointEditorDialogModel::apply() { - // Reset flag before applying - bypass_errors = false; - const char* str; - char old_name[255]; - int i; - object* ptr; +void WaypointEditorDialogModel::initializeData() +{ + _enabled = true; if (query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_WAYPOINT) { - Assert( - _editor->cur_waypoint_list == find_waypoint_list_with_instance(Objects[_editor->currentObject].instance)); + Assertion(_editor->cur_waypoint_list == find_waypoint_list_with_instance(Objects[_editor->currentObject].instance), "Waypoint no longer exists in the mission!"); } - if (_editor->cur_waypoint_list != NULL) { - for (i = 0; i < MAX_WINGS; i++) { - if (!stricmp(Wings[i].name, _currentName.c_str())) { - if (showErrorDialog("This waypoint path name is already being used by a wing\n" - "Press OK to restore old name", "Error")) { - return false; - } - - _currentName = _editor->cur_waypoint_list->get_name(); - modelChanged(); - } - } + updateWaypointPathList(); - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { - if (!stricmp(_currentName.c_str(), Ships[ptr->instance].ship_name)) { - if (showErrorDialog("This waypoint path name is already being used by a ship\n" - "Press OK to restore old name", "Error")) { - return false; - } - - _currentName = _editor->cur_waypoint_list->get_name(); - modelChanged(); - } - } + if (_editor->cur_waypoint_list != nullptr) { + _currentName = _editor->cur_waypoint_list->get_name(); + } else { + _currentName = ""; + _enabled = false; + } - ptr = GET_NEXT(ptr); - } + Q_EMIT waypointPathMarkingChanged(); +} - // We don't need to check teams. "Unknown" is a valid name and also an IFF. +void WaypointEditorDialogModel::updateWaypointPathList() +{ - for (i = 0; i < (int) Ai_tp_list.size(); i++) { - if (!stricmp(_currentName.c_str(), Ai_tp_list[i].name)) { - if (showErrorDialog("This waypoint path name is already being used by a target priority group.\n" - "Press OK to restore old name", "Error")) { - return false; - } + _waypointPathList.clear(); + _currentWaypointPathSelected = -1; - _currentName = _editor->cur_waypoint_list->get_name(); - modelChanged(); - } - } + for (size_t i = 0; i < Waypoint_lists.size(); ++i) { + _waypointPathList.emplace_back(Waypoint_lists[i].get_name(), static_cast(i)); + } - for (const auto &ii: Waypoint_lists) { - if (!stricmp(ii.get_name(), _currentName.c_str()) && (&ii != _editor->cur_waypoint_list)) { - if (showErrorDialog("This waypoint path name is already being used by another waypoint path\n" - "Press OK to restore old name", "Error")) { - return false; - } + if (_editor->cur_waypoint_list != nullptr) { + int index = find_index_of_waypoint_list(_editor->cur_waypoint_list); + Assertion(index >= 0, "Could not find waypoint path in waypoint path list!"); + _currentWaypointPathSelected = index; + } +} - _currentName = _editor->cur_waypoint_list->get_name(); - modelChanged(); - } - } +bool WaypointEditorDialogModel::validateData() +{ + // Reset flag before applying + _bypass_errors = false; - if (jumpnode_get_by_name(_currentName.c_str()) != NULL) { - if (showErrorDialog("This waypoint path name is already being used by a jump node\n" - "Press OK to restore old name", "Error")) { - return false; - } + if (query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_WAYPOINT) { + Assertion(_editor->cur_waypoint_list == find_waypoint_list_with_instance(Objects[_editor->currentObject].instance), "Waypoint no longer exists in the mission!"); + } - _currentName = _editor->cur_waypoint_list->get_name(); - modelChanged(); + // wing name collision + for (auto& wing : Wings) { + if (!stricmp(wing.name, _currentName.c_str())) { + showErrorDialogNoCancel("This waypoint path name is already being used by a wing"); + return false; } + } - if (_currentName[0] == '<') { - if (showErrorDialog("Waypoint names not allowed to begin with <\n" - "Press OK to restore old name", "Error")) { + // ship name collision + object* ptr = GET_FIRST(&obj_used_list); + while (ptr != END_OF_LIST(&obj_used_list)) { + if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { + if (!stricmp(_currentName.c_str(), Ships[ptr->instance].ship_name)) { + showErrorDialogNoCancel("This waypoint path name is already being used by a ship"); return false; } - - _currentName = _editor->cur_waypoint_list->get_name(); - modelChanged(); - } - - - strcpy_s(old_name, _editor->cur_waypoint_list->get_name()); - str = _currentName.c_str(); - _editor->cur_waypoint_list->set_name(str); - if (strcmp(old_name, str) != 0) { - modified = true; - update_sexp_references(old_name, str); - _editor->ai_update_goal_references(sexp_ref_type::WAYPOINT, old_name, str); - _editor->update_texture_replacements(old_name, str); - } - - _editor->missionChanged(); - } else if (Objects[_editor->currentObject].type == OBJ_JUMP_NODE) { - auto jnp = jumpnode_get_by_objnum(_editor->currentObject); - - for (i = 0; i < MAX_WINGS; i++) { - if (!stricmp(Wings[i].name, _currentName.c_str())) { - if (showErrorDialog("This jump node name is already being used by a wing\n" - "Press OK to restore old name", "Error")) { - return false; - } - - _currentName = jnp->GetName(); - modelChanged(); - } } - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if ((ptr->type == OBJ_SHIP) || (ptr->type == OBJ_START)) { - if (!stricmp(_currentName.c_str(), Ships[ptr->instance].ship_name)) { - if (showErrorDialog("This jump node name is already being used by a ship\n" - "Press OK to restore old name", "Error")) { - return false; - } - - _currentName = jnp->GetName(); - modelChanged(); - } - } - - ptr = GET_NEXT(ptr); - } - - // We don't need to check teams. "Unknown" is a valid name and also an IFF. - - for (i = 0; i < (int) Ai_tp_list.size(); i++) { - if (!stricmp(_currentName.c_str(), Ai_tp_list[i].name)) { - if (showErrorDialog("This jump node name is already being used by a target priority group.\n" - "Press OK to restore old name", "Error")) { - return false; - } - - _currentName = jnp->GetName(); - modelChanged(); - } - } + ptr = GET_NEXT(ptr); + } - if (find_matching_waypoint_list(_currentName.c_str()) != NULL) { - if (showErrorDialog("This jump node name is already being used by a waypoint path\n" - "Press OK to restore old name", "Error")) { - return false; - } + // We don't need to check teams. "Unknown" is a valid name and also an IFF. - _currentName = jnp->GetName(); - modelChanged(); + // target priority group name collision + for (auto& ai : Ai_tp_list) { + if (!stricmp(_currentName.c_str(), ai.name)) { + showErrorDialogNoCancel("This waypoint path name is already being used by a target priority group"); + return false; } + } - if (_currentName[0] == '<') { - if (showErrorDialog("Jump node names not allowed to begin with <\n" - "Press OK to restore old name", "Error")) { - return false; - } - - _currentName = jnp->GetName(); - modelChanged(); - } - - CJumpNode* found = jumpnode_get_by_name(_currentName.c_str()); - if (found != NULL && &(*jnp) != found) { - if (showErrorDialog("This jump node name is already being used by another jump node\n" - "Press OK to restore old name", "Error")) { - return false; - } - - _currentName = jnp->GetName(); - modelChanged(); + // waypoint path name collision + for (const auto& ii : Waypoint_lists) { + if (!stricmp(ii.get_name(), _currentName.c_str()) && (&ii != _editor->cur_waypoint_list)) { + showErrorDialogNoCancel("This waypoint path name is already being used by another waypoint path"); + return false; } + } - strcpy_s(old_name, jnp->GetName()); - jnp->SetName(_currentName.c_str()); - - str = _currentName.c_str(); - if (strcmp(old_name, str) != 0) { - update_sexp_references(old_name, str); - } + // jump node name collision + if (jumpnode_get_by_name(_currentName.c_str()) != nullptr) { + showErrorDialogNoCancel("This waypoint path name is already being used by a jump node"); + return false; + } - _editor->missionChanged(); + // formatting + if (!_currentName.empty() && _currentName[0] == '<') { + showErrorDialogNoCancel("Waypoint names not allowed to begin with '<'"); + return false; } return true; } -void WaypointEditorDialogModel::reject() { + +void WaypointEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) +{ + if (_bypass_errors) { + return; + } + + _bypass_errors = true; + _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Error", message, {DialogButton::Ok}); } + void WaypointEditorDialogModel::onSelectedObjectChanged(int) { initializeData(); } + void WaypointEditorDialogModel::onSelectedObjectMarkingChanged(int, bool) { initializeData(); } -void WaypointEditorDialogModel::initializeData() { - _enabled = true; - - updateElementList(); - - if (query_valid_object(_editor->currentObject) && Objects[_editor->currentObject].type == OBJ_WAYPOINT) { - Assert( - _editor->cur_waypoint_list == find_waypoint_list_with_instance(Objects[_editor->currentObject].instance)); - } - - if (_editor->cur_waypoint_list != NULL) { - _currentName = _editor->cur_waypoint_list->get_name(); - } else if (Objects[_editor->currentObject].type == OBJ_JUMP_NODE) { - auto jnp = jumpnode_get_by_objnum(_editor->currentObject); - _currentName = jnp ? jnp->GetName() : ""; - } else { - _currentName = ""; - _enabled = false; - } - modelChanged(); +void WaypointEditorDialogModel::onMissionChanged() +{ + // When the mission is changed we also need to update our data in case one of our elements changed + initializeData(); } + const SCP_string& WaypointEditorDialogModel::getCurrentName() const { return _currentName; } -int WaypointEditorDialogModel::getCurrentElementId() const { - return _currentElementId; -} -bool WaypointEditorDialogModel::isEnabled() const { - return _enabled; -} -const SCP_vector& WaypointEditorDialogModel::getElements() const { - return _elements; -} -void WaypointEditorDialogModel::updateElementList() { - int i; - SCP_vector::iterator ii; - SCP_list::iterator jnp; - - _elements.clear(); - _currentElementId = -1; - - for (i = 0, ii = Waypoint_lists.begin(); ii != Waypoint_lists.end(); ++i, ++ii) { - _elements.push_back(PointListElement(ii->get_name(), ID_WAYPOINT_MENU + i)); - } - - i = 0; - for (jnp = Jump_nodes.begin(); jnp != Jump_nodes.end(); ++jnp) { - _elements.push_back(PointListElement(jnp->GetName(), ID_JUMP_NODE_MENU + i)); - if (jnp->GetSCPObjectNumber() == _editor->currentObject) { - _currentElementId = ID_JUMP_NODE_MENU + i; - } - i++; - } +void WaypointEditorDialogModel::setCurrentName(const SCP_string& name) +{ + modify(_currentName, name); +} - if (_editor->cur_waypoint_list != NULL) { - int index = find_index_of_waypoint_list(_editor->cur_waypoint_list); - Assert(index >= 0); - _currentElementId = ID_WAYPOINT_MENU + index; - } +int WaypointEditorDialogModel::getCurrentlySelectedPath() const { + return _currentWaypointPathSelected; } -void WaypointEditorDialogModel::idSelected(int id) { - if (_currentElementId == id) { + +void WaypointEditorDialogModel::setCurrentlySelectedPath(int id) +{ + if (_currentWaypointPathSelected == id) { // Nothing to do here return; } - int point; - object* ptr; - - if ((id >= ID_WAYPOINT_MENU) && (id < ID_WAYPOINT_MENU + (int) Waypoint_lists.size())) { - if (apply()) { - point = id - ID_WAYPOINT_MENU; - _editor->unmark_all(); - ptr = GET_FIRST(&obj_used_list); - while (ptr != END_OF_LIST(&obj_used_list)) { - if (ptr->type == OBJ_WAYPOINT) { - if (calc_waypoint_list_index(ptr->instance) == point) { - _editor->markObject(OBJ_INDEX(ptr)); - } - } - - ptr = GET_NEXT(ptr); - } - - return; - } + if (id < 0 || id >= static_cast(Waypoint_lists.size())) { + return; // out of range; ignore } - if ((id >= ID_JUMP_NODE_MENU) && (id < ID_JUMP_NODE_MENU + (int) Jump_nodes.size())) { - if (apply()) { - point = id - ID_JUMP_NODE_MENU; - _editor->unmark_all(); - ptr = GET_FIRST(&obj_used_list); - while ((ptr != END_OF_LIST(&obj_used_list)) && (point > -1)) { - if (ptr->type == OBJ_JUMP_NODE) { - if (point == 0) { - _editor->markObject(OBJ_INDEX(ptr)); - } - point--; - } + if (apply()) { + _editor->unmark_all(); - ptr = GET_NEXT(ptr); + // mark all waypoints belonging to the selected list + int listIndex = id; + for (auto* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_WAYPOINT) { + if (calc_waypoint_list_index(ptr->instance) == listIndex) { + _editor->markObject(OBJ_INDEX(ptr)); + } } - - return; } + + _currentWaypointPathSelected = id; } } -void WaypointEditorDialogModel::setNameEditText(const SCP_string& name) { - _currentName = name; - modelChanged(); -} -void WaypointEditorDialogModel::missionChanged() { - // When the mission is changed we also need to update our data in case one of our elements changed - initializeData(); +bool WaypointEditorDialogModel::isEnabled() const { + return _enabled; } -WaypointEditorDialogModel::PointListElement::PointListElement(const SCP_string& in_name, int in_id) : - name(in_name), id(in_id) { -} -} -} +const SCP_vector>& WaypointEditorDialogModel::getWaypointPathList() const +{ + return _waypointPathList; } + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h index b547f2680cc..9f0a892d0cc 100644 --- a/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/WaypointEditorDialogModel.h @@ -1,61 +1,45 @@ #pragma once - #include "mission/dialogs/AbstractDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { class WaypointEditorDialogModel: public AbstractDialogModel { Q_OBJECT public: - struct PointListElement { - SCP_string name; - int id = -1; - - PointListElement(const SCP_string& name, int id); - }; - WaypointEditorDialogModel(QObject* parent, EditorViewport* viewport); bool apply() override; - void reject() override; - static const int ID_JUMP_NODE_MENU = 8000; - static const int ID_WAYPOINT_MENU = 9000; - const SCP_string& getCurrentName() const; - int getCurrentElementId() const; + void setCurrentName(const SCP_string& name); + int getCurrentlySelectedPath() const; + void setCurrentlySelectedPath(int elementId); + bool isEnabled() const; - const SCP_vector& getElements() const; + const SCP_vector>& getWaypointPathList() const; - void idSelected(int elementId); - void setNameEditText(const SCP_string& name); - - inline bool query_modified() const { return modified; } // TODO: needs handling in the waypoint dialog - - private: - bool showErrorDialog(const SCP_string& message, const SCP_string& title); +signals: + void waypointPathMarkingChanged(); + +private slots: void onSelectedObjectChanged(int); void onSelectedObjectMarkingChanged(int, bool); - void missionChanged(); - - void updateElementList(); + void onMissionChanged(); + private: // NOLINT(readability-redundant-access-specifiers) void initializeData(); + void updateWaypointPathList(); + bool validateData(); + void showErrorDialogNoCancel(const SCP_string& message); SCP_string _currentName; - int _currentElementId = -1; + int _currentWaypointPathSelected = -1; bool _enabled = false; - SCP_vector _elements; - - bool bypass_errors = false; - bool modified = false; + SCP_vector> _waypointPathList; + bool _bypass_errors = false; }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp b/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp index a3f1296ac09..f261eb182b9 100644 --- a/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/WaypointEditorDialog.cpp @@ -1,88 +1,89 @@ -#include -#include #include #include "ui/dialogs/WaypointEditorDialog.h" #include "ui/util/SignalBlockers.h" #include "ui_WaypointEditorDialog.h" -namespace fso { -namespace fred { -namespace dialogs { +#include + +namespace fso::fred::dialogs { WaypointEditorDialog::WaypointEditorDialog(FredView* parent, EditorViewport* viewport) : QDialog(parent), _viewport(viewport), - _editor(viewport->editor), ui(new Ui::WaypointEditorDialog()), - _model(new WaypointEditorDialogModel(this, viewport)) { + _model(new WaypointEditorDialogModel(this, viewport)) +{ + this->setFocus(); ui->setupUi(this); - connect(this, &QDialog::accepted, _model.get(), &WaypointEditorDialogModel::apply); - connect(this, &QDialog::rejected, _model.get(), &WaypointEditorDialogModel::reject); - - connect(parent, &FredView::viewWindowActivated, _model.get(), &WaypointEditorDialogModel::apply); - - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &WaypointEditorDialog::updateUI); - - connect(ui->pathSelection, - static_cast(&QComboBox::currentIndexChanged), - this, - &WaypointEditorDialog::pathSelectionChanged); + initializeUi(); + updateUi(); - connect(ui->nameEdit, &QLineEdit::textChanged, this, &WaypointEditorDialog::nameTextChanged); - - // Initial set up of the UI - updateUI(); + connect(_model.get(), &WaypointEditorDialogModel::waypointPathMarkingChanged, this, [this] { + initializeUi(); + updateUi(); + }); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); } -WaypointEditorDialog::~WaypointEditorDialog() { -} -void WaypointEditorDialog::pathSelectionChanged(int index) { - auto itemId = ui->pathSelection->itemData(index).value(); - _model->idSelected(itemId); -} -void WaypointEditorDialog::reject() { - // This dialog never rejects - accept(); + +WaypointEditorDialog::~WaypointEditorDialog() = default; + +void WaypointEditorDialog::initializeUi() +{ + util::SignalBlockers blockers(this); + + updateWaypointListComboBox(); + ui->nameEdit->setEnabled(_model->isEnabled()); } -void WaypointEditorDialog::updateComboBox() { - // Remove all previous entries + +void WaypointEditorDialog::updateWaypointListComboBox() +{ ui->pathSelection->clear(); - for (auto& el : _model->getElements()) { - ui->pathSelection->addItem(QString::fromStdString(el.name), QVariant(el.id)); + for (auto& wp : _model->getWaypointPathList()) { + ui->pathSelection->addItem(QString::fromStdString(wp.first), wp.second); } - auto itemIndex = ui->pathSelection->findData(QVariant(_model->getCurrentElementId())); - ui->pathSelection->setCurrentIndex(itemIndex); // This also works if the index is -1 - - ui->pathSelection->setEnabled(ui->pathSelection->count() > 0); + ui->pathSelection->setEnabled(!_model->getWaypointPathList().empty()); } -void WaypointEditorDialog::updateUI() { - util::SignalBlockers blockers(this); - - updateComboBox(); +void WaypointEditorDialog::updateUi() +{ + util::SignalBlockers blockers(this); ui->nameEdit->setText(QString::fromStdString(_model->getCurrentName())); - ui->nameEdit->setEnabled(_model->isEnabled()); + ui->pathSelection->setCurrentIndex(ui->pathSelection->findData(_model->getCurrentlySelectedPath())); } -void WaypointEditorDialog::nameTextChanged(const QString& newText) { - _model->setNameEditText(newText.toStdString()); + +void WaypointEditorDialog::on_pathSelection_currentIndexChanged(int index) +{ + auto itemId = ui->pathSelection->itemData(index).value(); + _model->setCurrentlySelectedPath(itemId); } -bool WaypointEditorDialog::event(QEvent* event) { - switch(event->type()) { - case QEvent::WindowDeactivate: - _model->apply(); - event->accept(); - return true; - default: - return QDialog::event(event); + +// This will run any time an edit is finished which includes the entire window closing, losing focus, +// the user clicking elsewhere in the dialog, or pressing Enter in the edit box. +// This is ok here because this is literally the only field that can be edited but if this dialog +// ever expands then it would be wise to change the whole thing to an ok/cancel type dialog. +void WaypointEditorDialog::on_nameEdit_editingFinished() +{ + // Waypoint editor applies immediately when the name is changed + // so save the current, try to apply, if fails, restore the current + // and update the text in the edit box + + SCP_string current = _model->getCurrentName(); + + SCP_string newText = ui->nameEdit->text().toUtf8().constData(); + _model->setCurrentName(newText); + + if (!_model->apply()) { + util::SignalBlockers blockers(this); + // If apply failed, restore the old name + ui->nameEdit->setText(QString::fromStdString(current)); + _model->setCurrentName(current); // Restore the model's current name } } -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/WaypointEditorDialog.h b/qtfred/src/ui/dialogs/WaypointEditorDialog.h index 3ca0b1a5882..d130b82a5cf 100644 --- a/qtfred/src/ui/dialogs/WaypointEditorDialog.h +++ b/qtfred/src/ui/dialogs/WaypointEditorDialog.h @@ -1,15 +1,9 @@ #pragma once - #include - #include #include -#include - -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class WaypointEditorDialog; @@ -21,30 +15,19 @@ class WaypointEditorDialog : public QDialog { WaypointEditorDialog(FredView* parent, EditorViewport* viewport); ~WaypointEditorDialog() override; - void reject() override; - - protected: - bool event(QEvent* event) override; - - private: - - void pathSelectionChanged(int index); - - void updateComboBox(); +private slots: + void on_pathSelection_currentIndexChanged(int index); + void on_nameEdit_editingFinished(); - void updateUI(); - - void nameTextChanged(const QString& newText); - - EditorViewport* _viewport = nullptr; - Editor* _editor = nullptr; - + private: // NOLINT(readability-redundant-access-specifiers) + EditorViewport* _viewport; std::unique_ptr ui; - std::unique_ptr _model; + + void initializeUi(); + void updateWaypointListComboBox(); + void updateUi(); }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/WaypointEditorDialog.ui b/qtfred/ui/WaypointEditorDialog.ui index c432d21329b..ccc1378daa0 100644 --- a/qtfred/ui/WaypointEditorDialog.ui +++ b/qtfred/ui/WaypointEditorDialog.ui @@ -6,49 +6,63 @@ 0 0 - 217 - 90 + 270 + 80 - Waypoint Path/Jump Node Editor + Waypoint Path Editor QLayout::SetFixedSize - - - - &Name - - - nameEdit - - - - - - - - - - Wa&ypoint Path - - - pathSelection - - - - - - - - 0 - 0 - - - + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Wa&ypoint Path + + + pathSelection + + + + + + + + 0 + 0 + + + + + + + + &Name + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + nameEdit + + + + + + + + + From 7d8b437847f42de6e36275a57cee2e43dd6cb9dc Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 23 Aug 2025 09:57:54 -0500 Subject: [PATCH 404/466] Qtfred Wing Editor Dialog (#6939) * wing editor dialog base functionality * flags and path masks plus some cleanup * squad logo selection * custom warp params using the ship editor version * some cleanup * some finalization * cleanup, fixes, and clang * remove duplicate lines after rebase * merge namespaces --- code/mission/missionhotkey.cpp | 3 +- code/mission/missionhotkey.h | 3 + code/mission/missionparse.cpp | 74 +- code/mission/missionparse.h | 6 + code/ship/ship_flags.h | 3 +- fred2/missionsave.cpp | 31 +- fred2/wing_editor.cpp | 4 +- qtfred/source_groups.cmake | 5 + qtfred/src/mission/Editor.h | 25 +- qtfred/src/mission/EditorWing.cpp | 142 +- .../ShipEditor/ShipCustomWarpDialogModel.cpp | 76 +- .../ShipEditor/ShipCustomWarpDialogModel.h | 56 +- .../ShipEditor/ShipEditorDialogModel.cpp | 4 +- .../ShipEditor/ShipEditorDialogModel.h | 2 +- .../mission/dialogs/WingEditorDialogModel.cpp | 1305 +++++++++++++++++ .../mission/dialogs/WingEditorDialogModel.h | 145 ++ qtfred/src/mission/missionsave.cpp | 31 +- qtfred/src/ui/FredView.cpp | 14 + qtfred/src/ui/FredView.h | 3 + .../ShipEditor/ShipCustomWarpDialog.cpp | 55 +- .../dialogs/ShipEditor/ShipCustomWarpDialog.h | 4 + qtfred/src/ui/dialogs/WingEditorDialog.cpp | 688 +++++++++ qtfred/src/ui/dialogs/WingEditorDialog.h | 100 ++ qtfred/ui/WingEditorDialog.ui | 890 +++++++++++ 24 files changed, 3550 insertions(+), 119 deletions(-) create mode 100644 qtfred/src/mission/dialogs/WingEditorDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/WingEditorDialogModel.h create mode 100644 qtfred/src/ui/dialogs/WingEditorDialog.cpp create mode 100644 qtfred/src/ui/dialogs/WingEditorDialog.h create mode 100644 qtfred/ui/WingEditorDialog.ui diff --git a/code/mission/missionhotkey.cpp b/code/mission/missionhotkey.cpp index 37f975acec4..18a73de7889 100644 --- a/code/mission/missionhotkey.cpp +++ b/code/mission/missionhotkey.cpp @@ -26,7 +26,6 @@ #include "mod_table/mod_table.h" #include "object/object.h" #include "parse/parselo.h" -#include "playerman/player.h" #include "ship/ship.h" #include "sound/audiostr.h" #include "ui/ui.h" @@ -34,7 +33,7 @@ #include "weapon/weapon.h" -static int Key_sets[MAX_KEYED_TARGETS] = { +int Key_sets[MAX_KEYED_TARGETS] = { KEY_F5, KEY_F6, KEY_F7, diff --git a/code/mission/missionhotkey.h b/code/mission/missionhotkey.h index 806812a9577..c2abed669c9 100644 --- a/code/mission/missionhotkey.h +++ b/code/mission/missionhotkey.h @@ -13,9 +13,12 @@ #define __MISSIONHOTKEY_H__ #include "globalincs/globals.h" +#include "playerman/player.h" #define MAX_LINES MAX_SHIPS // retail was 200, bump it to match MAX_SHIPS +extern int Key_sets[MAX_KEYED_TARGETS]; + // Types of items that can be in the hotkey list enum class HotkeyLineType { diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 5496313d613..b81c1d626d0 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -401,6 +401,35 @@ parse_object_flag_description Parse_object_flag_des const size_t Num_parse_object_flags = sizeof(Parse_object_flags) / sizeof(flag_def_list_new); +flag_def_list_new Parse_wing_flags[] = { + {"ignore-count", Ship::Wing_Flags::Ignore_count, true, false}, + {"reinforcement", Ship::Wing_Flags::Reinforcement, true, false}, + {"no-arrival-music", Ship::Wing_Flags::No_arrival_music, true, false}, + {"no-arrival-message", Ship::Wing_Flags::No_arrival_message, true, false}, + {"no-first-wave-message", Ship::Wing_Flags::No_first_wave_message, true, false}, + {"no-arrival-warp", Ship::Wing_Flags::No_arrival_warp, true, false}, + {"no-departure-warp", Ship::Wing_Flags::No_departure_warp, true, false}, + {"no-dynamic", Ship::Wing_Flags::No_dynamic, true, false}, + {"nav-carry-status", Ship::Wing_Flags::Nav_carry, true, false}, + {"same-arrival-warp-when-docked", Ship::Wing_Flags::Same_arrival_warp_when_docked, true, false}, + {"same-departure-warp-when-docked", Ship::Wing_Flags::Same_departure_warp_when_docked, true, false} +}; + +parse_object_flag_description Parse_wing_flag_descriptions[] = { + { Ship::Wing_Flags::Ignore_count, "Ignore this wing when counting ship types for goals." }, + { Ship::Wing_Flags::Reinforcement, "This wing is a reinforcement wing." }, + { Ship::Wing_Flags::No_arrival_music, "Don't play arrival music when wing arrives." }, + { Ship::Wing_Flags::No_arrival_message, "Don't play arrival message when wing arrives." }, + { Ship::Wing_Flags::No_first_wave_message, "Don't play the 'first wave' message when this is the first wing to arrive." }, + { Ship::Wing_Flags::No_arrival_warp, "No arrival warp-in effect." }, + { Ship::Wing_Flags::No_departure_warp, "No departure warp-in effect." }, + { Ship::Wing_Flags::No_dynamic, "Will stop allowing the AI to pursue dynamic goals (eg: chasing ships it was not ordered to)." }, + { Ship::Wing_Flags::Nav_carry, "Ships in this wing autopilot with the player." }, + { Ship::Wing_Flags::Same_arrival_warp_when_docked, "Docked ships use the same warp effect size upon arrival as if they were not docked instead of the enlarged aggregate size." }, + { Ship::Wing_Flags::Same_departure_warp_when_docked, "Docked ship use the same warp effect size upon departure as if they were not docked instead of the enlarged aggregate size." }}; + +const size_t Num_parse_wing_flags = sizeof(Parse_wing_flags) / sizeof(flag_def_list_new); + // These are only the flags that are saved to the mission file. See the MEF_ #defines. flag_def_list Mission_event_flags[] = { { "interval & delay use msecs", MEF_USE_MSECS, 0 }, @@ -4580,7 +4609,7 @@ void parse_wing(mission *pm) { int wingnum, i, wing_goals; char name[NAME_LENGTH], ship_names[MAX_SHIPS_PER_WING][NAME_LENGTH]; - char wing_flag_strings[PARSEABLE_WING_FLAGS][NAME_LENGTH]; + char wing_flag_strings[Num_parse_wing_flags][NAME_LENGTH]; wing *wingp; Assert(pm != NULL); @@ -4761,33 +4790,22 @@ void parse_wing(mission *pm) } if (optional_string("+Flags:")) { - auto count = (int) stuff_string_list(wing_flag_strings, PARSEABLE_WING_FLAGS); - - for (i = 0; i < count; i++) { - if (!stricmp(wing_flag_strings[i], NOX("ignore-count"))) - wingp->flags.set(Ship::Wing_Flags::Ignore_count); - else if (!stricmp(wing_flag_strings[i], NOX("reinforcement"))) - wingp->flags.set(Ship::Wing_Flags::Reinforcement); - else if (!stricmp(wing_flag_strings[i], NOX("no-arrival-music"))) - wingp->flags.set(Ship::Wing_Flags::No_arrival_music); - else if (!stricmp(wing_flag_strings[i], NOX("no-arrival-message"))) - wingp->flags.set(Ship::Wing_Flags::No_arrival_message); - else if (!stricmp(wing_flag_strings[i], NOX("no-first-wave-message"))) - wingp->flags.set(Ship::Wing_Flags::No_first_wave_message); - else if (!stricmp(wing_flag_strings[i], NOX("no-arrival-warp"))) - wingp->flags.set(Ship::Wing_Flags::No_arrival_warp); - else if (!stricmp(wing_flag_strings[i], NOX("no-departure-warp"))) - wingp->flags.set(Ship::Wing_Flags::No_departure_warp); - else if (!stricmp(wing_flag_strings[i], NOX("no-dynamic"))) - wingp->flags.set(Ship::Wing_Flags::No_dynamic); - else if (!stricmp(wing_flag_strings[i], NOX("nav-carry-status"))) - wingp->flags.set(Ship::Wing_Flags::Nav_carry); - else if (!stricmp(wing_flag_strings[i], NOX("same-arrival-warp-when-docked"))) - wingp->flags.set(Ship::Wing_Flags::Same_arrival_warp_when_docked); - else if (!stricmp(wing_flag_strings[i], NOX("same-departure-warp-when-docked"))) - wingp->flags.set(Ship::Wing_Flags::Same_departure_warp_when_docked); - else - Warning(LOCATION, "unknown wing flag\n%s\n\nSkipping.", wing_flag_strings[i]); + auto count = stuff_string_list(wing_flag_strings, Num_parse_wing_flags); + + for (size_t j = 0; j < count; j++) { + auto tok = wing_flag_strings[j]; + bool matched = false; + for (auto& Parse_wing_flag : Parse_wing_flags) { + if (!stricmp(tok, Parse_wing_flag.name)) { + wingp->flags.set(Parse_wing_flag.def); + matched = true; + break; + } + } + + if (!matched) { + Warning(LOCATION, "Unknown wing flag '%s', skipping!", tok); + } } } diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index 07ecbb85ed9..c9e4fa99eed 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -45,6 +45,9 @@ enum class DepartureLocation; #define SPECIAL_ARRIVAL_ANCHOR_FLAG 0x1000 #define SPECIAL_ARRIVAL_ANCHOR_PLAYER_FLAG 0x0100 +#define MIN_TARGET_ARRIVAL_DISTANCE 500.0f // float because that's how FRED does the math +#define MIN_TARGET_ARRIVAL_MULTIPLIER 2.0f // minimum distance is 2 * target radius, but at least 500 + int get_special_anchor(const char *name); // MISSION_VERSION should be the earliest version of FSO that can load the current mission format without @@ -291,6 +294,9 @@ extern char *Object_flags[]; extern flag_def_list_new Parse_object_flags[]; extern parse_object_flag_description Parse_object_flag_descriptions[]; extern const size_t Num_parse_object_flags; +extern flag_def_list_new Parse_wing_flags[]; +extern parse_object_flag_description Parse_wing_flag_descriptions[]; +extern const size_t Num_parse_wing_flags; extern const char *Icon_names[]; extern const char *Mission_event_log_flags[]; diff --git a/code/ship/ship_flags.h b/code/ship/ship_flags.h index 90307207a3a..5dc49062883 100644 --- a/code/ship/ship_flags.h +++ b/code/ship/ship_flags.h @@ -268,8 +268,7 @@ namespace Ship { // Not all wing flags are parseable or saveable in mission files. Right now, the only ones which can be set by mission designers are: // ignore_count, reinforcement, no_arrival_music, no_arrival_message, no_first_wave_message, no_arrival_warp, no_departure_warp, // same_arrival_warp_when_docked, same_departure_warp_when_docked, no_dynamic, and nav_carry_status - // Should that change, bump this variable and make sure to make the necessary changes to parse_wing (in missionparse) -#define PARSEABLE_WING_FLAGS 11 + // The list of parseable flags is in missionparse.cpp FLAG_LIST(Wing_Flags) { Gone, // all ships were either destroyed or departed diff --git a/fred2/missionsave.cpp b/fred2/missionsave.cpp index eaedf1ba06a..8c4b9f21c70 100644 --- a/fred2/missionsave.cpp +++ b/fred2/missionsave.cpp @@ -5194,30 +5194,39 @@ int CFred_mission_save::save_wings() } else fout("\n+Flags: ("); + auto get_flag_name = [](Ship::Wing_Flags flag) -> const char* { + for (size_t i = 0; i < Num_parse_wing_flags; ++i) { + if (Parse_wing_flags[i].def == flag) { + return Parse_wing_flags[i].name; + } + } + return nullptr; + }; + if (Wings[i].flags[Ship::Wing_Flags::Ignore_count]) - fout(" \"ignore-count\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Ignore_count)); if (Wings[i].flags[Ship::Wing_Flags::Reinforcement]) - fout(" \"reinforcement\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Reinforcement)); if (Wings[i].flags[Ship::Wing_Flags::No_arrival_music]) - fout(" \"no-arrival-music\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_arrival_music)); if (Wings[i].flags[Ship::Wing_Flags::No_arrival_message]) - fout(" \"no-arrival-message\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_arrival_message)); if (Wings[i].flags[Ship::Wing_Flags::No_first_wave_message]) - fout(" \"no-first-wave-message\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_first_wave_message)); if (Wings[i].flags[Ship::Wing_Flags::No_arrival_warp]) - fout(" \"no-arrival-warp\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_arrival_warp)); if (Wings[i].flags[Ship::Wing_Flags::No_departure_warp]) - fout(" \"no-departure-warp\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_departure_warp)); if (Wings[i].flags[Ship::Wing_Flags::No_dynamic]) - fout(" \"no-dynamic\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_dynamic)); if (Mission_save_format != FSO_FORMAT_RETAIL) { if (Wings[i].flags[Ship::Wing_Flags::Nav_carry]) - fout(" \"nav-carry-status\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Nav_carry)); if (Wings[i].flags[Ship::Wing_Flags::Same_arrival_warp_when_docked]) - fout(" \"same-arrival-warp-when-docked\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Same_arrival_warp_when_docked)); if (Wings[i].flags[Ship::Wing_Flags::Same_departure_warp_when_docked]) - fout(" \"same-departure-warp-when-docked\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Same_departure_warp_when_docked)); } fout(" )"); diff --git a/fred2/wing_editor.cpp b/fred2/wing_editor.cpp index b6dac0ed2c0..c29f26da94b 100644 --- a/fred2/wing_editor.cpp +++ b/fred2/wing_editor.cpp @@ -821,12 +821,12 @@ void wing_editor::update_data_safe() // when arriving near or in front of a ship, be sure that we are far enough away from it!!! if (((m_arrival_location != static_cast(ArrivalLocation::AT_LOCATION)) && (m_arrival_location != static_cast(ArrivalLocation::FROM_DOCK_BAY))) && (i >= 0) && !(i & SPECIAL_ARRIVAL_ANCHOR_FLAG)) { - d = int(std::min(500.0f, 2.0f * Objects[Ships[i].objnum].radius)); + d = int(std::min(MIN_TARGET_ARRIVAL_DISTANCE, MIN_TARGET_ARRIVAL_MULTIPLIER * Objects[Ships[i].objnum].radius)); if ((Wings[cur_wing].arrival_distance < d) && (Wings[cur_wing].arrival_distance > -d)) { if (!bypass_errors) { sprintf(buf, "Ship must arrive at least %d meters away from target.\n" "Value has been reset to this. Use with caution!\r\n" - "Recommended distance is %d meters.\r\n", d, (int)(2.0f * Objects[Ships[i].objnum].radius) ); + "Recommended distance is %d meters.\r\n", d, (int)(MIN_TARGET_ARRIVAL_MULTIPLIER * Objects[Ships[i].objnum].radius) ); MessageBox(buf); } diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index ad29365981c..1c723ad5fb4 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -74,6 +74,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/VariableDialogModel.h src/mission/dialogs/WaypointEditorDialogModel.cpp src/mission/dialogs/WaypointEditorDialogModel.h + src/mission/dialogs/WingEditorDialogModel.cpp + src/mission/dialogs/WingEditorDialogModel.h ) add_file_folder("Source/Mission/Dialogs/ShipEditor" src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h @@ -156,6 +158,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/VoiceActingManager.cpp src/ui/dialogs/WaypointEditorDialog.cpp src/ui/dialogs/WaypointEditorDialog.h + src/ui/dialogs/WingEditorDialog.cpp + src/ui/dialogs/WingEditorDialog.h ) add_file_folder("Source/UI/Dialogs/ShipEditor" src/ui/dialogs/ShipEditor/ShipEditorDialog.h @@ -257,6 +261,7 @@ add_file_folder("UI" ui/ShipAltShipClass.ui ui/ShipWeaponsDialog.ui ui/VariableDialog.ui + ui/WingEditorDialog.ui ) add_file_folder("Resources" diff --git a/qtfred/src/mission/Editor.h b/qtfred/src/mission/Editor.h index 3d767a6b940..730c6c4ae7b 100644 --- a/qtfred/src/mission/Editor.h +++ b/qtfred/src/mission/Editor.h @@ -21,6 +21,23 @@ namespace fso { namespace fred { +enum class WingNameError { + None, + Empty, + TooLong, + DuplicateWing, + DuplicateShip, + DuplicateTargetPriority, + DuplicateWaypointList, + DuplicateJumpNode, +}; + +struct WingNameCheck { + bool ok; + WingNameError error; + std::string message; // human-readable for dialogs +}; + /*! Game editor. * Handles everything needed to edit the game, * without any knowledge of the actual GUI framework stack. @@ -159,7 +176,13 @@ class Editor : public QObject { bool query_single_wing_marked(); - static bool wing_is_player_wing(int); + bool wing_is_player_wing(int); + + static bool wing_contains_player_start(int); + + static WingNameCheck validate_wing_name(const SCP_string& new_name, int ignore_wing = -1); + + bool rename_wing(int wing, const SCP_string& new_name, bool rename_members = true); /** * @brief Delete a whole wing, leaving ships intact but wingless. diff --git a/qtfred/src/mission/EditorWing.cpp b/qtfred/src/mission/EditorWing.cpp index 3e9ed610a09..4a1c8908e5b 100644 --- a/qtfred/src/mission/EditorWing.cpp +++ b/qtfred/src/mission/EditorWing.cpp @@ -430,24 +430,148 @@ bool Editor::query_single_wing_marked() bool Editor::wing_is_player_wing(int wing) { - int i; - if (wing < 0) return false; - if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { - for (i = 0; i < MAX_TVT_WINGS; i++) { - if (wing == TVT_wings[i]) - return true; + // Multiplayer wing check + if (The_mission.game_type & MISSION_TYPE_MULTI) { + if (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) { + for (int twing : TVT_wings) { + if (wing == twing) + return true; + } + } else { + for (int swing : Starting_wings) { + if (wing == swing) + return true; + } } + // Single player wing check } else { - for (i = 0; i < MAX_STARTING_WINGS; i++) { - if (wing == Starting_wings[i]) - return true; + if (Player_start_shipnum >= 0 && Player_start_shipnum < MAX_SHIPS) { + const int pw = Ships[Player_start_shipnum].wingnum; + return pw >= 0 && pw == wing; } } return false; } + +bool Editor::wing_contains_player_start(int wing) +{ + return wing >= 0 && Player_start_shipnum >= 0 && Player_start_shipnum < MAX_SHIPS && + Ships[Player_start_shipnum].objnum >= 0 && Ships[Player_start_shipnum].wingnum == wing; +} + +WingNameCheck Editor::validate_wing_name(const SCP_string& new_name, int ignore_wing) +{ + WingNameCheck r{false, WingNameError::None, {}}; + if (new_name.empty()) { + r.error = WingNameError::Empty; + r.message = "Name is empty."; + return r; + } + + if (new_name.empty()) { + r.error = WingNameError::Empty; + r.message = "Name is empty."; + return r; + } + if (new_name.size() >= NAME_LENGTH) { + r.error = WingNameError::TooLong; + r.message = "Name is too long."; + return r; + } + + // Other wings + for (int i = 0; i < MAX_WINGS; ++i) { + if (i == ignore_wing) + continue; + if (Wings[i].wave_count <= 0) + continue; + if (!stricmp(new_name.c_str(), Wings[i].name)) { + r.error = WingNameError::DuplicateWing; + r.message = "This wing name is already used by another wing."; + return r; + } + } + + // Ships + for (object* ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + if (ptr->type == OBJ_SHIP || ptr->type == OBJ_START) { + const int si = get_ship_from_obj(ptr); + if (!stricmp(new_name.c_str(), Ships[si].ship_name)) { + r.error = WingNameError::DuplicateShip; + r.message = "This wing name is already used by a ship."; + return r; + } + } + } + + // Target priority groups + for (auto& ai : Ai_tp_list) { + if (!stricmp(new_name.c_str(), ai.name)) { + r.error = WingNameError::DuplicateTargetPriority; + r.message = "This wing name is already used by a target priority group."; + return r; + } + } + + // Waypoint paths + if (find_matching_waypoint_list(new_name.c_str()) != nullptr) { + r.error = WingNameError::DuplicateWaypointList; + r.message = "This wing name is already used by a waypoint path."; + return r; + } + + // Jump nodes + if (jumpnode_get_by_name(new_name.c_str()) != nullptr) { + r.error = WingNameError::DuplicateJumpNode; + r.message = "This wing name is already used by a jump node."; + return r; + } + + r.ok = true; + r.message.clear(); + return r; +} + +bool Editor::rename_wing(int wing, const SCP_string& new_name, bool rename_members) +{ + if (wing < 0 || wing >= MAX_WINGS) + return false; + if (Wings[wing].wave_count <= 0) + return false; + + auto check = validate_wing_name(new_name, wing); + if (!check.ok) + return false; + + char old_name[NAME_LENGTH]; + strncpy(old_name, Wings[wing].name, NAME_LENGTH - 1); + old_name[NAME_LENGTH - 1] = '\0'; + + strncpy(Wings[wing].name, new_name.c_str(), NAME_LENGTH - 1); + Wings[wing].name[NAME_LENGTH - 1] = '\0'; + + if (rename_members) { + for (int i = 0; i < Wings[wing].wave_count; ++i) { + const int ship_idx = Wings[wing].ship_index[i]; + if (ship_idx < 0 || ship_idx >= MAX_SHIPS) + continue; + char buf[NAME_LENGTH]; + wing_bash_ship_name(buf, Wings[wing].name, i + 1); + rename_ship(ship_idx, buf); + } + } + + ai_update_goal_references(sexp_ref_type::WING, old_name, Wings[wing].name); + update_custom_wing_indexes(); + + missionChanged(); + updateAllViewports(); + return true; +} + } // namespace fred } // namespace fso diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp index 7766a6c3cdc..332071971f3 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp @@ -3,7 +3,17 @@ #include "ship/shipfx.h" namespace fso::fred::dialogs { ShipCustomWarpDialogModel::ShipCustomWarpDialogModel(QObject* parent, EditorViewport* viewport, bool departure) - : AbstractDialogModel(parent, viewport), _m_departure(departure) + : AbstractDialogModel(parent, viewport), _m_departure(departure), _target(Target::Selection) +{ + initializeData(); +} + +ShipCustomWarpDialogModel::ShipCustomWarpDialogModel(QObject* parent, + EditorViewport* viewport, + bool departure, + Target target, + int wingIndex) + : AbstractDialogModel(parent, viewport), _m_departure(departure), _target(target), _wingIndex(wingIndex) { initializeData(); } @@ -50,7 +60,7 @@ bool ShipCustomWarpDialogModel::apply() params.accel_exp = _m_accel_exp; } if (_m_radius) { - params.accel_exp = _m_radius; + params.radius = _m_radius; } if (!_m_anim.empty()) { strcpy_s(params.anim, _m_anim.c_str()); @@ -61,13 +71,28 @@ bool ShipCustomWarpDialogModel::apply() } int index = find_or_add_warp_params(params); - for (object* objp : list_range(&obj_used_list)) { - if ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) { - if (objp->flags[Object::Object_Flags::Marked]) { + if (_target == Target::Wing && _wingIndex >= 0) { + for (object* objp : list_range(&obj_used_list)) { + if ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) { + auto& sh = Ships[objp->instance]; + if (sh.wingnum != _wingIndex) + continue; if (!_m_departure) - Ships[objp->instance].warpin_params_index = index; + sh.warpin_params_index = index; else - Ships[objp->instance].warpout_params_index = index; + sh.warpout_params_index = index; + } + } + } else { + for (object* objp : list_range(&obj_used_list)) { + if ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) { + if (objp->flags[Object::Object_Flags::Marked]) { + auto& sh = Ships[objp->instance]; + if (!_m_departure) + sh.warpin_params_index = index; + else + sh.warpout_params_index = index; + } } } } @@ -146,18 +171,35 @@ void ShipCustomWarpDialogModel::initializeData() { // find the params of the first marked ship WarpParams* params = nullptr; - for (object* objp : list_range(&obj_used_list)) { - if ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) { - if (objp->flags[Object::Object_Flags::Marked]) { - if (!_m_departure) { - params = &Warp_params[Ships[objp->instance].warpin_params_index]; - } else { - params = &Warp_params[Ships[objp->instance].warpout_params_index]; + if (_target == Target::Wing && _wingIndex >= 0) { + // Use first ship in the wing for initial values; mark _m_player if the wing contains the player + for (object* objp : list_range(&obj_used_list)) { + if ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) { + const auto& sh = Ships[objp->instance]; + if (sh.wingnum == _wingIndex) { + if (!_m_departure) + params = &Warp_params[sh.warpin_params_index]; + else + params = &Warp_params[sh.warpout_params_index]; + if (objp->type == OBJ_START) + _m_player = true; + break; } - if (objp->type == OBJ_START) { - _m_player = true; + } + } + } else { + for (object* objp : list_range(&obj_used_list)) { + if ((objp->type == OBJ_SHIP) || (objp->type == OBJ_START)) { + if (objp->flags[Object::Object_Flags::Marked]) { + const auto& sh = Ships[objp->instance]; + if (!_m_departure) + params = &Warp_params[sh.warpin_params_index]; + else + params = &Warp_params[sh.warpout_params_index]; + if (objp->type == OBJ_START) + _m_player = true; + break; } - break; } } } diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h index 1b94dc3a295..7aa3fe90fc1 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.h @@ -5,31 +5,11 @@ namespace fso::fred::dialogs { * @brief Model for QtFRED's Custom warp dialog */ class ShipCustomWarpDialogModel : public AbstractDialogModel { - private: - /** - * @brief Initialises data for the model - */ - void initializeData(); - bool _m_departure; - - int _m_warp_type; - SCP_string _m_start_sound; - SCP_string _m_end_sound; - float _m_warpout_engage_time; - float _m_speed; - float _m_time; - float _m_accel_exp; - float _m_radius; - SCP_string _m_anim; - bool _m_supercap_warp_physics; - float _m_player_warpout_speed; - - bool _m_player = false; - /** - * @brief Marks the model as modifed - */ - public: + enum class Target { + Selection, + Wing + }; /** * @brief Constructor * @param [in] parent The parent dialog. @@ -37,6 +17,7 @@ class ShipCustomWarpDialogModel : public AbstractDialogModel { * @param [in] departure Whether the dialog is changeing warp-in or warp-out. */ ShipCustomWarpDialogModel(QObject* parent, EditorViewport* viewport, bool departure); + ShipCustomWarpDialogModel(QObject* parent, EditorViewport* viewport, bool departure, Target target, int wingIndex); bool apply() override; void reject() override; @@ -119,6 +100,33 @@ class ShipCustomWarpDialogModel : public AbstractDialogModel { void setAnim(const SCP_string&); void setSupercap(const bool); void setPlayerSpeed(const double); + + private: + /** + * @brief Initialises data for the model + */ + void initializeData(); + bool _m_departure; + + int _m_warp_type; + SCP_string _m_start_sound; + SCP_string _m_end_sound; + float _m_warpout_engage_time; + float _m_speed; + float _m_time; + float _m_accel_exp; + float _m_radius; + SCP_string _m_anim; + bool _m_supercap_warp_physics; + float _m_player_warpout_speed; + + bool _m_player = false; + Target _target = Target::Selection; + int _wingIndex = -1; + + /** + * @brief Marks the model as modifed + */ }; } // namespace dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp index f0e55ac9d2d..89f398e221a 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp @@ -1361,9 +1361,9 @@ namespace fso { } } - bool ShipEditorDialogModel::wing_is_player_wing(const int wing) + bool ShipEditorDialogModel::wing_is_player_wing(const int wing) const { - return Editor::wing_is_player_wing(wing); + return _editor->wing_is_player_wing(wing); } const std::set &ShipEditorDialogModel::getShipOrders() const diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h index 77fab5574d4..2c6c252c06d 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h @@ -181,7 +181,7 @@ class ShipEditorDialogModel : public AbstractDialogModel { * @brief Returns true if the wing is a player wing * @param wing Takes an integer id of the wing */ - static bool wing_is_player_wing(const int); + bool wing_is_player_wing(const int) const; const std::set &getShipOrders() const; bool getTexEditEnable() const; diff --git a/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp b/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp new file mode 100644 index 00000000000..246e6f1eafb --- /dev/null +++ b/qtfred/src/mission/dialogs/WingEditorDialogModel.cpp @@ -0,0 +1,1305 @@ +#include "WingEditorDialogModel.h" +#include "FredApplication.h" +#include +#include "iff_defs/iff_defs.h" +#include "mission/missionhotkey.h" +#include "mission/missionparse.h" +#include +#include + +namespace fso::fred::dialogs { +WingEditorDialogModel::WingEditorDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + reloadFromCurWing(); + prepareSquadLogoList(); + + connect(_editor, &Editor::currentObjectChanged, this, &WingEditorDialogModel::onEditorSelectionChanged); + connect(_editor, &Editor::missionChanged, this, &WingEditorDialogModel::onEditorMissionChanged); +} + +void WingEditorDialogModel::onEditorSelectionChanged(int) +{ + reloadFromCurWing(); +} + +void WingEditorDialogModel::onEditorMissionChanged() +{ + reloadFromCurWing(); +} + +void WingEditorDialogModel::reloadFromCurWing() +{ + int w = _editor->cur_wing; + + if (w == _currentWingIndex) + return; // no change + + _currentWingIndex = w; + + if (w < 0 || Wings[w].wave_count == 0) { + // No wing selected + modify(_currentWingIndex, -1); + modify(_currentWingName, SCP_string()); + return; + } + + const auto& wing = Wings[w]; + modify(_currentWingIndex, w); + modify(_currentWingName, SCP_string(wing.name)); + + Q_EMIT wingChanged(); +} + +bool WingEditorDialogModel::wingIsValid() const +{ + return _currentWingIndex >= 0 && _currentWingIndex < MAX_WINGS && Wings[_currentWingIndex].wave_count > 0; +} + +wing* WingEditorDialogModel::getCurrentWing() const +{ + if (!wingIsValid()) { + return nullptr; + } + return &Wings[_currentWingIndex]; +} + +std::vector> WingEditorDialogModel::getDockBayPathsForWingMask(uint32_t mask, int anchorShipnum) +{ + std::vector> out; + + if (anchorShipnum < 0 || !ship_has_dock_bay(anchorShipnum)) + return out; + + const int sii = Ships[anchorShipnum].ship_info_index; + const int model_num = Ship_info[sii].model_num; + auto* pm = model_get(model_num); + if (!pm || !pm->ship_bay) + return out; + + const int num_paths = pm->ship_bay->num_paths; + const auto* idx = pm->ship_bay->path_indexes; + + const bool all_allowed = (mask == 0); + out.reserve(static_cast(num_paths)); + + for (int i = 0; i < num_paths; ++i) { + const int path_id = idx[i]; + const char* name = pm->paths[path_id].name; + const bool allowed = all_allowed ? true : ((mask & (1u << i)) != 0); + out.emplace_back(name ? SCP_string{name} : SCP_string{""}, allowed); + } + + return out; +} + +void WingEditorDialogModel::prepareSquadLogoList() +{ + pilot_load_squad_pic_list(); + + for (int i = 0; i < Num_pilot_squad_images; i++) { + squadLogoList.emplace_back(Pilot_squad_image_names[i]); + } +} + +bool WingEditorDialogModel::isPlayerWing() const +{ + if (!wingIsValid()) { + return false; + } + + return _editor->wing_is_player_wing(_currentWingIndex); +} + +bool WingEditorDialogModel::containsPlayerStart() const +{ + if (!wingIsValid()) { + return false; + } + + return Editor::wing_contains_player_start(_currentWingIndex); +} + +bool WingEditorDialogModel::wingAllFighterBombers() const +{ + const auto w = getCurrentWing(); + + if (!w) + return false; + + for (int i = 0; i < w->wave_count; ++i) { + const int si = w->ship_index[i]; + if (si < 0 || si >= MAX_SHIPS) + return false; + const int sclass = Ships[si].ship_info_index; + if (!SCP_vector_inbounds(Ship_info, sclass)) + return false; + if (!Ship_info[sclass].is_fighter_bomber()) + return false; + } + return true; +} + +bool WingEditorDialogModel::arrivalIsDockBay() const +{ + const auto w = getCurrentWing(); + + if (!w) + return false; + + switch (w->arrival_location) { + case ArrivalLocation::FROM_DOCK_BAY: + return true; + default: + return false; + } +} + +bool WingEditorDialogModel::arrivalNeedsTarget() const +{ + const auto w = getCurrentWing(); + + if (!w) + return false; + + switch (w->arrival_location) { + case ArrivalLocation::AT_LOCATION: + return false; + default: + return true; + } +} + + +bool WingEditorDialogModel::departureIsDockBay() const +{ + const auto w = getCurrentWing(); + + if (!w) + return false; + + switch (w->departure_location) { + case DepartureLocation::TO_DOCK_BAY: + return true; + default: + return false; + } +} + +bool WingEditorDialogModel::departureNeedsTarget() const +{ + const auto w = getCurrentWing(); + + if (!w) + return false; + + switch (w->departure_location) { + case DepartureLocation::AT_LOCATION: + return false; + default: + return true; + } +} + +int WingEditorDialogModel::getMaxWaveThreshold() const +{ + if (!wingIsValid()) + return 0; + + const auto w = getCurrentWing(); + if (!w) + return 0; + + const int perWaveMax = w->wave_count - 1; + const int poolLimit = MAX_SHIPS_PER_WING - w->wave_count; + return std::max(0, std::min(perWaveMax, poolLimit)); +} + +int WingEditorDialogModel::getMinArrivalDistance() const +{ + if (!wingIsValid()) + return 0; + + const auto w = getCurrentWing(); + if (!w) + return 0; + + switch (w->arrival_location) { + case ArrivalLocation::AT_LOCATION: + case ArrivalLocation::FROM_DOCK_BAY: + return 0; + default: + break; + } + + const int anchor = w->arrival_anchor; + + // If special anchor or invalid, no radius to enforce + if (anchor < 0 || (anchor & SPECIAL_ARRIVAL_ANCHOR_FLAG)) + return 0; + + // Anchor should be a real ship + if (anchor >= 0 && anchor < MAX_SHIPS) { + const int objnum = Ships[anchor].objnum; + if (objnum >= 0) { + const object& obj = Objects[objnum]; + + // Enforce at least min(500, 2.0 * target_radius) + float min_rad = std::round(MIN_TARGET_ARRIVAL_MULTIPLIER * obj.radius); + return std::min(MIN_TARGET_ARRIVAL_DISTANCE, min_rad); + } + } + + return 0; +} + +std::pair> WingEditorDialogModel::getLeaderList() const +{ + std::pair> items; + if (!wingIsValid()) + return items; + + auto w = getCurrentWing(); + + items.first = w->special_ship; + for (int x = 0; x < w->wave_count; ++x) { + int si = w->ship_index[x]; + if (si >= 0 && si < MAX_SHIPS) { + items.second.emplace_back(Ships[si].ship_name); + } + } + return items; +} + +std::vector> WingEditorDialogModel::getHotkeyList() +{ + std::vector> items; + items.emplace_back(-1, "None"); + + for (int i = 0; i < MAX_KEYED_TARGETS; ++i) { + auto key = textify_scancode(Key_sets[i]); + SCP_string key_str = "Set " + std::to_string(i + 1) + " (" + key + ")"; + items.emplace_back(i, key_str); + } + + items.emplace_back(MAX_KEYED_TARGETS, "Hidden"); + + return items; +} + +std::vector> WingEditorDialogModel::getFormationList() +{ + std::vector> items; + items.emplace_back(-1, "Default"); + + for (int i = 0; i < static_cast(Wing_formations.size()); i++) { + items.emplace_back(i, Wing_formations[i].name); + } + + return items; +} + +std::vector> WingEditorDialogModel::getArrivalLocationList() +{ + std::vector> items; + items.reserve(MAX_ARRIVAL_NAMES); + for (int i = 0; i < MAX_ARRIVAL_NAMES; i++) { + items.emplace_back(i, Arrival_location_names[i]); + } + return items; +} + +std::vector> WingEditorDialogModel::getDepartureLocationList() +{ + std::vector> items; + items.reserve(MAX_DEPARTURE_NAMES); + for (int i = 0; i < MAX_DEPARTURE_NAMES; i++) { + items.emplace_back(i, Departure_location_names[i]); + } + return items; +} + +static bool shipHasDockBay(int ship_info_index) +{ + if (ship_info_index < 0 || ship_info_index >= (int)::Ship_info.size()) + return false; + auto mn = Ship_info[ship_info_index].model_num; + if (mn < 0) + return false; + auto pm = model_get(mn); + return pm && pm->ship_bay && pm->ship_bay->num_paths > 0; +} + +std::vector> WingEditorDialogModel::getArrivalTargetList() const +{ + std::vector> items; + const auto* w = getCurrentWing(); + if (!w) + return items; + + // No target needed for free-space arrival + if (w->arrival_location == ArrivalLocation::AT_LOCATION) + return items; + + const bool requireDockBay = (w->arrival_location == ArrivalLocation::FROM_DOCK_BAY); + + // Add special anchors (Any friendly/hostile/etc); both all ships and players only variants + if (!requireDockBay) { + char buf[NAME_LENGTH + 15]; + for (int restrict_to_players = 0; restrict_to_players < 2; ++restrict_to_players) { + for (int iff = 0; iff < (int)::Iff_info.size(); ++iff) { + stuff_special_arrival_anchor_name(buf, iff, restrict_to_players, 0); + items.emplace_back(get_special_anchor(buf), buf); + } + } + } + + // Add ships and player starts that are NOT currently marked + for (object* objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp)) { + if ((objp->type != OBJ_SHIP && objp->type != OBJ_START) || objp->flags[Object::Object_Flags::Marked]) { + continue; + } + + const int ship_idx = objp->instance; + const int sclass = Ships[ship_idx].ship_info_index; + + if (requireDockBay && !shipHasDockBay(sclass)) + continue; + + items.emplace_back(ship_idx, Ships[ship_idx].ship_name); + } + + return items; +} + +std::vector> WingEditorDialogModel::getDepartureTargetList() const +{ + std::vector> items; + const auto* w = getCurrentWing(); + if (!w) + return items; + + // Only dockbay departures need a specific target + if (w->departure_location != DepartureLocation::TO_DOCK_BAY) + return items; + + for (object* objp = GET_FIRST(&obj_used_list); objp != END_OF_LIST(&obj_used_list); objp = GET_NEXT(objp)) { + if ((objp->type != OBJ_SHIP && objp->type != OBJ_START) || objp->flags[Object::Object_Flags::Marked]) { + continue; + } + + const int ship_idx = objp->instance; + const int sclass = Ships[ship_idx].ship_info_index; + + if (!shipHasDockBay(sclass)) + continue; + + items.emplace_back(ship_idx, Ships[ship_idx].ship_name); + } + + return items; +} + +SCP_string WingEditorDialogModel::getWingName() const +{ + if (!wingIsValid()) + return ""; + + return _currentWingName; +} + +void WingEditorDialogModel::setWingName(const SCP_string& name) +{ + if (!wingIsValid()) + return; + + if (_editor->rename_wing(_currentWingIndex, name)) { + modify(_currentWingName, name); + Q_EMIT modelChanged(); + } +} + +int WingEditorDialogModel::getWingLeaderIndex() const +{ + if (!wingIsValid()) + return -1; + + const auto w = getCurrentWing(); + int idx = w->special_ship; + + return (idx >= 0 && idx < w->wave_count) ? idx : -1; +} + +void WingEditorDialogModel::setWingLeaderIndex(int idx) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + + modify(w->special_ship, idx); +} + +int WingEditorDialogModel::getNumberOfWaves() const +{ + if (!wingIsValid()) + return 1; + + const auto w = getCurrentWing(); + int num = w->num_waves; + + return num; +} + +void WingEditorDialogModel::setNumberOfWaves(int num) +{ + if (!wingIsValid()) + return; + + // you read that right, I don't see a limit for the number of waves. + // Original Fred had a UI limit of 99, but yolo + if (num < 1) { + num = 1; + } + auto* w = getCurrentWing(); + + modify(w->num_waves, num); +} + +int WingEditorDialogModel::getWaveThreshold() const +{ + if (!wingIsValid()) + return 0; + + const auto w = getCurrentWing(); + int thr = w->threshold; + + return thr; +} + +void WingEditorDialogModel::setWaveThreshold(int newThreshold) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + + modify(w->threshold, std::clamp(newThreshold, 0, getMaxWaveThreshold())); +} + +int WingEditorDialogModel::getHotkey() const +{ + if (!wingIsValid()) + return -1; + + const auto w = getCurrentWing(); + int idx = w->hotkey; + + return idx; +} + +void WingEditorDialogModel::setHotkey(int newHotkeyIndex) +{ + if (!wingIsValid()) + return; + + // Valid values: + // -1 = None + // 0..MAX_KEYED_TARGETS-1 = Sets 1..N + // MAX_KEYED_TARGETS = Hidden + if (newHotkeyIndex < -1 || newHotkeyIndex > MAX_KEYED_TARGETS) { + newHotkeyIndex = -1; // ignore bad input; treat as None + } + + auto* w = getCurrentWing(); + modify(w->hotkey, newHotkeyIndex); +} + +int WingEditorDialogModel::getFormationId() const +{ + if (!wingIsValid()) + return -1; + + const auto w = getCurrentWing(); + int id = w->formation; + + return id; +} + +void WingEditorDialogModel::setFormationId(int newFormationId) +{ + if (!wingIsValid()) + return; + auto* w = getCurrentWing(); + + if (!SCP_vector_inbounds(Wing_formations, newFormationId)) { + newFormationId = 0; // ignore bad input; treat as Default + } + + modify(w->formation, newFormationId); +} + +float WingEditorDialogModel::getFormationScale() const +{ + if (!wingIsValid()) + return 1.0f; + + const auto w = getCurrentWing(); + float scale = w->formation_scale; + + return scale; +} + +void WingEditorDialogModel::setFormationScale(float newScale) +{ + if (!wingIsValid()) + return; + auto* w = getCurrentWing(); + + if (newScale < 0.0f) { + newScale = 0.0f; // Unsure if formation scale has a minimum value + } + + modify(w->formation_scale, newScale); +} + +void WingEditorDialogModel::alignWingFormation() +{ + if (!wingIsValid()) + return; + + auto wingp = getCurrentWing(); + auto leader_objp = &Objects[Ships[wingp->ship_index[0]].objnum]; + + // TODO Handle this when the dialog supports temporary changes in the future + //make all changes to the model temporary and only apply them on close/next/previous + //auto old_formation = wingp->formation; + //auto old_formation_scale = wingp->formation_scale; + + //wingp->formation = m_formation - 1; + //wingp->formation_scale = (float)atof(m_formation_scale); + + for (int i = 1; i < wingp->wave_count; i++) { + auto objp = &Objects[Ships[wingp->ship_index[i]].objnum]; + + get_absolute_wing_pos(&objp->pos, leader_objp, _currentWingIndex, i, false); + objp->orient = leader_objp->orient; + } + + // roll back temporary formation + //wingp->formation = old_formation; + //wingp->formation_scale = old_formation_scale; + + _editor->updateAllViewports(); +} + +SCP_string WingEditorDialogModel::getSquadLogo() const +{ + if (!wingIsValid()) + return ""; + + const auto w = getCurrentWing(); + + return w->wing_squad_filename; +} + +void WingEditorDialogModel::setSquadLogo(const SCP_string& filename) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + + if (filename.size() >= TOKEN_LENGTH) { + return; + } + + strcpy_s(w->wing_squad_filename, filename.c_str()); + set_modified(); +} + +void WingEditorDialogModel::selectPreviousWing() +{ + int begin = (_currentWingIndex >= 0 && _currentWingIndex < MAX_WINGS) ? _currentWingIndex : 0; + int prv = -1; + + for (int step = 1; step <= MAX_WINGS; ++step) { + // add MAX_WINGS before modulo to avoid negative + int i = (begin - step + MAX_WINGS) % MAX_WINGS; + if (Wings[i].wave_count > 0 && Wings[i].name[0] != '\0') { + prv = i; + break; + } + } + + if (prv < 0) { + return; // no other wings + } + + _editor->unmark_all(); + _editor->mark_wing(prv); + reloadFromCurWing(); +} + +void WingEditorDialogModel::selectNextWing() +{ + int begin = (_currentWingIndex >= 0 && _currentWingIndex < MAX_WINGS) ? _currentWingIndex : -1; + int nxt = -1; + + for (int step = 1; step <= MAX_WINGS; ++step) { + // add MAX_WINGS before modulo to avoid negative + int i = (begin + step + MAX_WINGS) % MAX_WINGS; + if (Wings[i].wave_count > 0 && Wings[i].name[0] != '\0') { + nxt = i; + break; + } + } + + if (nxt < 0) { + return; // no other wings + } + + _editor->unmark_all(); + _editor->mark_wing(nxt); + reloadFromCurWing(); +} + +void WingEditorDialogModel::deleteCurrentWing() +{ + if (!wingIsValid()) + return; + + _editor->delete_wing(_currentWingIndex); + reloadFromCurWing(); +} + +void WingEditorDialogModel::disbandCurrentWing() +{ + if (!wingIsValid()) + return; + + _editor->remove_wing(_currentWingIndex); + reloadFromCurWing(); +} + +std::vector> WingEditorDialogModel::getWingFlags() const +{ + std::vector> flags; + if (!wingIsValid()) + return flags; + + const auto* w = getCurrentWing(); + + for (size_t i = 0; i < Num_parse_wing_flags; ++i) { + auto flagDef = Parse_wing_flags[i]; + + // Skip flags that have checkboxes elsewhere in the dialog + if (flagDef.def == Ship::Wing_Flags::No_arrival_warp || flagDef.def == Ship::Wing_Flags::No_departure_warp || + flagDef.def == Ship::Wing_Flags::Same_arrival_warp_when_docked || + flagDef.def == Ship::Wing_Flags::Same_departure_warp_when_docked) { + continue; + } + + bool checked = w->flags[flagDef.def]; + flags.emplace_back(flagDef.name, checked); + } + + return flags; +} + +void WingEditorDialogModel::setWingFlags(const std::vector>& newFlags) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + + for (const auto& [name, checked] : newFlags) { + // Find the matching flagDef by name + for (size_t i = 0; i < Num_parse_wing_flags; ++i) { + if (!stricmp(name.c_str(), Parse_wing_flags[i].name)) { + if (checked) + w->flags.set(Parse_wing_flags[i].def); + else + w->flags.remove(Parse_wing_flags[i].def); + break; + } + } + } + + set_modified(); +} + +ArrivalLocation WingEditorDialogModel::getArrivalType() const +{ + if (!wingIsValid()) + return ArrivalLocation::AT_LOCATION; // fallback to a default value + + const auto w = getCurrentWing(); + return w->arrival_location; +} + +void WingEditorDialogModel::setArrivalType(ArrivalLocation newArrivalType) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + modify(w->arrival_location, newArrivalType); + + // If the new arrival type is a dock bay, clear warp in parameters + // else, clear arrival paths + if (newArrivalType == ArrivalLocation::FROM_DOCK_BAY) { + for (auto& ship : Ships) { + if (ship.objnum < 0) + continue; + if (ship.wingnum != _currentWingIndex) + continue; + + ship.warpin_params_index = -1; + } + } else { + modify(w->arrival_path_mask, 0); + } + + // If the new arrival type does not need a target, clear it + if (newArrivalType == ArrivalLocation::AT_LOCATION) { + modify(w->arrival_anchor, -1); + modify(w->arrival_distance, 0); + } else { + + // Set the target to the first available + const auto& targets = getArrivalTargetList(); + + if (targets.empty()) { + // No targets available, set to -1 + modify(w->arrival_anchor, -1); + modify(w->arrival_distance, 0); + return; + } + + const int currentAnchor = w->arrival_anchor; + + bool valid_anchor = std::find_if(targets.begin(), targets.end(), [currentAnchor](const auto& entry) { + return entry.first == currentAnchor; + }) != targets.end(); + + if (!valid_anchor) { + // Set to the first available target + modify(w->arrival_anchor, targets[0].first); + } + + // Set the distance to minimum if current is smaller + int minDistance = getMinArrivalDistance(); + if (w->arrival_distance < minDistance) { + setArrivalDistance(minDistance); + } + } +} + +int WingEditorDialogModel::getArrivalDelay() const +{ + if (!wingIsValid()) + return 0; + + const auto w = getCurrentWing(); + return w->arrival_delay; +} + +void WingEditorDialogModel::setArrivalDelay(int delayIn) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + if (delayIn < 0) { + delayIn = 0; + } + modify(w->arrival_delay, delayIn); +} + +int WingEditorDialogModel::getMinWaveDelay() const +{ + if (!wingIsValid()) + return 0; + + const auto w = getCurrentWing(); + return w->wave_delay_min; +} + +void WingEditorDialogModel::setMinWaveDelay(int newMin) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + if (newMin < 0) { + newMin = 0; + } + // Ensure the minimum is not greater than the maximum + if (newMin > w->wave_delay_max) { + w->wave_delay_max = newMin; + } + modify(w->wave_delay_min, newMin); +} + +int WingEditorDialogModel::getMaxWaveDelay() const +{ + if (!wingIsValid()) + return 0; + + const auto w = getCurrentWing(); + return w->wave_delay_max; +} + +void WingEditorDialogModel::setMaxWaveDelay(int newMax) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + if (newMax < 0) { + newMax = 0; + } + // Ensure the maximum is not less than the minimum + if (newMax < w->wave_delay_min) { + w->wave_delay_min = newMax; + } + modify(w->wave_delay_max, newMax); +} + +int WingEditorDialogModel::getArrivalTarget() const +{ + if (!wingIsValid()) + return -1; + + const auto w = getCurrentWing(); + // If the arrival location is AT_LOCATION, no target is needed. + if (w->arrival_location == ArrivalLocation::AT_LOCATION) { + return -1; + } + + return w->arrival_anchor; +} + +void WingEditorDialogModel::setArrivalTarget(int targetIndex) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + + // If the arrival location is AT_LOCATION, no target is needed. + if (w->arrival_location == ArrivalLocation::AT_LOCATION) { + targetIndex = -1; + } + + // Validate against the dynamic list which includes special anchors unless dock-bay is required + bool ok = false; + for (const auto& [id, /*label*/ _] : getArrivalTargetList()) { + if (id == targetIndex) { + ok = true; + break; + } + } + + if (!ok) { + targetIndex = -1; + } + + if (w->arrival_anchor == targetIndex) { + return; // no change + } + + modify(w->arrival_anchor, targetIndex); + + // Set the distance to minimum if current is smaller + int minDistance = getMinArrivalDistance(); + if (minDistance < w->arrival_distance) { + setArrivalDistance(0); + } + + modify(w->arrival_path_mask, 0); +} + +int WingEditorDialogModel::getArrivalDistance() const +{ + if (!wingIsValid()) + return 0; + + const auto w = getCurrentWing(); + return w->arrival_distance; +} + +void WingEditorDialogModel::setArrivalDistance(int newDistance) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + if (newDistance < 0) { + newDistance = 0; + } + + // Enforce safe min distance + const int minD = getMinArrivalDistance(); + if (newDistance != 0 && std::abs(newDistance) < minD) { + newDistance = minD; + } + + modify(w->arrival_distance, newDistance); +} + +std::vector> WingEditorDialogModel::getArrivalPaths() const +{ + if (!wingIsValid()) + return {}; + + const auto* w = getCurrentWing(); + if (w->arrival_location != ArrivalLocation::FROM_DOCK_BAY) + return {}; + + return getDockBayPathsForWingMask(w->arrival_path_mask, w->arrival_anchor); +} + +void WingEditorDialogModel::setArrivalPaths(const std::vector>& chosen) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + + if (w->arrival_location != ArrivalLocation::FROM_DOCK_BAY) + return; + + const int anchor = w->arrival_anchor; + if (anchor < 0 || !ship_has_dock_bay(anchor)) + return; + + // Rebuild mask in the same order we produced the list + int mask = 0; + int num_allowed = 0; + + for (size_t i = 0; i < chosen.size(); ++i) { + if (chosen[i].second) { + mask |= (1 << static_cast(i)); + ++num_allowed; + } + } + + // if all are allowed, store 0 + if (num_allowed == static_cast(chosen.size())) { + mask = 0; + } + + if (mask != w->arrival_path_mask) { + w->arrival_path_mask = mask; + set_modified(); + } +} + +int WingEditorDialogModel::getArrivalTree() const +{ + if (!wingIsValid()) + return 0; + + const auto w = getCurrentWing(); + return w->arrival_cue; +} + +void WingEditorDialogModel::setArrivalTree(int newTree) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + + modify(w->arrival_cue, newTree); +} + +bool WingEditorDialogModel::getNoArrivalWarpFlag() const +{ + if (!wingIsValid()) + return false; + + const auto w = getCurrentWing(); + return w->flags[Ship::Wing_Flags::No_arrival_warp]; +} + +void WingEditorDialogModel::setNoArrivalWarpFlag(bool flagIn) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + if (flagIn) { + w->flags.set(Ship::Wing_Flags::No_arrival_warp); + } else { + w->flags.remove(Ship::Wing_Flags::No_arrival_warp); + } + set_modified(); +} + +bool WingEditorDialogModel::getNoArrivalWarpAdjustFlag() const +{ + if (!wingIsValid()) + return false; + + const auto w = getCurrentWing(); + return w->flags[Ship::Wing_Flags::Same_arrival_warp_when_docked]; +} + +void WingEditorDialogModel::setNoArrivalWarpAdjustFlag(bool flagIn) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + if (flagIn) { + w->flags.set(Ship::Wing_Flags::Same_arrival_warp_when_docked); + } else { + w->flags.remove(Ship::Wing_Flags::Same_arrival_warp_when_docked); + } + set_modified(); +} + +DepartureLocation WingEditorDialogModel::getDepartureType() const +{ + if (!wingIsValid()) + return DepartureLocation::AT_LOCATION; // fallback to a default value + + const auto w = getCurrentWing(); + return w->departure_location; +} + +void WingEditorDialogModel::setDepartureType(DepartureLocation newDepartureType) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + modify(w->departure_location, newDepartureType); + + // If the new departure type is a dock bay,clear warp out parameters + // else, clear departure paths + if (newDepartureType == DepartureLocation::TO_DOCK_BAY) { + for (auto& ship : Ships) { + if (ship.objnum < 0) + continue; + if (ship.wingnum != _currentWingIndex) + continue; + + ship.warpout_params_index = -1; + } + } else { + modify(w->departure_path_mask, 0); + } + + // If the new departure type does not need a target, clear it + if (newDepartureType == DepartureLocation::AT_LOCATION) { + modify(w->departure_anchor, -1); + } else { + + // Set the target to the first available + const auto& targets = getDepartureTargetList(); + + if (targets.empty()) { + // No targets available, set to -1 + modify(w->departure_anchor, -1); + return; + } + + const int currentAnchor = w->departure_anchor; + + bool valid_anchor = std::find_if(targets.begin(), targets.end(), [currentAnchor](const auto& entry) { + return entry.first == currentAnchor; + }) != targets.end(); + + if (!valid_anchor) { + // Set to the first available target + modify(w->departure_anchor, targets[0].first); + } + } +} + +int WingEditorDialogModel::getDepartureDelay() const +{ + if (!wingIsValid()) + return 0; + + const auto w = getCurrentWing(); + return w->departure_delay; +} + +void WingEditorDialogModel::setDepartureDelay(int delayIn) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + if (delayIn < 0) { + delayIn = 0; + } + modify(w->departure_delay, delayIn); +} + +int WingEditorDialogModel::getDepartureTarget() const +{ + if (!wingIsValid()) + return -1; + + const auto w = getCurrentWing(); + // If the departure location is AT_LOCATION, no target is needed. + if (w->departure_location == DepartureLocation::AT_LOCATION) { + return -1; + } + + return w->departure_anchor; +} + +void WingEditorDialogModel::setDepartureTarget(int targetIndex) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + // If the departure location is AT_LOCATION, no target is needed. + if (w->departure_location == DepartureLocation::AT_LOCATION) { + targetIndex = -1; + } + + // Validate against the dynamic list that already filters for dock bays. + bool ok = false; + for (const auto& [id, /*label*/ _] : getDepartureTargetList()) { + if (id == targetIndex) { + ok = true; + break; + } + } + + if (!ok) { + targetIndex = -1; // invalid choice -> clear + } + + if (w->departure_anchor == targetIndex) { + return; // no change + } + + modify(w->departure_anchor, targetIndex); + modify(w->departure_path_mask, 0); +} + +std::vector> WingEditorDialogModel::getDeparturePaths() const +{ + if (!wingIsValid()) + return {}; + + const auto* w = getCurrentWing(); + if (w->departure_location != DepartureLocation::TO_DOCK_BAY) + return {}; + + return getDockBayPathsForWingMask(w->departure_path_mask, w->departure_anchor); +} + +void WingEditorDialogModel::setDeparturePaths(const std::vector>& chosen) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + + if (w->departure_location != DepartureLocation::TO_DOCK_BAY) + return; + + const int anchor = w->departure_anchor; + if (anchor < 0 || !ship_has_dock_bay(anchor)) + return; + + // Rebuild mask in the same order we produced the list + int mask = 0; + int num_allowed = 0; + + for (size_t i = 0; i < chosen.size(); ++i) { + if (chosen[i].second) { + mask |= (1 << static_cast(i)); + ++num_allowed; + } + } + + // if all are allowed, store 0 + if (num_allowed == static_cast(chosen.size())) { + mask = 0; + } + + if (mask != w->departure_path_mask) { + w->departure_path_mask = mask; + set_modified(); + } +} + +int WingEditorDialogModel::getDepartureTree() const +{ + if (!wingIsValid()) + return 0; + + const auto w = getCurrentWing(); + return w->departure_cue; +} + +void WingEditorDialogModel::setDepartureTree(int newTree) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + + modify(w->departure_cue, newTree); +} + +bool WingEditorDialogModel::getNoDepartureWarpFlag() const +{ + if (!wingIsValid()) + return false; + + const auto w = getCurrentWing(); + return w->flags[Ship::Wing_Flags::No_departure_warp]; +} + +void WingEditorDialogModel::setNoDepartureWarpFlag(bool flagIn) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + if (flagIn) { + w->flags.set(Ship::Wing_Flags::No_departure_warp); + } else { + w->flags.remove(Ship::Wing_Flags::No_departure_warp); + } + set_modified(); +} + +bool WingEditorDialogModel::getNoDepartureWarpAdjustFlag() const +{ + if (!wingIsValid()) + return false; + + const auto w = getCurrentWing(); + return w->flags[Ship::Wing_Flags::Same_departure_warp_when_docked]; +} + +void WingEditorDialogModel::setNoDepartureWarpAdjustFlag(bool flagIn) +{ + if (!wingIsValid()) + return; + + auto* w = getCurrentWing(); + if (flagIn) { + w->flags.set(Ship::Wing_Flags::Same_departure_warp_when_docked); + } else { + w->flags.remove(Ship::Wing_Flags::Same_departure_warp_when_docked); + } + set_modified(); +} + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/WingEditorDialogModel.h b/qtfred/src/mission/dialogs/WingEditorDialogModel.h new file mode 100644 index 00000000000..7360df82d40 --- /dev/null +++ b/qtfred/src/mission/dialogs/WingEditorDialogModel.h @@ -0,0 +1,145 @@ +#pragma once + +#include "AbstractDialogModel.h" +#include "mission/util.h" +#include "mission/Editor.h" +#include +#include +#include // for max hotkeys +#include // for squad logos +#include "ui/widgets/sexp_tree.h" + +#include "globalincs/pstypes.h" +#include +#include + +namespace fso::fred::dialogs { + + //TODO: This dialog currently works on the wing data directly instead of model members + // so it does not support temporary changes. This will need to be changed in a future PR + +/** + * @brief QTFred's Wing Editor's Model + */ +class WingEditorDialogModel : public AbstractDialogModel { + Q_OBJECT + + public: + WingEditorDialogModel(QObject* parent, EditorViewport* viewport); + + // The model in this dialog directly applies changes to the mission, so apply and reject are superfluous + bool apply() override { return true; } + void reject() override {} + + int getCurrentWingIndex() const { return _currentWingIndex; }; + + bool wingIsValid() const; + + bool isPlayerWing() const; + bool containsPlayerStart() const; + bool wingAllFighterBombers() const; + + bool arrivalIsDockBay() const; + bool arrivalNeedsTarget() const; + bool departureIsDockBay() const; + bool departureNeedsTarget() const; + int getMaxWaveThreshold() const; + int getMinArrivalDistance() const; + + std::pair> getLeaderList() const; + static std::vector> getHotkeyList(); + static std::vector> getFormationList(); + static std::vector> getArrivalLocationList(); + static std::vector> getDepartureLocationList(); + std::vector> getArrivalTargetList() const; + std::vector> getDepartureTargetList() const; + std::vector getSquadLogoList() const { return squadLogoList; }; + + // Top section, first column + SCP_string getWingName() const; + void setWingName(const SCP_string& name); + int getWingLeaderIndex() const; + void setWingLeaderIndex(int newLeaderIndex); + int getNumberOfWaves() const; + void setNumberOfWaves(int newTotalWaves); + int getWaveThreshold() const; + void setWaveThreshold(int newThreshhold); + int getHotkey() const; + void setHotkey(int newHotkeyIndex); + + // Top section, second column + int getFormationId() const; + void setFormationId(int newFormationId); + float getFormationScale() const; + void setFormationScale(float newScale); + void alignWingFormation(); + SCP_string getSquadLogo() const; + void setSquadLogo(const SCP_string& filename); + + // Top section, third column + void selectPreviousWing(); + void selectNextWing(); + void deleteCurrentWing(); + void disbandCurrentWing(); + // Initial orders is handled by its own dialog, so no model function here + std::vector> getWingFlags() const; + void setWingFlags(const std::vector>& newFlags); + + + // Arrival controls + ArrivalLocation getArrivalType() const; + void setArrivalType(ArrivalLocation arrivalType); + int getArrivalDelay() const; + void setArrivalDelay(int delayIn); + int getMinWaveDelay() const; + void setMinWaveDelay(int newMin); + int getMaxWaveDelay() const; + void setMaxWaveDelay(int newMax); + int getArrivalTarget() const; + void setArrivalTarget(int targetIndex); + int getArrivalDistance() const; + void setArrivalDistance(int newDistance); + std::vector> getArrivalPaths() const; + void setArrivalPaths(const std::vector>& newFlags); + int getArrivalTree() const; + void setArrivalTree(int newTree); + bool getNoArrivalWarpFlag() const; + void setNoArrivalWarpFlag(bool flagIn); + bool getNoArrivalWarpAdjustFlag() const; + void setNoArrivalWarpAdjustFlag(bool flagIn); + + // Departure controls + DepartureLocation getDepartureType() const; + void setDepartureType(DepartureLocation departureType); + int getDepartureDelay() const; + void setDepartureDelay(int delayIn); + int getDepartureTarget() const; + void setDepartureTarget(int targetIndex); + std::vector> getDeparturePaths() const; + void setDeparturePaths(const std::vector>& newFlags); + int getDepartureTree() const; + void setDepartureTree(int newTree); + bool getNoDepartureWarpFlag() const; + void setNoDepartureWarpFlag(bool flagIn); + bool getNoDepartureWarpAdjustFlag() const; + void setNoDepartureWarpAdjustFlag(bool flagIn); + + signals: + void wingChanged(); + + private slots: + void onEditorSelectionChanged(int); // currentObjectChanged + void onEditorMissionChanged(); // missionChanged + + private: // NOLINT(readability-redundant-access-specifiers) + void reloadFromCurWing(); + wing* getCurrentWing() const; + static std::vector> getDockBayPathsForWingMask(uint32_t mask, int anchorShipnum); + void prepareSquadLogoList(); + + int _currentWingIndex = -1; + SCP_string _currentWingName; + + SCP_vector squadLogoList; +}; +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/missionsave.cpp b/qtfred/src/mission/missionsave.cpp index d52fd0a07e8..4d411b32305 100644 --- a/qtfred/src/mission/missionsave.cpp +++ b/qtfred/src/mission/missionsave.cpp @@ -5364,39 +5364,48 @@ int CFred_mission_save::save_wings() fout("\n+Flags: ("); } + auto get_flag_name = [](Ship::Wing_Flags flag) -> const char* { + for (size_t k = 0; k < Num_parse_wing_flags; ++k) { + if (Parse_wing_flags[k].def == flag) { + return Parse_wing_flags[k].name; + } + } + return nullptr; + }; + if (Wings[i].flags[Ship::Wing_Flags::Ignore_count]) { - fout(" \"ignore-count\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Ignore_count)); } if (Wings[i].flags[Ship::Wing_Flags::Reinforcement]) { - fout(" \"reinforcement\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Reinforcement)); } if (Wings[i].flags[Ship::Wing_Flags::No_arrival_music]) { - fout(" \"no-arrival-music\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_arrival_music)); } if (Wings[i].flags[Ship::Wing_Flags::No_arrival_message]) { - fout(" \"no-arrival-message\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_arrival_message)); } if (Wings[i].flags[Ship::Wing_Flags::No_first_wave_message]) { - fout(" \"no-first-wave-message\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_first_wave_message)); } if (Wings[i].flags[Ship::Wing_Flags::No_arrival_warp]) { - fout(" \"no-arrival-warp\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_arrival_warp)); } if (Wings[i].flags[Ship::Wing_Flags::No_departure_warp]) { - fout(" \"no-departure-warp\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_departure_warp)); } if (Wings[i].flags[Ship::Wing_Flags::No_dynamic]) { - fout(" \"no-dynamic\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::No_dynamic)); } if (save_format != MissionFormat::RETAIL) { if (Wings[i].flags[Ship::Wing_Flags::Nav_carry]) { - fout(" \"nav-carry-status\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Nav_carry)); } if (Wings[i].flags[Ship::Wing_Flags::Same_arrival_warp_when_docked]) { - fout(" \"same-arrival-warp-when-docked\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Same_arrival_warp_when_docked)); } if (Wings[i].flags[Ship::Wing_Flags::Same_departure_warp_when_docked]) { - fout(" \"same-departure-warp-when-docked\""); + fout(" \"%s\"", get_flag_name(Ship::Wing_Flags::Same_departure_warp_when_docked)); } } diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 1de10227451..34b1abd6f70 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -772,6 +773,19 @@ void FredView::on_actionShips_triggered(bool) } } +void FredView::on_actionWings_triggered(bool) +{ + if (!_wingEditorDialog) { + _wingEditorDialog = new dialogs::WingEditorDialog(this, _viewport); + _wingEditorDialog->setAttribute(Qt::WA_DeleteOnClose); + // When the user closes it, reset our pointer so we can open a new one later + connect(_wingEditorDialog, &QObject::destroyed, this, [this]() { _wingEditorDialog = nullptr; }); + _wingEditorDialog->show(); + } else { + _wingEditorDialog->raise(); + _wingEditorDialog->activateWindow(); + } +} void FredView::on_actionCampaign_triggered(bool) { //TODO: Save if Changes auto editorCampaign = new dialogs::CampaignEditorDialog(this, _viewport); diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 3b2eda34146..e0a55f1fceb 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -21,6 +21,7 @@ class RenderWidget; namespace dialogs { class ShipEditorDialog; +class WingEditorDialog; } namespace Ui { @@ -99,6 +100,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionJump_Nodes_triggered(bool); void on_actionObjects_triggered(bool); void on_actionShips_triggered(bool); + void on_actionWings_triggered(bool); void on_actionCampaign_triggered(bool); void on_actionCommand_Briefing_triggered(bool); void on_actionReinforcements_triggered(bool); @@ -216,6 +218,7 @@ class FredView: public QMainWindow, public IDialogProvider { EditorViewport* _viewport = nullptr; fso::fred::dialogs::ShipEditorDialog* _shipEditorDialog = nullptr; + fso::fred::dialogs::WingEditorDialog* _wingEditorDialog = nullptr; bool _inKeyPressHandler = false; bool _inKeyReleaseHandler = false; diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp index 478f08612c1..1ba6d98d239 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.cpp @@ -15,6 +15,52 @@ ShipCustomWarpDialog::ShipCustomWarpDialog(QDialog* parent, EditorViewport* view _model(new ShipCustomWarpDialogModel(this, viewport, departure)), _viewport(viewport) { ui->setupUi(this); + setupConnections(); + + if (departure) { + this->setWindowTitle("Edit Warp-Out Parameters"); + } else { + this->setWindowTitle("Edit Warp-In Parameters"); + } + updateUI(true); + // Resize the dialog to the minimum size + resize(QDialog::sizeHint()); +} + +// Wing mode constructor +ShipCustomWarpDialog::ShipCustomWarpDialog(QDialog* parent, + EditorViewport* viewport, + bool departure, + int wingIndex, + bool wingMode) + : QDialog(parent), ui(new Ui::ShipCustomWarpDialog()), _viewport(viewport) +{ + ui->setupUi(this); + + if (wingMode) { + _model.reset(new ShipCustomWarpDialogModel(this, + viewport, + departure, + ShipCustomWarpDialogModel::Target::Wing, + wingIndex)); + } else { + _model.reset(new ShipCustomWarpDialogModel(this, viewport, departure)); + } + + setupConnections(); + + if (departure) { + this->setWindowTitle("Edit Warp-Out Parameters"); + } else { + this->setWindowTitle("Edit Warp-In Parameters"); + } + updateUI(true); + // Resize the dialog to the minimum size + resize(QDialog::sizeHint()); +} + +void ShipCustomWarpDialog::setupConnections() +{ connect(this, &QDialog::accepted, _model.get(), &ShipCustomWarpDialogModel::apply); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &ShipCustomWarpDialog::rejectHandler); @@ -55,15 +101,6 @@ ShipCustomWarpDialog::ShipCustomWarpDialog(QDialog* parent, EditorViewport* view QOverload::of(&QDoubleSpinBox::valueChanged), _model.get(), &ShipCustomWarpDialogModel::setPlayerSpeed); - - if (departure) { - this->setWindowTitle("Edit Warp-Out Parameters"); - } else { - this->setWindowTitle("Edit Warp-In Parameters"); - } - updateUI(true); - // Resize the dialog to the minimum size - resize(QDialog::sizeHint()); } ShipCustomWarpDialog::~ShipCustomWarpDialog() = default; diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h index 4d5e90f5fec..7f24cddf4df 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipCustomWarpDialog.h @@ -20,6 +20,8 @@ class ShipCustomWarpDialog : public QDialog { * @param [in] departure Whether the dialog is changeing warp-in or warp-out. */ explicit ShipCustomWarpDialog(QDialog* parent, EditorViewport* viewport, const bool departure = false); + // Constructor for wing mode + ShipCustomWarpDialog(QDialog* parent, EditorViewport* viewport, bool departure, int wingIndex, bool wingMode); ~ShipCustomWarpDialog() override; protected: @@ -55,5 +57,7 @@ class ShipCustomWarpDialog : public QDialog { * @brief Update model with the contents of the anim text box */ void animChanged(); + + void setupConnections(); }; } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/WingEditorDialog.cpp b/qtfred/src/ui/dialogs/WingEditorDialog.cpp new file mode 100644 index 00000000000..efbd704f37b --- /dev/null +++ b/qtfred/src/ui/dialogs/WingEditorDialog.cpp @@ -0,0 +1,688 @@ +#include "WingEditorDialog.h" +#include "General/CheckBoxListDialog.h" +#include "General/ImagePickerDialog.h" +#include "ShipEditor/ShipGoalsDialog.h" +#include "ShipEditor/ShipCustomWarpDialog.h" + +#include "ui_WingEditorDialog.h" + +#include +#include +#include + +namespace fso::fred::dialogs { + +WingEditorDialog::WingEditorDialog(FredView* parent, EditorViewport* viewport) + : QDialog(parent), ui(new Ui::WingEditorDialog()), _model(new WingEditorDialogModel(this, viewport)), + _viewport(viewport) +{ + ui->setupUi(this); + + setWindowTitle(tr("Wing Editor")); + + // Whenever the model reports changes, refresh the UI + connect(_model.get(), &AbstractDialogModel::modelChanged, this, &WingEditorDialog::updateUi); + connect(_model.get(), &WingEditorDialogModel::wingChanged, this, [this] { + refreshAllDynamicCombos(); + updateUi(); + }); + + refreshAllDynamicCombos(); + updateUi(); + + // Resize the dialog to the minimum size + resize(QDialog::sizeHint()); +} + +WingEditorDialog::~WingEditorDialog() = default; + +void WingEditorDialog::updateUi() +{ + util::SignalBlockers blockers(this); + + ui->waveThresholdSpinBox->setMaximum(_model->getMaxWaveThreshold()); + ui->arrivalDistanceSpinBox->setMinimum(_model->getMinArrivalDistance()); + + // Top section, first column + ui->wingNameEdit->setText(_model->getWingName().c_str()); + ui->wingLeaderCombo->setCurrentIndex(_model->getWingLeaderIndex()); + ui->numWavesSpinBox->setValue(_model->getNumberOfWaves()); + ui->waveThresholdSpinBox->setValue(_model->getWaveThreshold()); + ui->hotkeyCombo->setCurrentIndex(ui->hotkeyCombo->findData(_model->getHotkey())); + + // Top section, second column + ui->formationCombo->setCurrentIndex(ui->formationCombo->findData(_model->getFormationId())); + ui->formationScaleSpinBox->setValue(_model->getFormationScale()); + updateLogoPreview(); + + // Arrival controls + ui->arrivalLocationCombo->setCurrentIndex(static_cast(_model->getArrivalType())); + ui->arrivalDelaySpinBox->setValue(_model->getArrivalDelay()); + ui->minDelaySpinBox->setValue(_model->getMinWaveDelay()); + ui->maxDelaySpinBox->setValue(_model->getMaxWaveDelay()); + ui->arrivalTargetCombo->setCurrentIndex(ui->arrivalTargetCombo->findData(_model->getArrivalTarget())); + ui->arrivalDistanceSpinBox->setValue(_model->getArrivalDistance()); + + ui->arrivalTree->initializeEditor(_viewport->editor, this); + ui->arrivalTree->load_tree(_model->getArrivalTree()); + if (ui->arrivalTree->select_sexp_node != -1) { + ui->arrivalTree->hilite_item(ui->arrivalTree->select_sexp_node); + } + ui->noArrivalWarpCheckBox->setChecked(_model->getNoArrivalWarpFlag()); + ui->noArrivalWarpAdjustCheckbox->setChecked(_model->getNoArrivalWarpAdjustFlag()); + + // Departure controls + ui->departureLocationCombo->setCurrentIndex(static_cast(_model->getDepartureType())); + ui->departureDelaySpinBox->setValue(_model->getDepartureDelay()); + ui->departureTargetCombo->setCurrentIndex(ui->departureTargetCombo->findData(_model->getDepartureTarget())); + ui->departureTree->initializeEditor(_viewport->editor, this); + ui->departureTree->load_tree(_model->getDepartureTree()); + if (ui->departureTree->select_sexp_node != -1) { + ui->departureTree->hilite_item(ui->departureTree->select_sexp_node); + } + ui->noDepartureWarpCheckBox->setChecked(_model->getNoDepartureWarpFlag()); + ui->noDepartureWarpAdjustCheckbox->setChecked(_model->getNoDepartureWarpAdjustFlag()); + + enableOrDisableControls(); +} + +void WingEditorDialog::updateLogoPreview() +{ + QImage img; + QString err; + const auto filename = _model->getSquadLogo(); + if (fso::fred::util::loadImageToQImage(filename, img, &err)) { + // scale to the preview area + const auto pix = QPixmap::fromImage(img).scaled(ui->squadLogoImage->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + ui->squadLogoImage->setPixmap(pix); + ui->squadLogoFile->setText(filename.c_str()); + } else { + ui->squadLogoImage->setPixmap(QPixmap()); + ui->squadLogoFile->setText("(no logo)"); + } +} + +void WingEditorDialog::enableOrDisableControls() +{ + util::SignalBlockers blockers(this); + + auto enableAll = [&](bool on) { + // Top section, first column + ui->wingNameEdit->setEnabled(on); + ui->wingLeaderCombo->setEnabled(on); + ui->numWavesSpinBox->setEnabled(on); + ui->waveThresholdSpinBox->setEnabled(on); + ui->hotkeyCombo->setEnabled(on); + ui->wingFlagsButton->setEnabled(on); + // Top section, second column + ui->formationCombo->setEnabled(on); + ui->formationScaleSpinBox->setEnabled(on); + ui->alignFormationButton->setEnabled(on); + // Top section, third column + ui->deleteWingButton->setEnabled(on); + ui->disbandWingButton->setEnabled(on); + ui->initialOrdersButton->setEnabled(on); + // Middle section + ui->setSquadLogoButton->setEnabled(on); + // Arrival controls + ui->arrivalLocationCombo->setEnabled(on); + ui->arrivalDelaySpinBox->setEnabled(on); + ui->minDelaySpinBox->setEnabled(on); + ui->maxDelaySpinBox->setEnabled(on); + ui->arrivalTargetCombo->setEnabled(on); + ui->arrivalDistanceSpinBox->setEnabled(on); + ui->restrictArrivalPathsButton->setEnabled(on); + ui->customWarpinButton->setEnabled(on); + ui->arrivalTree->setEnabled(on); + ui->noArrivalWarpCheckBox->setEnabled(on); + ui->noArrivalWarpAdjustCheckbox->setEnabled(on); + // Departure controls + ui->departureLocationCombo->setEnabled(on); + ui->departureDelaySpinBox->setEnabled(on); + ui->departureTargetCombo->setEnabled(on); + ui->restrictDeparturePathsButton->setEnabled(on); + ui->customWarpoutButton->setEnabled(on); + ui->departureTree->setEnabled(on); + ui->noDepartureWarpCheckBox->setEnabled(on); + ui->noDepartureWarpAdjustCheckbox->setEnabled(on); + }; + + if (!_model->wingIsValid()) { + enableAll(false); + clearGeneralFields(); + clearArrivalFields(); + clearDepartureFields(); + return; + } + + enableAll(true); + + const bool isPlayerWing = _model->isPlayerWing(); + const bool containsPlayerStart = _model->containsPlayerStart(); + const bool allFighterBombers = _model->wingAllFighterBombers(); + + // Waves / Threshold: enabled only if NOT a player wing and all members are fighter/bombers + const bool wavesEnabled = (!isPlayerWing) && allFighterBombers; + ui->numWavesSpinBox->setEnabled(wavesEnabled); + ui->waveThresholdSpinBox->setEnabled(wavesEnabled); + + // Arrival section: disabled for starting wings (SP player wing or MP starting wing) + const bool arrivalEditable = !isPlayerWing; + ui->arrivalLocationCombo->setEnabled(arrivalEditable); + ui->arrivalDelaySpinBox->setEnabled(arrivalEditable); + ui->minDelaySpinBox->setEnabled(arrivalEditable); + ui->maxDelaySpinBox->setEnabled(arrivalEditable); + if (!arrivalEditable) { + clearArrivalFields(); + } + + // Arrival target/distance and path/custom buttons + const bool arrivalIsDockBay = _model->arrivalIsDockBay(); + const bool arrivalNeedsTarget = _model->arrivalNeedsTarget(); + + ui->arrivalTargetCombo->setEnabled(arrivalEditable && arrivalNeedsTarget); + ui->arrivalDistanceSpinBox->setEnabled(arrivalEditable && arrivalNeedsTarget); + ui->restrictArrivalPathsButton->setEnabled(arrivalEditable && arrivalIsDockBay); + ui->customWarpinButton->setEnabled(arrivalEditable && !arrivalIsDockBay); + + // Arrival cue tree: lock when the wing actually contains Player-1 start (retail behavior) + ui->arrivalTree->setEnabled(!containsPlayerStart); + + // Also tie the "no arrival warp" checkboxes to whether arrival is editable + ui->noArrivalWarpCheckBox->setEnabled(arrivalEditable); + ui->noArrivalWarpAdjustCheckbox->setEnabled(arrivalEditable); + + // Departure side: never gated by starting-wing rule + ui->departureLocationCombo->setEnabled(true); + ui->departureDelaySpinBox->setEnabled(true); + ui->departureTree->setEnabled(true); + + // Departure target and path/custom depends on location + const bool departureIsDockBay = _model->departureIsDockBay(); + const bool departureNeedsTarget = _model->departureNeedsTarget(); + + ui->departureTargetCombo->setEnabled(departureNeedsTarget); + ui->restrictDeparturePathsButton->setEnabled(departureIsDockBay); + ui->customWarpoutButton->setEnabled(!departureIsDockBay); + + // "No departure warp" checkboxes always enabled with a valid wing + ui->noDepartureWarpCheckBox->setEnabled(true); + ui->noDepartureWarpAdjustCheckbox->setEnabled(true); +} + +void WingEditorDialog::clearGeneralFields() +{ + util::SignalBlockers blockers(this); + + ui->wingNameEdit->clear(); + ui->wingLeaderCombo->setCurrentIndex(-1); + + ui->hotkeyCombo->setCurrentIndex(-1); + ui->formationCombo->setCurrentIndex(-1); + + ui->squadLogoFile->setText(""); +} + +void WingEditorDialog::clearArrivalFields() +{ + util::SignalBlockers blockers(this); + + ui->arrivalLocationCombo->setCurrentIndex(-1); + ui->arrivalDelaySpinBox->setValue(ui->arrivalDelaySpinBox->minimum()); + ui->minDelaySpinBox->setValue(ui->minDelaySpinBox->minimum()); + ui->maxDelaySpinBox->setValue(ui->maxDelaySpinBox->minimum()); + + ui->arrivalTargetCombo->setCurrentIndex(-1); + ui->arrivalDistanceSpinBox->setValue(ui->arrivalDistanceSpinBox->minimum()); + + ui->arrivalTree->clear(); +} + +void WingEditorDialog::clearDepartureFields() +{ + util::SignalBlockers blockers(this); + + ui->departureLocationCombo->setCurrentIndex(-1); + ui->departureDelaySpinBox->setValue(ui->departureDelaySpinBox->minimum()); + + ui->departureTargetCombo->setCurrentIndex(-1); + + ui->departureTree->clear(); +} + +void WingEditorDialog::refreshLeaderCombo() +{ + util::SignalBlockers blockers(this); + ui->wingLeaderCombo->clear(); + auto [sel, names] = _model->getLeaderList(); + for (int i = 0; i < (int)names.size(); ++i) { + ui->wingLeaderCombo->addItem(QString::fromUtf8(names[i].c_str()), i); + } + ui->wingLeaderCombo->setCurrentIndex((sel >= 0 && sel < (int)names.size()) ? sel : -1); +} + +void WingEditorDialog::refreshHotkeyCombo() +{ + util::SignalBlockers blockers(this); + ui->hotkeyCombo->clear(); + for (auto& [id, label] : _model->getHotkeyList()) + ui->hotkeyCombo->addItem(QString::fromUtf8(label.c_str()), id); +} + +void WingEditorDialog::refreshFormationCombo() +{ + util::SignalBlockers blockers(this); + ui->formationCombo->clear(); + for (auto& [id, label] : _model->getFormationList()) + ui->formationCombo->addItem(QString::fromUtf8(label.c_str()), id); +} + +void WingEditorDialog::refreshArrivalLocationCombo() +{ + util::SignalBlockers blockers(this); + ui->arrivalLocationCombo->clear(); + for (auto& [id, label] : _model->getArrivalLocationList()) + ui->arrivalLocationCombo->addItem(QString::fromUtf8(label.c_str()), id); +} + +void WingEditorDialog::refreshDepartureLocationCombo() +{ + util::SignalBlockers blockers(this); + ui->departureLocationCombo->clear(); + for (auto& [id, label] : _model->getDepartureLocationList()) + ui->departureLocationCombo->addItem(QString::fromUtf8(label.c_str()), id); +} + +void WingEditorDialog::refreshArrivalTargetCombo() +{ + util::SignalBlockers blockers(this); + ui->arrivalTargetCombo->clear(); + auto items = _model->getArrivalTargetList(); + for (auto& [id, label] : items) { + ui->arrivalTargetCombo->addItem(QString::fromUtf8(label.c_str()), id); + } +} + +void WingEditorDialog::refreshDepartureTargetCombo() +{ + util::SignalBlockers blockers(this); + ui->departureTargetCombo->clear(); + auto items = _model->getDepartureTargetList(); + for (auto& [id, label] : items) { + ui->departureTargetCombo->addItem(QString::fromUtf8(label.c_str()), id); + } +} + +void WingEditorDialog::refreshAllDynamicCombos() +{ + refreshLeaderCombo(); + refreshHotkeyCombo(); + refreshFormationCombo(); + refreshArrivalLocationCombo(); + refreshDepartureLocationCombo(); + refreshArrivalTargetCombo(); + refreshDepartureTargetCombo(); +} + +void WingEditorDialog::on_hideCuesButton_clicked() +{ + _cues_hidden = !_cues_hidden; + + ui->arrivalGroupBox->setHidden(_cues_hidden); + ui->departureGroupBox->setHidden(_cues_hidden); + ui->helpText->setHidden(_cues_hidden); + ui->HelpTitle->setHidden(_cues_hidden); + ui->hideCuesButton->setText(_cues_hidden ? "Show Cues" : "Hide Cues"); + + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + resize(sizeHint()); +} + +void WingEditorDialog::on_wingNameEdit_editingFinished() +{ + const auto newName = ui->wingNameEdit->text().toStdString(); + _model->setWingName(newName); +} + +void WingEditorDialog::on_wingLeaderCombo_currentIndexChanged(int index) +{ + _model->setWingLeaderIndex(index); +} + +void WingEditorDialog::on_numberOfWavesSpinBox_valueChanged(int value) +{ + _model->setNumberOfWaves(value); + ui->waveThresholdSpinBox->setMaximum(_model->getMaxWaveThreshold()); +} + +void WingEditorDialog::on_waveThresholdSpinBox_valueChanged(int value) +{ + _model->setWaveThreshold(value); +} + +void WingEditorDialog::on_hotkeyCombo_currentIndexChanged(int /*index*/) +{ + const int value = ui->hotkeyCombo->currentData().toInt(); // -1, 0..MAX_KEYED_TARGETS, or MAX_KEYED_TARGETS for Hidden + _model->setHotkey(value); +} + +void WingEditorDialog::on_formationCombo_currentIndexChanged(int /*index*/) +{ + _model->setFormationId(ui->formationCombo->currentData().toInt()); +} + +void WingEditorDialog::on_formationScaleSpinBox_valueChanged(double value) +{ + _model->setFormationScale(static_cast(value)); +} + +void WingEditorDialog::on_alignFormationButton_clicked() +{ + _model->alignWingFormation(); +} + +void WingEditorDialog::on_setSquadLogoButton_clicked() +{ + const auto files = _model->getSquadLogoList(); + if (files.empty()) { + QMessageBox::information(this, "Select Squad Image", "No images found."); + return; + } + + QStringList qnames; + qnames.reserve(static_cast(files.size())); + for (const auto& s : files) + qnames << QString::fromStdString(s); + + ImagePickerDialog dlg(this); + dlg.setWindowTitle("Select Squad Image"); + dlg.allowUnset(true); + dlg.setImageFilenames(qnames); + + // Optional: preselect current + dlg.setInitialSelection(QString::fromStdString(_model->getSquadLogo())); + + if (dlg.exec() != QDialog::Accepted) + return; + + const std::string chosen = dlg.selectedFile().toUtf8().constData(); + _model->setSquadLogo(chosen); + updateLogoPreview(); +} + +void WingEditorDialog::on_prevWingButton_clicked() +{ + _model->selectPreviousWing(); +} + +void WingEditorDialog::on_nextWingButton_clicked() +{ + _model->selectNextWing(); +} + +void WingEditorDialog::on_deleteWingButton_clicked() +{ + if (QMessageBox::question(this, "Confirm", "Are you sure you want to delete this wing? This will remove the wing and delete its ships.") == QMessageBox::Yes) { + _model->deleteCurrentWing(); + } +} + +void WingEditorDialog::on_disbandWingButton_clicked() +{ + if (QMessageBox::question(this, "Confirm", "Are you sure you want to disband this wing? This will remove the wing but leave its ships intact.") == QMessageBox::Yes) { + _model->disbandCurrentWing(); + } +} + +void WingEditorDialog::on_initialOrdersButton_clicked() +{ + if (!_model->wingIsValid()) { + QMessageBox::warning(this, "Initial Orders", "No valid wing selected."); + return; + } + + const int wingIndex = _model->getCurrentWingIndex(); // or your equivalent getter + if (wingIndex < 0) { + QMessageBox::warning(this, "Initial Orders", "No valid wing selected."); + return; + } + + // block for empty wings (matches old FRED behavior where goals apply to the wing’s ships) + if (Wings[wingIndex].wave_count <= 0) { + QMessageBox::information(this, "Initial Orders", "This wing has no ships (wave_count == 0)."); + return; + } + + // Open the existing ShipGoals dialog in wing mode + fso::fred::dialogs::ShipGoalsDialog dlg(this, _viewport, false, -1, wingIndex); + + dlg.exec(); +} + +void WingEditorDialog::on_wingFlagsButton_clicked() +{ + CheckBoxListDialog dlg(this); + dlg.setCaption("Select Wing Flags"); + + // Get our flag list and convert it to Qt's internal types + auto wingFlags = _model->getWingFlags(); + + QVector> checkbox_list; + + for (const auto& flag : wingFlags) { + checkbox_list.append({flag.first.c_str(), flag.second}); + } + + dlg.setOptions(checkbox_list); // TODO upgrade checkbox to accept and display item descriptions + + if (dlg.exec() == QDialog::Accepted) { + auto returned_values = dlg.getCheckedStates(); + + std::vector> updatedFlags; + + for (int i = 0; i < checkbox_list.size(); ++i) { + // Convert back to std::string + std::string name = checkbox_list[i].first.toUtf8().constData(); + updatedFlags.emplace_back(name, returned_values[i]); + } + + _model->setWingFlags(updatedFlags); + } +} + +void WingEditorDialog::on_arrivalLocationCombo_currentIndexChanged(int /*index*/) +{ + const int value = ui->arrivalLocationCombo->currentData().toInt(); + _model->setArrivalType(static_cast(value)); + refreshArrivalTargetCombo(); + updateUi(); +} + +void WingEditorDialog::on_arrivalDelaySpinBox_valueChanged(int value) +{ + _model->setArrivalDelay(value); +} + +void WingEditorDialog::on_minDelaySpinBox_valueChanged(int value) +{ + _model->setMinWaveDelay(value); + + util::SignalBlockers blockers(this); + ui->maxDelaySpinBox->setMinimum(value); +} + +void WingEditorDialog::on_maxDelaySpinBox_valueChanged(int value) +{ + _model->setMaxWaveDelay(value); +} + +void WingEditorDialog::on_arrivalTargetCombo_currentIndexChanged(int /*index*/) +{ + const int value = ui->arrivalTargetCombo->currentData().toInt(); + _model->setArrivalTarget(value); + updateUi(); +} + +void WingEditorDialog::on_arrivalDistanceSpinBox_valueChanged(int value) +{ + _model->setArrivalDistance(value); +} + +void WingEditorDialog::on_restrictArrivalPathsButton_clicked() +{ + CheckBoxListDialog dlg(this); + dlg.setCaption("Select Wing Flags"); + + // Get our path list and convert it to Qt's internal types + auto wingFlags = _model->getArrivalPaths(); + + QVector> checkbox_list; + + for (const auto& flag : wingFlags) { + checkbox_list.append({flag.first.c_str(), flag.second}); + } + + dlg.setOptions(checkbox_list); + + if (dlg.exec() == QDialog::Accepted) { + auto returned_values = dlg.getCheckedStates(); + + std::vector> updatedFlags; + + for (int i = 0; i < checkbox_list.size(); ++i) { + // Convert back to std::string + std::string name = checkbox_list[i].first.toUtf8().constData(); + updatedFlags.emplace_back(name, returned_values[i]); + } + + _model->setArrivalPaths(updatedFlags); + } +} + +void WingEditorDialog::on_customWarpinButton_clicked() +{ + if (!_model->wingIsValid()) + return; + + auto dlg = fso::fred::dialogs::ShipCustomWarpDialog(this, + _viewport, + false, + _model->getCurrentWingIndex(), + true); + dlg.exec(); +} + +void WingEditorDialog::on_arrivalTree_nodeChanged(int newTree) +{ + _model->setArrivalTree(newTree); //TODO This seems broken in a wierd way. Will need followup +} + +void WingEditorDialog::on_noArrivalWarpCheckBox_toggled(bool checked) +{ + _model->setNoArrivalWarpFlag(checked); +} + +void WingEditorDialog::on_noArrivalWarpAdjustCheckbox_toggled(bool checked) +{ + _model->setNoArrivalWarpAdjustFlag(checked); +} + +void WingEditorDialog::on_departureLocationCombo_currentIndexChanged(int /*index*/) +{ + const int value = ui->departureLocationCombo->currentData().toInt(); + _model->setDepartureType(static_cast(value)); + refreshDepartureTargetCombo(); + updateUi(); +} + +void WingEditorDialog::on_departureDelaySpinBox_valueChanged(int value) +{ + _model->setDepartureDelay(value); +} + +void WingEditorDialog::on_departureTargetCombo_currentIndexChanged(int /*index*/) +{ + const int value = ui->departureTargetCombo->currentData().toInt(); + _model->setDepartureTarget(value); +} + +void WingEditorDialog::on_restrictDeparturePathsButton_clicked() +{ + CheckBoxListDialog dlg(this); + dlg.setCaption("Select Wing Flags"); + + // Get our path list and convert it to Qt's internal types + auto wingFlags = _model->getDeparturePaths(); + + QVector> checkbox_list; + + for (const auto& flag : wingFlags) { + checkbox_list.append({flag.first.c_str(), flag.second}); + } + + dlg.setOptions(checkbox_list); + + if (dlg.exec() == QDialog::Accepted) { + auto returned_values = dlg.getCheckedStates(); + + std::vector> updatedFlags; + + for (int i = 0; i < checkbox_list.size(); ++i) { + // Convert back to std::string + std::string name = checkbox_list[i].first.toUtf8().constData(); + updatedFlags.emplace_back(name, returned_values[i]); + } + + _model->setDeparturePaths(updatedFlags); + } +} + +void WingEditorDialog::on_customWarpoutButton_clicked() +{ + if (!_model->wingIsValid()) + return; + + auto dlg = fso::fred::dialogs::ShipCustomWarpDialog(this, + _viewport, + true, + _model->getCurrentWingIndex(), + true); + dlg.exec(); +} + +void WingEditorDialog::on_departureTree_nodeChanged(int newTree) +{ + _model->setDepartureTree(newTree); //TODO This seems broken in a wierd way. Will need followup +} + +void WingEditorDialog::on_noDepartureWarpCheckBox_toggled(bool checked) +{ + _model->setNoDepartureWarpFlag(checked); +} + +void WingEditorDialog::on_noDepartureWarpAdjustCheckbox_toggled(bool checked) +{ + _model->setNoDepartureWarpAdjustFlag(checked); +} + +void WingEditorDialog::on_arrivalTree_helpChanged(const QString& help) +{ + ui->helpText->setPlainText(help); +} + +void WingEditorDialog::on_arrivalTree_miniHelpChanged(const QString& help) +{ + ui->HelpTitle->setText(help); +} + +void WingEditorDialog::on_departureTree_helpChanged(const QString& help) +{ + ui->helpText->setPlainText(help); +} + +void WingEditorDialog::on_departureTree_miniHelpChanged(const QString& help) +{ + ui->HelpTitle->setText(help); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/WingEditorDialog.h b/qtfred/src/ui/dialogs/WingEditorDialog.h new file mode 100644 index 00000000000..d44dee63f90 --- /dev/null +++ b/qtfred/src/ui/dialogs/WingEditorDialog.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include + +#include +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class WingEditorDialog; +} + +class WingEditorDialog : public QDialog, public SexpTreeEditorInterface { + Q_OBJECT + public: + explicit WingEditorDialog(FredView* parent, EditorViewport* viewport); + ~WingEditorDialog() override; + + private slots: + void on_hideCuesButton_clicked(); + + // Top section, first column + void on_wingNameEdit_editingFinished(); + void on_wingLeaderCombo_currentIndexChanged(int index); + void on_numberOfWavesSpinBox_valueChanged(int value); + void on_waveThresholdSpinBox_valueChanged(int value); + void on_hotkeyCombo_currentIndexChanged(int /*index*/); + + // Top section, second column + void on_formationCombo_currentIndexChanged(int /*index*/); + void on_formationScaleSpinBox_valueChanged(double value); + void on_alignFormationButton_clicked(); + void on_setSquadLogoButton_clicked(); + + // Top section, third column + void on_prevWingButton_clicked(); + void on_nextWingButton_clicked(); + void on_deleteWingButton_clicked(); + void on_disbandWingButton_clicked(); + void on_initialOrdersButton_clicked(); + void on_wingFlagsButton_clicked(); + + // Arrival controls + void on_arrivalLocationCombo_currentIndexChanged(int /*index*/); + void on_arrivalDelaySpinBox_valueChanged(int value); + void on_minDelaySpinBox_valueChanged(int value); + void on_maxDelaySpinBox_valueChanged(int value); + void on_arrivalTargetCombo_currentIndexChanged(int /*index*/); + void on_arrivalDistanceSpinBox_valueChanged(int value); + void on_restrictArrivalPathsButton_clicked(); + void on_customWarpinButton_clicked(); + void on_arrivalTree_nodeChanged(int newTree); + void on_noArrivalWarpCheckBox_toggled(bool checked); + void on_noArrivalWarpAdjustCheckbox_toggled(bool checked); + + // Departure controls + void on_departureLocationCombo_currentIndexChanged(int /*index*/); + void on_departureDelaySpinBox_valueChanged(int value); + void on_departureTargetCombo_currentIndexChanged(int /*index*/); + void on_restrictDeparturePathsButton_clicked(); + void on_customWarpoutButton_clicked(); + void on_departureTree_nodeChanged(int newTree); + void on_noDepartureWarpCheckBox_toggled(bool checked); + void on_noDepartureWarpAdjustCheckbox_toggled(bool checked); + + // Sexp help text + void on_arrivalTree_helpChanged(const QString& help); + void on_arrivalTree_miniHelpChanged(const QString& help); + void on_departureTree_helpChanged(const QString& help); + void on_departureTree_miniHelpChanged(const QString& help); + + private: // NOLINT(readability-redundant-access-specifiers) + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + bool _cues_hidden = false; + + void updateUi(); + void enableOrDisableControls(); + + void clearArrivalFields(); + void clearDepartureFields(); + void clearGeneralFields(); + + void refreshLeaderCombo(); + void refreshHotkeyCombo(); + void refreshFormationCombo(); + void refreshArrivalLocationCombo(); + void refreshDepartureLocationCombo(); + void refreshArrivalTargetCombo(); + void refreshDepartureTargetCombo(); + void refreshAllDynamicCombos(); + + void updateLogoPreview(); +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/WingEditorDialog.ui b/qtfred/ui/WingEditorDialog.ui new file mode 100644 index 00000000000..74ef6d39890 --- /dev/null +++ b/qtfred/ui/WingEditorDialog.ui @@ -0,0 +1,890 @@ + + + fso::fred::dialogs::WingEditorDialog + + + + 0 + 0 + 584 + 1022 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Edit Wing + + + + QLayout::SetDefaultConstraint + + + + + QLayout::SetDefaultConstraint + + + + + + + 0 + + + + + + + Hotkey + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + # of Waves + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + <html><head/><body><p>Sets the ship's name</p></body></html> + + + + + + + Wave Threshold + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 1 + + + 16777215 + + + + + + + + + + + + + Wing Name + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Wing Leader + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + true + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + 6 + + + + + 16777215.000000000000000 + + + 0.100000000000000 + + + + + + + Formation Scale + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Formation + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + <html><head/><body><p><br/></p></body></html> + + + + + + + + + + + + Align Formation + + + + + + + + + 6 + + + + + <html><head/><body><p>Set ship's flags (Add special Features)</p></body></html> + + + Set Squad Logo + + + + + + + <none> + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 0 + 0 + + + + + 100 + 100 + + + + + 100 + 100 + + + + true + + + QFrame::Box + + + QFrame::Plain + + + <none> + + + Qt::AlignCenter + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + QLayout::SetDefaultConstraint + + + 0 + + + + + 6 + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Prev + + + + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Next + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Disband Wing + + + + + + + <html><head/><body><p>Give orders to the ship</p></body></html> + + + Initial Orders + + + + + + + Delete Wing + + + + + + + <html><head/><body><p>Changes ship's textures</p></body></html> + + + Set Wing Flags + + + + + + + + + + + QLayout::SetDefaultConstraint + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + <html><head/><body><p>Hide the arrival/departure section</p></body></html> + + + Hide Cues + + + true + + + + + + + + + + + Arrival + + + + + + + + Distance + + + + + + + Location + + + + + + + <html><head/><body><p>Target Ship</p></body></html> + + + + + + + Target + + + + + + + Delay + + + + + + + <html><head/><body><p>Sets ship's arrival location</p><p>Hyperspace - Ship appears where it is on the map,</p><p>Near Ship - Ship appears at a random location the specified distance of the target,</p><p>In front of ship - Ship appears ate the specified distance somewhere in a cone in front of the target,</p><p>Docking Bay - Ship arrives in the targets fighter bay</p></body></html> + + + + + + + + + <html><head/><body><p>Time to wait after cue conditions met</p></body></html> + + + 16777215 + + + + + + + Seconds + + + + + + + + + + + <html><head/><body><p>Distance from target</p></body></html> + + + QAbstractSpinBox::NoButtons + + + 16777215 + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 40 + 20 + + + + + + + + + + + + Delay Between Waves + + + + + + Min + + + + + + + 16777215 + + + + + + + Max + + + + + + + 16777215 + + + + + + + + + + <html><head/><body><p>Limit the fighterbay paths the arriving ship can use</p></body></html> + + + Restrict Arrival Paths + + + + + + + <html><head/><body><p>Change the apperance, speed and sound of the warp effect (Excluding Docking bay)</p></body></html> + + + Custom Warp-in Parameters + + + + + + + + + Cue: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + <html><head/><body><p>Ship pops into existance</p></body></html> + + + No Warp Effect + + + + + + + Don't Adjust Warp When Docked + + + + + + + + + + Departure + + + + + + + + Target + + + + + + + <html><head/><body><p>Set departure target</p></body></html> + + + + + + + Delay + + + + + + + <html><head/><body><p>Sets ship's departure location</p><p>Hyperspace - Ship leaves from where it is,</p><p>Docking Bay - Ship departs into the targets fighter bay</p></body></html> + + + + + + + + + <html><head/><body><p>Time to wait after cue conditions met</p></body></html> + + + 16777215 + + + + + + + Seconds + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Location + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 22 + + + + + + + + + + + + + false + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + + <html><head/><body><p>Limit the fighterbay paths the departing ship can use</p></body></html> + + + Restrict Departure Paths + + + + + + + <html><head/><body><p>Change the apperance, speed and sound of the warp effect (Hyperspace only)</p></body></html> + + + Custom Warp-out Parameters + + + + + + + + + Cue: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + <html><head/><body><p>Ship pops out of existance</p></body></html> + + + No Warp Effect + + + + + + + Don't Adjust Warp When Docked + + + + + + + + + + + + true + + + true + + + + + + + true + + + Qt::ScrollBarAlwaysOff + + + true + + + + + + + + + + fso::fred::sexp_tree + QTreeView +
ui/widgets/sexp_tree.h
+
+ + fso::fred::ShipFlagCheckbox + QCheckBox +
ui/widgets/ShipFlagCheckbox.h
+
+
+ + + + +
From 9db567ea4025cf277cb39fd09caf946978462ed7 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 23 Aug 2025 09:58:14 -0500 Subject: [PATCH 405/466] QtFRED Mission Goals Dialog Cleanup (#6951) * cleanup and bugfixes of mission goals editor * redundant specifier * static casts --- .../dialogs/MissionGoalsDialogModel.cpp | 60 +++-- .../mission/dialogs/MissionGoalsDialogModel.h | 12 +- qtfred/src/ui/dialogs/MissionGoalsDialog.cpp | 251 ++++++++++-------- qtfred/src/ui/dialogs/MissionGoalsDialog.h | 44 +-- qtfred/ui/MissionGoalsDialog.ui | 27 +- 5 files changed, 212 insertions(+), 182 deletions(-) diff --git a/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp index b1328360e26..f13c7b81dee 100644 --- a/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp @@ -1,12 +1,7 @@ -// -// - #include "MissionGoalsDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { MissionGoalsDialogModel::MissionGoalsDialogModel(QObject* parent, fso::fred::EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { @@ -14,7 +9,6 @@ MissionGoalsDialogModel::MissionGoalsDialogModel(QObject* parent, fso::fred::Edi bool MissionGoalsDialogModel::apply() { SCP_vector> names; - int i; auto changes_detected = query_modified(); @@ -24,7 +18,7 @@ bool MissionGoalsDialogModel::apply() } // rename all sexp references to old goals - for (i=0; i<(int)m_goals.size(); i++) { + for (size_t i=0; i= 0) { names.emplace_back(Mission_goals[m_sig[i]].name, m_goals[i].name); Mission_goals[m_sig[i]].satisfied = 1; @@ -50,7 +44,7 @@ bool MissionGoalsDialogModel::apply() Mission_goals.push_back(dialog_goal); Mission_goals.back().formula = _sexp_tree->save_tree(dialog_goal.formula); if ( The_mission.game_type & MISSION_TYPE_MULTI_TEAMS ) { - Assert( dialog_goal.team != -1 ); + Assertion(dialog_goal.team != -1, "Invalid goal team!"); } } @@ -70,17 +64,17 @@ void MissionGoalsDialogModel::reject() { // Nothing to do here } mission_goal& MissionGoalsDialogModel::getCurrentGoal() { - Assertion(cur_goal >= 0 && cur_goal < (int)m_goals.size(), "Current goal index is not valid!"); + Assertion(SCP_vector_inbounds(m_goals, cur_goal), "Current goal index is not valid!"); return m_goals[cur_goal]; } bool MissionGoalsDialogModel::isCurrentGoalValid() const { - return cur_goal >= 0 && cur_goal < (int)m_goals.size(); + return SCP_vector_inbounds(m_goals, cur_goal); } void MissionGoalsDialogModel::initializeData() { m_goals.clear(); m_sig.clear(); - for (int i=0; i<(int)Mission_goals.size(); i++) { + for (size_t i=0; i m_goals; bool modified = false; - int m_display_goal_types; + int m_display_goal_types = 0; sexp_tree* _sexp_tree = nullptr; }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionGoalsDialog.cpp b/qtfred/src/ui/dialogs/MissionGoalsDialog.cpp index 022e2519570..bc39397a0f2 100644 --- a/qtfred/src/ui/dialogs/MissionGoalsDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionGoalsDialog.cpp @@ -5,109 +5,22 @@ #include "mission/util.h" #include "ui_MissionGoalsDialog.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { -MissionGoalsDialog::MissionGoalsDialog(QWidget* parent, EditorViewport* viewport) : - QDialog(parent), - SexpTreeEditorInterface({ TreeFlags::LabeledRoot, TreeFlags::RootDeletable }), - ui(new Ui::MissionGoalsDialog()), - _model(new MissionGoalsDialogModel(this, viewport)), - _viewport(viewport) +MissionGoalsDialog::MissionGoalsDialog(QWidget* parent, EditorViewport* viewport) + : QDialog(parent), SexpTreeEditorInterface({TreeFlags::LabeledRoot, TreeFlags::RootDeletable}), + ui(new Ui::MissionGoalsDialog()), _model(new MissionGoalsDialogModel(this, viewport)), _viewport(viewport) { - ui->setupUi(this); + ui->setupUi(this); - ui->goalEventTree->initializeEditor(viewport->editor, this); - _model->setTreeControl(ui->goalEventTree); + ui->goalEventTree->initializeEditor(viewport->editor, this); + _model->setTreeControl(ui->goalEventTree); ui->goalName->setMaxLength(NAME_LENGTH - 1); ui->helpTextBox->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); - connect(this, &QDialog::accepted, _model.get(), &MissionGoalsDialogModel::apply); - connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &MissionGoalsDialog::rejectHandler); - - connect(_model.get(), &MissionGoalsDialogModel::modelChanged, this, &MissionGoalsDialog::updateUI); - - connect(ui->displayTypeCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int index) { - _model->setGoalDisplayType(index); - recreate_tree(); - }); - - connect(ui->goalEventTree, &sexp_tree::selectedRootChanged, this, [this](int forumla) { - auto& goals = _model->getGoals(); - for (auto i = 0; i < (int)goals.size(); ++i) { - if (goals[i].formula == forumla) { - _model->setCurrentGoal(i); - break; - } - } - }); - connect(ui->goalEventTree, &sexp_tree::rootNodeDeleted, this, [this](int node) { - _model->deleteGoal(node); - }); - connect(ui->goalEventTree, &sexp_tree::rootNodeFormulaChanged, this, [this](int old, int node) { - _model->changeFormula(old, node); - }); - connect(ui->goalEventTree, &sexp_tree::helpChanged, this, [this](const QString& help) { - ui->helpTextBox->setPlainText(help); - }); - - connect(ui->newObjectiveBtn, &QPushButton::clicked, this, [this](bool) { - createNewObjective(); - }); - - connect(ui->goalDescription, &QLineEdit::textChanged, this, [this](const QString& text) { - if (_model->isCurrentGoalValid()) { - _model->setCurrentGoalMessage(text.toUtf8().constData()); - } - }); - - connect(ui->goalScore, QOverload::of(&QSpinBox::valueChanged), this, [this](int value) { - if (_model->isCurrentGoalValid()) { - _model->setCurrentGoalScore(value); - } - }); - - connect(ui->goalTypeCombo, - QOverload::of(&QComboBox::currentIndexChanged), - this, - &MissionGoalsDialog::changeGoalCategory); - - connect(ui->goalName, &QLineEdit::textChanged, this, [this](const QString& text) { - if (_model->isCurrentGoalValid()) { - _model->setCurrentGoalName(text.toUtf8().constData()); - - auto item = ui->goalEventTree->currentItem(); - while (item->parent() != nullptr) { - item = item->parent(); - } - - item->setText(0, text); - } - }); - - connect(ui->objectiveInvalidCheck, &QCheckBox::stateChanged, this, [this](int state) { - bool checked = state == Qt::Checked; - - if (_model->isCurrentGoalValid()) { - _model->setCurrentGoalInvalid(checked); - } - }); - connect(ui->noCompletionMusicCheck, &QCheckBox::stateChanged, this, [this](int state) { - bool checked = state == Qt::Checked; - - if (_model->isCurrentGoalValid()) { - _model->setCurrentGoalNoMusic(checked); - } - }); - - connect(ui->goalTeamCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int team) { - if (_model->isCurrentGoalValid()) { - _model->setCurrentGoalTeam(team); - } - }); + connect(_model.get(), &MissionGoalsDialogModel::modelChanged, this, &MissionGoalsDialog::updateUi); _model->initializeData(); @@ -115,10 +28,37 @@ MissionGoalsDialog::MissionGoalsDialog(QWidget* parent, EditorViewport* viewport recreate_tree(); } -MissionGoalsDialog::~MissionGoalsDialog() + +MissionGoalsDialog::~MissionGoalsDialog() = default; + +void MissionGoalsDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void MissionGoalsDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close +} + +void MissionGoalsDialog::closeEvent(QCloseEvent* e) { + reject(); + e->ignore(); // Don't let the base class close the window } -void MissionGoalsDialog::updateUI() { + +void MissionGoalsDialog::updateUi() +{ // Avoid infinite recursion by blocking signal calls caused by our changes here util::SignalBlockers blocker(this); @@ -157,18 +97,20 @@ void MissionGoalsDialog::updateUI() { ui->noCompletionMusicCheck->setEnabled(true); ui->goalTeamCombo->setEnabled((The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) != 0); } -void MissionGoalsDialog::load_tree() { +void MissionGoalsDialog::load_tree() +{ ui->goalEventTree->clear_tree(); auto& goals = _model->getGoals(); - for (auto &goal: goals) { + for (auto& goal : goals) { goal.formula = ui->goalEventTree->load_sub_tree(goal.formula, true, "true"); } ui->goalEventTree->post_load(); } -void MissionGoalsDialog::recreate_tree() { +void MissionGoalsDialog::recreate_tree() +{ ui->goalEventTree->clear(); const auto& goals = _model->getGoals(); - for (const auto& goal: goals) { + for (const auto& goal : goals) { if (!_model->isGoalVisible(goal)) { continue; } @@ -180,7 +122,8 @@ void MissionGoalsDialog::recreate_tree() { _model->setCurrentGoal(-1); } -void MissionGoalsDialog::createNewObjective() { +void MissionGoalsDialog::createNewObjective() +{ auto& goal = _model->createNewGoal(); auto h = ui->goalEventTree->insert(goal.name.c_str()); @@ -192,21 +135,111 @@ void MissionGoalsDialog::createNewObjective() { ui->goalEventTree->setCurrentItem(h); } -void MissionGoalsDialog::changeGoalCategory(int type) { +void MissionGoalsDialog::changeGoalCategory(int type) +{ if (_model->isCurrentGoalValid()) { _model->setCurrentGoalCategory(type); recreate_tree(); } } -void MissionGoalsDialog::closeEvent(QCloseEvent* e) { - if (!rejectOrCloseHandler(this, _model.get(), _viewport)) { - e->ignore(); - }; + +void MissionGoalsDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void MissionGoalsDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void MissionGoalsDialog::on_displayTypeCombo_currentIndexChanged(int index) +{ + _model->setGoalDisplayType(index); + recreate_tree(); +} + +void MissionGoalsDialog::on_goalTypeCombo_currentIndexChanged(int index) +{ + changeGoalCategory(index); +} + +void MissionGoalsDialog::on_goalName_textChanged(const QString& text) +{ + if (_model->isCurrentGoalValid()) { + _model->setCurrentGoalName(text.toUtf8().constData()); + + auto item = ui->goalEventTree->currentItem(); + while (item->parent() != nullptr) { + item = item->parent(); + } + + item->setText(0, text); + } +} + +void MissionGoalsDialog::on_goalDescription_textChanged(const QString& text) +{ + _model->setCurrentGoalMessage(text.toUtf8().constData()); +} + +void MissionGoalsDialog::on_goalScore_valueChanged(int value) +{ + if (_model->isCurrentGoalValid()) { + _model->setCurrentGoalScore(value); + } +} + +void MissionGoalsDialog::on_goalTeamCombo_currentIndexChanged(int team) +{ + if (_model->isCurrentGoalValid()) { + _model->setCurrentGoalTeam(team); + } +} + +void MissionGoalsDialog::on_objectiveInvalidCheck_stateChanged(bool checked) +{ + if (_model->isCurrentGoalValid()) { + _model->setCurrentGoalInvalid(checked); + } } -void MissionGoalsDialog::rejectHandler() + +void MissionGoalsDialog::on_noCompletionMusicCheck_stateChanged(bool checked) { - this->close(); + if (_model->isCurrentGoalValid()) { + _model->setCurrentGoalNoMusic(checked); + } } + +void MissionGoalsDialog::on_newObjectiveBtn_clicked() +{ + createNewObjective(); } + +void MissionGoalsDialog::on_goalEventTree_selectedRootChanged(int formula) +{ + auto& goals = _model->getGoals(); + for (size_t i = 0; i < goals.size(); ++i) { + if (goals[i].formula == formula) { + _model->setCurrentGoal(static_cast(i)); + break; + } + } } + +void MissionGoalsDialog::on_goalEventTree_rootNodeDeleted(int node) +{ + _model->deleteGoal(node); } + +void MissionGoalsDialog::on_goalEventTree_rootNodeFormulaChanged(int old, int node) +{ + _model->changeFormula(old, node); +} + +void MissionGoalsDialog::on_goalEventTree_helpChanged(const QString& help) +{ + ui->helpTextBox->setPlainText(help); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionGoalsDialog.h b/qtfred/src/ui/dialogs/MissionGoalsDialog.h index 8cbe7a3d150..88730336eb1 100644 --- a/qtfred/src/ui/dialogs/MissionGoalsDialog.h +++ b/qtfred/src/ui/dialogs/MissionGoalsDialog.h @@ -1,5 +1,4 @@ -#ifndef MISSIONGOALSDIALOG_H -#define MISSIONGOALSDIALOG_H +#pragma once #include @@ -8,11 +7,7 @@ #include "ui/widgets/sexp_tree.h" -#include - -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class MissionGoalsDialog; @@ -26,15 +21,34 @@ class MissionGoalsDialog : public QDialog, public SexpTreeEditorInterface explicit MissionGoalsDialog(QWidget *parent, EditorViewport* viewport); ~MissionGoalsDialog() override; + void accept() override; + void reject() override; + protected: void closeEvent(QCloseEvent* event) override; - void rejectHandler(); - - private: - void updateUI(); +private slots: + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + void on_displayTypeCombo_currentIndexChanged(int index); + void on_goalTypeCombo_currentIndexChanged(int index); + void on_goalName_textChanged(const QString& text); + void on_goalDescription_textChanged(const QString& text); + void on_goalScore_valueChanged(int value); + void on_goalTeamCombo_currentIndexChanged(int team); + void on_objectiveInvalidCheck_stateChanged(bool checked); + void on_noCompletionMusicCheck_stateChanged(bool checked); + void on_newObjectiveBtn_clicked(); + + void on_goalEventTree_selectedRootChanged(int formula); + void on_goalEventTree_rootNodeDeleted(int node); + void on_goalEventTree_rootNodeFormulaChanged(int old, int node); + void on_goalEventTree_helpChanged(const QString& help); + + private: // NOLINT(readability-redundant-access-specifiers) + void updateUi(); void createNewObjective(); - void changeGoalCategory(int type); std::unique_ptr ui; @@ -45,8 +59,4 @@ class MissionGoalsDialog : public QDialog, public SexpTreeEditorInterface void recreate_tree(); }; -} -} -} - -#endif // MISSIONGOALSDIALOG_H +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/MissionGoalsDialog.ui b/qtfred/ui/MissionGoalsDialog.ui index 3d41d59779e..0adae32a084 100644 --- a/qtfred/ui/MissionGoalsDialog.ui +++ b/qtfred/ui/MissionGoalsDialog.ui @@ -108,7 +108,11 @@ - + + + 16777215 + + @@ -187,7 +191,7 @@ - + Qt::Horizontal @@ -254,22 +258,5 @@ - - - buttonBox - accepted() - fso::fred::dialogs::MissionGoalsDialog - accept() - - - 506 - 113 - - - 157 - 274 - - - - +
From 4b9ccfc68fee5a757903e78bfba7e71c06bb1ac2 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sat, 23 Aug 2025 10:20:38 -0500 Subject: [PATCH 406/466] QtFRED Asteroid Dialog (#6908) * begin asteroid dialog rewrite * activate fields * clean up connections * clean up and rearrange files for consistency * use constructor to zero out the field when needed * fix checks errors * remove duplicate lines * add missing z --- code/asteroid/asteroid.cpp | 13 +- code/asteroid/asteroid.h | 22 +- .../dialogs/AsteroidEditorDialogModel.cpp | 560 +++++++++--------- .../dialogs/AsteroidEditorDialogModel.h | 126 ++-- .../src/ui/dialogs/AsteroidEditorDialog.cpp | 440 +++++++------- qtfred/src/ui/dialogs/AsteroidEditorDialog.h | 104 ++-- qtfred/ui/AsteroidEditorDialog.ui | 478 ++++++++------- 7 files changed, 909 insertions(+), 834 deletions(-) diff --git a/code/asteroid/asteroid.cpp b/code/asteroid/asteroid.cpp index a33cdd703e9..6fedf4daa0c 100644 --- a/code/asteroid/asteroid.cpp +++ b/code/asteroid/asteroid.cpp @@ -823,18 +823,7 @@ void asteroid_create_debris_field(int num_asteroids, int asteroid_speed, SCP_vec */ void asteroid_level_init() { - Asteroid_field.num_initial_asteroids = 0; // disable asteroid field by default. - Asteroid_field.speed = 0.0f; - vm_vec_make(&Asteroid_field.min_bound, -1000.0f, -1000.0f, -1000.0f); - vm_vec_make(&Asteroid_field.max_bound, 1000.0f, 1000.0f, 1000.0f); - vm_vec_make(&Asteroid_field.inner_min_bound, -500.0f, -500.0f, -500.0f); - vm_vec_make(&Asteroid_field.inner_max_bound, 500.0f, 500.0f, 500.0f); - Asteroid_field.has_inner_bound = false; - Asteroid_field.field_type = FT_ACTIVE; - Asteroid_field.debris_genre = DG_ASTEROID; - Asteroid_field.field_debris_type.clear(); - Asteroid_field.field_asteroid_type.clear(); - Asteroid_field.target_names.clear(); + Asteroid_field = {}; if (!Fred_running) { diff --git a/code/asteroid/asteroid.h b/code/asteroid/asteroid.h index 4c1789fdd72..31781b5c978 100644 --- a/code/asteroid/asteroid.h +++ b/code/asteroid/asteroid.h @@ -17,6 +17,7 @@ #include "object/object_flags.h" #include "io/timer.h" #include "particle/ParticleSource.h" +#include "math/vecmat.h" class object; class polymodel; @@ -133,7 +134,7 @@ typedef enum { FT_PASSIVE } field_type_t; -typedef struct asteroid_field { +struct asteroid_field { vec3d min_bound; // Minimum range of field. vec3d max_bound; // Maximum range of field. float bound_rad; @@ -150,7 +151,24 @@ typedef struct asteroid_field { bool enhanced_visibility_checks; // if true then range checks are overridden for spawning and wrapping asteroids in the field SCP_vector target_names; // default retail behavior is to just throw at the first big ship in the field -} asteroid_field; + + asteroid_field() + { + num_initial_asteroids = 0; // disable the field by default + speed = 0.0f; + vm_vec_make(&min_bound, -1000.0f, -1000.0f, -1000.0f); + vm_vec_make(&max_bound, 1000.0f, 1000.0f, 1000.0f); + vm_vec_make(&inner_min_bound, -500.0f, -500.0f, -500.0f); + vm_vec_make(&inner_max_bound, 500.0f, 500.0f, 500.0f); + has_inner_bound = false; + field_type = FT_ACTIVE; + debris_genre = DG_ASTEROID; + enhanced_visibility_checks = false; + bound_rad = 0.0f; + vel = ZERO_VECTOR; + // the vectors default-construct to empty + } +}; extern SCP_vector< asteroid_info > Asteroid_info; extern asteroid Asteroids[MAX_ASTEROIDS]; diff --git a/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.cpp b/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.cpp index ca2ff87720a..4178e8c5412 100644 --- a/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.cpp @@ -1,16 +1,17 @@ #include "mission/dialogs/AsteroidEditorDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { AsteroidEditorDialogModel::AsteroidEditorDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport), + _bypass_errors(false), _enable_asteroids(false), _enable_inner_bounds(false), _enable_enhanced_checking(false), - _num_asteroids(0), - _avg_speed(0), + _field_type(FT_ACTIVE), + _debris_genre(DG_ASTEROID), + _num_asteroids(1), + _avg_speed(""), _min_x(""), _min_y(""), _min_z(""), @@ -22,26 +23,14 @@ AsteroidEditorDialogModel::AsteroidEditorDialogModel(QObject* parent, EditorView _inner_min_z(""), _inner_max_x(""), _inner_max_y(""), - _inner_max_z(""), - _field_type(FT_ACTIVE), - _debris_genre(DG_ASTEROID), - _bypass_errors(false), - _cur_field(0), - _last_field(-1) + _inner_max_z("") { - for (auto i = 0ul; i < ship_debris_idx_lookup.size(); ++i) { - debris_inverse_idx_lookup.emplace(ship_debris_idx_lookup[i], i); - } - // note that normal asteroids use the same index field! Need to add dummy entries for them as well - for (auto i = 0; i < NUM_ASTEROID_SIZES; ++i) { - debris_inverse_idx_lookup.emplace(i, 0); - } initializeData(); } bool AsteroidEditorDialogModel::apply() { - update_init(); + update_internal_field(); if (!AsteroidEditorDialogModel::validate_data()) { return false; } @@ -56,191 +45,119 @@ void AsteroidEditorDialogModel::reject() void AsteroidEditorDialogModel::initializeData() { - for (auto& i : _field_debris_type) { - i = -1; - } - - _a_field = Asteroid_field; -} - -void AsteroidEditorDialogModel::setEnabled(bool enabled) -{ - _enable_asteroids = enabled; -} - -bool AsteroidEditorDialogModel::getEnabled() -{ - return _enable_asteroids; -} - -void AsteroidEditorDialogModel::setInnerBoxEnabled(bool enabled) -{ - _enable_inner_bounds = enabled; -} - -bool AsteroidEditorDialogModel::getInnerBoxEnabled() -{ - return _enable_inner_bounds; -} + _a_field = Asteroid_field; // copy the current asteroid field data -void AsteroidEditorDialogModel::setEnhancedEnabled(bool enabled) -{ - _enable_enhanced_checking = enabled; -} - -bool AsteroidEditorDialogModel::getEnhancedEnabled() -{ - return _enable_enhanced_checking; -} - -void AsteroidEditorDialogModel::setAsteroidEnabled(_roid_types type, bool enabled) -{ - Assertion(type >=0 && type < NUM_ASTEROID_SIZES, "Invalid Asteroid checkbox type: %i\n", type); + // Now initialize the model data from the asteroid field + _enable_asteroids = (_a_field.num_initial_asteroids > 0); + _enable_inner_bounds = _a_field.has_inner_bound; + _enable_enhanced_checking = _a_field.enhanced_visibility_checks; - SCP_string name = "Brown"; - if (type == _AST_BLUE) { - name = "Blue"; - } else if (type == _AST_ORANGE) { - name = "Orange"; + _field_type = _a_field.field_type; + _debris_genre = _a_field.debris_genre; + + _num_asteroids = _a_field.num_initial_asteroids; + if (!_enable_asteroids) { + _num_asteroids = 1; // fallback } - bool in_list = false; - for (const auto& asteroid : _field_asteroid_type) { - if (name == asteroid) { - in_list = true; - } - } + CLAMP(_num_asteroids, 1, MAX_ASTEROIDS); - // If enabling and it's not enabled then add it - if (enabled && !in_list) { - _field_asteroid_type.push_back(name); - } + _avg_speed = QString::number(static_cast(vm_vec_mag(&_a_field.vel))); - // If disabling and it's in the lsit then remove it - if (!enabled && in_list) { - _field_asteroid_type.erase(std::remove(_field_asteroid_type.begin(), _field_asteroid_type.end(), name), _field_asteroid_type.end()); - } -} + // Convert coords to strings + _min_x = QString::number(_a_field.min_bound.xyz.x, 'f', 1); + _min_y = QString::number(_a_field.min_bound.xyz.y, 'f', 1); + _min_z = QString::number(_a_field.min_bound.xyz.z, 'f', 1); + _max_x = QString::number(_a_field.max_bound.xyz.x, 'f', 1); + _max_y = QString::number(_a_field.max_bound.xyz.y, 'f', 1); + _max_z = QString::number(_a_field.max_bound.xyz.z, 'f', 1); + _inner_min_x = QString::number(_a_field.inner_min_bound.xyz.x, 'f', 1); + _inner_min_y = QString::number(_a_field.inner_min_bound.xyz.y, 'f', 1); + _inner_min_z = QString::number(_a_field.inner_min_bound.xyz.z, 'f', 1); + _inner_max_x = QString::number(_a_field.inner_max_bound.xyz.x, 'f', 1); + _inner_max_y = QString::number(_a_field.inner_max_bound.xyz.y, 'f', 1); + _inner_max_z = QString::number(_a_field.inner_max_bound.xyz.z, 'f', 1); -bool AsteroidEditorDialogModel::getAsteroidEnabled(_roid_types type) -{ - Assertion(type >=0 && type < NUM_ASTEROID_SIZES, "Invalid Asteroid checkbox type: %i\n", type); + // Copy the object lists + _field_debris_type = _a_field.field_debris_type; + _field_asteroid_type = _a_field.field_asteroid_type; + _field_target_names = _a_field.target_names; - SCP_string name = "Brown"; - if (type == _AST_BLUE) { - name = "Blue"; - } else if (type == _AST_ORANGE) { - name = "Orange"; + // Initialize asteroid options + const auto& list = get_list_valid_asteroid_subtypes(); + for (const auto& name : list) { + asteroidOptions.push_back(name); } - bool enabled = false; - for (auto asteroid : _field_asteroid_type) { - if (name == asteroid) { - enabled = true; + // Initialize debris options + for (size_t i = 0; i < Asteroid_info.size(); ++i) { + if (Asteroid_info[i].type == -1) { + debrisOptions.emplace_back(std::make_pair(Asteroid_info[i].name, static_cast(i))); } } - return (enabled); -} - -void AsteroidEditorDialogModel::setNumAsteroids(int num_asteroids) -{ - modify(_num_asteroids, num_asteroids); } -int AsteroidEditorDialogModel::getNumAsteroids() +void AsteroidEditorDialogModel::update_internal_field() { - return _num_asteroids; -} - -QString & AsteroidEditorDialogModel::getBoxText(_box_line_edits type) -{ - switch (type) { - case _O_MIN_X: return _min_x; - case _O_MIN_Y: return _min_y; - case _O_MIN_Z: return _min_z; - case _O_MAX_X: return _max_x; - case _O_MAX_Y: return _max_y; - case _O_MAX_Z: return _max_z; - case _I_MIN_X: return _inner_min_x; - case _I_MIN_Y: return _inner_min_y; - case _I_MIN_Z: return _inner_min_z; - case _I_MAX_X: return _inner_max_x; - case _I_MAX_Y: return _inner_max_y; - case _I_MAX_Z: return _inner_max_z; - default: - UNREACHABLE("Unknown asteroid coordinates enum value found (%i); Get a coder! ", type); - return _min_x; + // if asteroids are not enabled, just clear the field and return + if (!_enable_asteroids) { + _a_field = {}; + return; } -} - -void AsteroidEditorDialogModel::setBoxText(const QString &text, _box_line_edits type) -{ - switch (type) { - case _O_MIN_X: modify(_min_x, text); break; - case _O_MIN_Y: modify(_min_y, text); break; - case _O_MIN_Z: modify(_min_z, text); break; - case _O_MAX_X: modify(_max_x, text); break; - case _O_MAX_Y: modify(_max_y, text); break; - case _O_MAX_Z: modify(_max_z, text); break; - case _I_MIN_X: modify(_inner_min_x, text); break; - case _I_MIN_Y: modify(_inner_min_y, text); break; - case _I_MIN_Z: modify(_inner_min_z, text); break; - case _I_MAX_X: modify(_inner_max_x, text); break; - case _I_MAX_Y: modify(_inner_max_y, text); break; - case _I_MAX_Z: modify(_inner_max_z, text); break; - default: - Error(LOCATION, "Get a coder! Unknown enum value found! %i", type); - break; + + // Do some quick data conversion + int num_asteroids = _enable_asteroids ? _num_asteroids : 0; + CLAMP(num_asteroids, 0, MAX_ASTEROIDS); + vec3d vel_vec = vmd_x_vector; + vm_vec_scale(&vel_vec, static_cast(_avg_speed.toInt())); + + // Now update the asteroid field with the current values + _a_field.has_inner_bound = _enable_inner_bounds; + _a_field.enhanced_visibility_checks = _enable_enhanced_checking; + + _a_field.field_type = _field_type; + _a_field.debris_genre = _debris_genre; + + _a_field.num_initial_asteroids = num_asteroids; + _a_field.vel = vel_vec; + + // save the box coords + _a_field.min_bound.xyz.x = _min_x.toFloat(); + _a_field.min_bound.xyz.y = _min_y.toFloat(); + _a_field.min_bound.xyz.z = _min_z.toFloat(); + _a_field.max_bound.xyz.x = _max_x.toFloat(); + _a_field.max_bound.xyz.y = _max_y.toFloat(); + _a_field.max_bound.xyz.z = _max_z.toFloat(); + + if (_enable_inner_bounds) { + _a_field.inner_min_bound.xyz.x = _inner_min_x.toFloat(); + _a_field.inner_min_bound.xyz.y = _inner_min_y.toFloat(); + _a_field.inner_min_bound.xyz.z = _inner_min_z.toFloat(); + _a_field.inner_max_bound.xyz.x = _inner_max_x.toFloat(); + _a_field.inner_max_bound.xyz.y = _inner_max_y.toFloat(); + _a_field.inner_max_bound.xyz.z = _inner_max_z.toFloat(); } -} -void AsteroidEditorDialogModel::setDebrisGenre(debris_genre_t genre) -{ - modify(_debris_genre, genre); -} + // clear the lists + _a_field.field_debris_type.clear(); + _a_field.field_asteroid_type.clear(); + _a_field.target_names.clear(); -debris_genre_t AsteroidEditorDialogModel::getDebrisGenre() -{ - return _debris_genre; -} - -void AsteroidEditorDialogModel::setFieldType(field_type_t type) -{ - modify(_field_type, type); -} - -field_type_t AsteroidEditorDialogModel::getFieldType() -{ - return _field_type; -} - -void AsteroidEditorDialogModel::setFieldDebrisType(int idx, int debris_type) -{ - if (!SCP_vector_inbounds(_field_debris_type, idx)) { - _field_debris_type.push_back(ship_debris_idx_lookup.at(debris_type)); - } else { - modify(_field_debris_type[idx], ship_debris_idx_lookup.at(debris_type)); + // debris + if ((_field_type == FT_PASSIVE) && (_debris_genre == DG_DEBRIS)) { + _a_field.field_debris_type = _field_debris_type; } -} -int AsteroidEditorDialogModel::getFieldDebrisType(int idx) -{ - if (!SCP_vector_inbounds(_field_debris_type, idx)) { - return 0; - } else { - return debris_inverse_idx_lookup.at(_field_debris_type[idx]); - } -} + // asteroids + if (_debris_genre == DG_ASTEROID) { + _a_field.field_asteroid_type = _field_asteroid_type; -void AsteroidEditorDialogModel::setAvgSpeed(int speed) -{ - modify(_avg_speed, speed); -} - -QString AsteroidEditorDialogModel::getAvgSpeed() -{ - return QString::number(_avg_speed); + // target ships + if (_field_type == FT_ACTIVE) { + _a_field.target_names = _field_target_names; + } + } } bool AsteroidEditorDialogModel::validate_data() @@ -364,14 +281,6 @@ bool AsteroidEditorDialogModel::validate_data() } } - // Compress the debris field vector - if (_a_field.field_debris_type.size() > 0) { - _a_field.field_debris_type.erase(std::remove_if(_a_field.field_debris_type.begin(), - _a_field.field_debris_type.end(), - [](int value) { return value < 0; }), - _a_field.field_debris_type.end()); - } - // for a ship debris (i.e. passive) field, need at least one debris type is selected if (_a_field.field_type == FT_PASSIVE) { if (_a_field.debris_genre == DG_DEBRIS) { @@ -395,117 +304,208 @@ bool AsteroidEditorDialogModel::validate_data() return true; } -void AsteroidEditorDialogModel::update_init() +void AsteroidEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) { - int num_asteroids; + if (_bypass_errors) { + return; + } - if (_last_field >= 0) { - // store into temp asteroid field - num_asteroids = _a_field.num_initial_asteroids; - _a_field.num_initial_asteroids = _enable_asteroids ? _num_asteroids : 0; - CLAMP(_a_field.num_initial_asteroids, 0, MAX_ASTEROIDS); + _bypass_errors = true; + _viewport->dialogProvider->showButtonDialog(DialogType::Error, + "Error", + message, + { DialogButton::Ok }); +} - if (num_asteroids != _a_field.num_initial_asteroids) { - set_modified(); - } +void AsteroidEditorDialogModel::setFieldEnabled(bool enabled) +{ + modify(_enable_asteroids, enabled); +} - vec3d vel_vec = vmd_x_vector; - vm_vec_scale(&vel_vec, static_cast(_avg_speed)); - modify(_a_field.vel, vel_vec); - - // save the box coords - modify(_a_field.min_bound.xyz.x, _min_x.toFloat()); - modify(_a_field.min_bound.xyz.y, _min_y.toFloat()); - modify(_a_field.min_bound.xyz.z, _min_z.toFloat()); - modify(_a_field.max_bound.xyz.x, _max_x.toFloat()); - modify(_a_field.max_bound.xyz.y, _max_y.toFloat()); - modify(_a_field.max_bound.xyz.z, _max_z.toFloat()); - modify(_a_field.inner_min_bound.xyz.x, _inner_min_x.toFloat()); - modify(_a_field.inner_min_bound.xyz.y, _inner_min_y.toFloat()); - modify(_a_field.inner_min_bound.xyz.z, _inner_min_z.toFloat()); - modify(_a_field.inner_max_bound.xyz.x, _inner_max_x.toFloat()); - modify(_a_field.inner_max_bound.xyz.y, _inner_max_y.toFloat()); - modify(_a_field.inner_max_bound.xyz.z, _inner_max_z.toFloat()); - - // type of field - modify(_a_field.field_type, _field_type); - modify(_a_field.debris_genre, _debris_genre); - - // debris - if ( (_field_type == FT_PASSIVE) && (_debris_genre == DG_DEBRIS) ) { - for (size_t idx = 0; idx < _field_debris_type.size(); ++idx) { - if (SCP_vector_inbounds(_a_field.field_debris_type, idx)) { - modify(_a_field.field_debris_type[idx], _field_debris_type[idx]); - } else { - _a_field.field_debris_type.push_back(_field_debris_type[idx]); - } - } - } +bool AsteroidEditorDialogModel::getFieldEnabled() const +{ + return _enable_asteroids; +} - // asteroids - if ( _debris_genre == DG_ASTEROID ) { - for (size_t idx = 0; idx < _field_asteroid_type.size(); ++idx) { - if (SCP_vector_inbounds(_a_field.field_asteroid_type, idx)) { - modify(_a_field.field_asteroid_type[idx], _field_asteroid_type[idx]); - } else { - _a_field.field_asteroid_type.push_back(_field_asteroid_type[idx]); - } - } +void AsteroidEditorDialogModel::setInnerBoxEnabled(bool enabled) +{ + modify(_enable_inner_bounds, enabled); +} + +bool AsteroidEditorDialogModel::getInnerBoxEnabled() const +{ + return _enable_inner_bounds; +} + +void AsteroidEditorDialogModel::setEnhancedEnabled(bool enabled) +{ + modify(_enable_enhanced_checking, enabled); +} + +bool AsteroidEditorDialogModel::getEnhancedEnabled() const +{ + return _enable_enhanced_checking; +} + +void AsteroidEditorDialogModel::setFieldType(field_type_t type) +{ + modify(_field_type, type); +} + +field_type_t AsteroidEditorDialogModel::getFieldType() +{ + return _field_type; +} + +void AsteroidEditorDialogModel::setDebrisGenre(debris_genre_t genre) +{ + modify(_debris_genre, genre); +} + +debris_genre_t AsteroidEditorDialogModel::getDebrisGenre() +{ + return _debris_genre; +} + +void AsteroidEditorDialogModel::setNumAsteroids(int num_asteroids) +{ + modify(_num_asteroids, num_asteroids); +} + +int AsteroidEditorDialogModel::getNumAsteroids() const +{ + return _num_asteroids; +} + +void AsteroidEditorDialogModel::setAvgSpeed(const QString& speed) +{ + modify(_avg_speed, speed); +} + +QString& AsteroidEditorDialogModel::getAvgSpeed() +{ + return _avg_speed; +} + +void AsteroidEditorDialogModel::setBoxText(const QString &text, _box_line_edits type) +{ + switch (type) { + case _O_MIN_X: modify(_min_x, text); break; + case _O_MIN_Y: modify(_min_y, text); break; + case _O_MIN_Z: modify(_min_z, text); break; + case _O_MAX_X: modify(_max_x, text); break; + case _O_MAX_Y: modify(_max_y, text); break; + case _O_MAX_Z: modify(_max_z, text); break; + case _I_MIN_X: modify(_inner_min_x, text); break; + case _I_MIN_Y: modify(_inner_min_y, text); break; + case _I_MIN_Z: modify(_inner_min_z, text); break; + case _I_MAX_X: modify(_inner_max_x, text); break; + case _I_MAX_Y: modify(_inner_max_y, text); break; + case _I_MAX_Z: modify(_inner_max_z, text); break; + default: + Error(LOCATION, "Get a coder! Unknown enum value found! %i", type); + break; + } +} + +QString & AsteroidEditorDialogModel::getBoxText(_box_line_edits type) +{ + switch (type) { + case _O_MIN_X: return _min_x; + case _O_MIN_Y: return _min_y; + case _O_MIN_Z: return _min_z; + case _O_MAX_X: return _max_x; + case _O_MAX_Y: return _max_y; + case _O_MAX_Z: return _max_z; + case _I_MIN_X: return _inner_min_x; + case _I_MIN_Y: return _inner_min_y; + case _I_MIN_Z: return _inner_min_z; + case _I_MAX_X: return _inner_max_x; + case _I_MAX_Y: return _inner_max_y; + case _I_MAX_Z: return _inner_max_z; + default: + UNREACHABLE("Unknown asteroid coordinates enum value found (%i); Get a coder! ", type); + return _min_x; + } +} + +void AsteroidEditorDialogModel::setAsteroidSelections(const QVector& selected) +{ + SCP_vector selectedTypes; + for (size_t i = 0; i < asteroidOptions.size(); ++i) { + if (selected.at(static_cast(i))) { + selectedTypes.push_back(asteroidOptions[i]); } + } - modify(_a_field.has_inner_bound, _enable_inner_bounds); + modify(_field_asteroid_type, selectedTypes); +} - modify(_a_field.enhanced_visibility_checks, _enable_enhanced_checking); +QVector> AsteroidEditorDialogModel::getAsteroidSelections() const +{ + QVector> options; + for (const auto& name : asteroidOptions) { + bool enabled = SCP_vector_contains(_field_asteroid_type, name); + options.append({QString::fromStdString(name), enabled}); } + return options; +} - // get from temp asteroid field into class - _enable_asteroids = _a_field.num_initial_asteroids ? true : false; - _enable_inner_bounds = _a_field.has_inner_bound; - _num_asteroids = _a_field.num_initial_asteroids; - _enable_enhanced_checking = _a_field.enhanced_visibility_checks; - if (!_enable_asteroids) { - _num_asteroids = 10; +void AsteroidEditorDialogModel::setDebrisSelections(const QVector& selected) +{ + SCP_vector selectedTypes; + for (size_t i = 0; i < debrisOptions.size(); ++i) { + if (selected.at(static_cast(i))) { + selectedTypes.push_back(debrisOptions[i].second); + } } - // set field type - _field_type = _a_field.field_type; - _debris_genre = _a_field.debris_genre; + modify(_field_debris_type, selectedTypes); +} - _avg_speed = static_cast(vm_vec_mag(&_a_field.vel)); +QVector> AsteroidEditorDialogModel::getDebrisSelections() const +{ + QVector> options; + for (const auto& setting : debrisOptions) { + bool enabled = SCP_vector_contains(_field_debris_type, setting.second); + options.append({QString::fromStdString(setting.first), enabled}); + } + return options; +} - _min_x = QString::number(_a_field.min_bound.xyz.x, 'f', 1); - _min_y = QString::number(_a_field.min_bound.xyz.y, 'f', 1); - _min_z = QString::number(_a_field.min_bound.xyz.z, 'f', 1); - _max_x = QString::number(_a_field.max_bound.xyz.x, 'f', 1); - _max_y = QString::number(_a_field.max_bound.xyz.y, 'f', 1); - _max_z = QString::number(_a_field.max_bound.xyz.z, 'f', 1); - _inner_min_x = QString::number(_a_field.inner_min_bound.xyz.x, 'f', 1); - _inner_min_y = QString::number(_a_field.inner_min_bound.xyz.y, 'f', 1); - _inner_min_z = QString::number(_a_field.inner_min_bound.xyz.z, 'f', 1); - _inner_max_x = QString::number(_a_field.inner_max_bound.xyz.x, 'f', 1); - _inner_max_y = QString::number(_a_field.inner_max_bound.xyz.y, 'f', 1); - _inner_max_z = QString::number(_a_field.inner_max_bound.xyz.z, 'f', 1); +void AsteroidEditorDialogModel::setShipSelections(const QVector& selected) +{ + SCP_vector selectedTypes; - // ship debris or asteroids - _field_debris_type.clear(); - _field_debris_type = _a_field.field_debris_type; + for (size_t i = 0; i < shipOptions.size(); ++i) { + if (selected.at(static_cast(i))) { + selectedTypes.push_back(shipOptions[i]); + } + } - _last_field = _cur_field; + modify(_field_target_names, selectedTypes); + + // Now we can clear the shipOptions vector since we're done with it + shipOptions.clear(); } -void AsteroidEditorDialogModel::showErrorDialogNoCancel(const SCP_string& message) +QVector> AsteroidEditorDialogModel::getShipSelections() { - if (_bypass_errors) { - return; + // Ships can be placed while the Asteroid field editor is open so we need to initialize this every time + shipOptions.clear(); + for (auto& ship : Ships) { + if (ship.objnum >= 0) { + SCP_string name = ship.ship_name; + shipOptions.push_back(name); + } } - _bypass_errors = true; - _viewport->dialogProvider->showButtonDialog(DialogType::Error, - "Error", - message, - { DialogButton::Ok }); + QVector> options; + for (const auto& name : shipOptions) { + bool enabled = SCP_vector_contains(_field_target_names, name); + options.append({QString::fromStdString(name), enabled}); + } + return options; } -} // namespace dialogs -} // namespace fred -} // namespace fso +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.h b/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.h index ffe409d2521..b77e6f40f9d 100644 --- a/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/AsteroidEditorDialogModel.h @@ -4,9 +4,11 @@ #include "asteroid/asteroid.h" -namespace fso { -namespace fred { -namespace dialogs { +#include +#include +#include + +namespace fso::fred::dialogs { class AsteroidEditorDialogModel: public AbstractDialogModel { Q_OBJECT @@ -15,63 +17,90 @@ Q_OBJECT AsteroidEditorDialogModel(QObject* parent, EditorViewport* viewport); enum _box_line_edits { - _I_MIN_X =0, - _I_MIN_Y, - _I_MIN_Z, - _I_MAX_X, - _I_MAX_Y, - _I_MAX_Z, - _O_MIN_X, + _O_MIN_X = 0, _O_MIN_Y, _O_MIN_Z, _O_MAX_X, _O_MAX_Y, _O_MAX_Z, - }; - enum _roid_types { - _AST_BROWN =0, - _AST_BLUE =1, - _AST_ORANGE =2, + _I_MIN_X, + _I_MIN_Y, + _I_MIN_Z, + _I_MAX_X, + _I_MAX_Y, + _I_MAX_Z, }; + // overrides bool apply() override; void reject() override; - void setEnabled(bool enabled); - bool getEnabled(); + // toggles + void setFieldEnabled(bool enabled); + bool getFieldEnabled() const; + void setInnerBoxEnabled(bool enabled); - bool getInnerBoxEnabled(); + bool getInnerBoxEnabled() const; + void setEnhancedEnabled(bool enabled); - bool getEnhancedEnabled(); - void setAsteroidEnabled(_roid_types type, bool enabled); - bool getAsteroidEnabled(_roid_types type); - void setNumAsteroids(int num_asteroids); - int getNumAsteroids(); - void setDebrisGenre(debris_genre_t genre); - debris_genre_t getDebrisGenre(); + bool getEnhancedEnabled() const; + + // field types void setFieldType(field_type_t type); field_type_t getFieldType(); - void setFieldDebrisType(int idx, int num_asteroids); - int getFieldDebrisType(int idx); - void setAvgSpeed(int speed); - QString getAvgSpeed(); - void setBoxText(const QString &text, _box_line_edits type); - QString & getBoxText(_box_line_edits type); - - void update_init(); - bool validate_data(); + + void setDebrisGenre(debris_genre_t genre); + debris_genre_t getDebrisGenre(); + + // basic values + void setNumAsteroids(int num_asteroids); + int getNumAsteroids() const; + + void setAvgSpeed(const QString& speed); + QString& getAvgSpeed(); + + // box values + void setBoxText(const QString& text, _box_line_edits type); + QString& getBoxText(_box_line_edits type); + + // object selections + QVector> getAsteroidSelections() const; + void setAsteroidSelections(const QVector& selected); + + QVector> getDebrisSelections() const; + void setDebrisSelections(const QVector& selected); + + QVector> getShipSelections(); + void setShipSelections(const QVector& selected); private: - void showErrorDialogNoCancel(const SCP_string& message); void initializeData(); + void update_internal_field(); + bool validate_data(); + void showErrorDialogNoCancel(const SCP_string& message); + + // boilerplate + bool _bypass_errors; + const int _MIN_BOX_THICKNESS = 400; + + // working copy of the asteroid field + asteroid_field _a_field; + // toggles bool _enable_asteroids; bool _enable_inner_bounds; bool _enable_enhanced_checking; + + // field types + field_type_t _field_type; // active or passive + debris_genre_t _debris_genre; // debris or asteroid + + // basic values int _num_asteroids; - int _avg_speed; + QString _avg_speed; + // box values QString _min_x; QString _min_y; QString _min_z; @@ -85,24 +114,15 @@ Q_OBJECT QString _inner_max_y; QString _inner_max_z; - SCP_vector _field_debris_type; // debris + // object selections SCP_vector _field_asteroid_type; // asteroid types - field_type_t _field_type; // active or passive - debris_genre_t _debris_genre; // ship or asteroid - asteroid_field _a_field; // :v: had unfinished plans for multiple fields? + SCP_vector _field_debris_type; // debris types + SCP_vector _field_target_names; // target ships - bool _bypass_errors; - int _cur_field; - int _last_field; - - const int _MIN_BOX_THICKNESS = 400; - // for debris combo box indexes - // -1 == none, 3 == terran debris (small), etc to 11 == shivan debris (large) - const std::array ship_debris_idx_lookup{ {-1, 3, 4, 5, 6, 7, 8, 9, 10, 11} }; - // and the inverse as a map + roids - populate in ctor - std::unordered_map debris_inverse_idx_lookup; + // Helper vectors for the checkbox dialog + SCP_vector asteroidOptions; // asteroid options for the checkbox dialog + SCP_vector> debrisOptions; // debris options for the checkbox dialog.. for this one we include the index in the pair so we can use it to map + SCP_vector shipOptions; // ship options for the checkbox dialog }; -} // namespace dialogs -} // namespace fred -} // namespace fso +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/AsteroidEditorDialog.cpp b/qtfred/src/ui/dialogs/AsteroidEditorDialog.cpp index a49ec326639..31af12546c5 100644 --- a/qtfred/src/ui/dialogs/AsteroidEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/AsteroidEditorDialog.cpp @@ -1,4 +1,5 @@ #include "ui/dialogs/AsteroidEditorDialog.h" +#include "ui/dialogs/General/CheckBoxListDialog.h" #include "ui/util/SignalBlockers.h" #include @@ -6,15 +7,7 @@ #include "ui_AsteroidEditorDialog.h" #include -namespace fso { -namespace fred { -namespace dialogs { - -static bool sort_qcombobox_by_name(const QComboBox *left, const QComboBox *right) -{ - Assertion(left != nullptr && right != nullptr, "Don't pass nullptr's to sort!\n"); - return left->objectName() < right->objectName(); -} +namespace fso::fred::dialogs { AsteroidEditorDialog::AsteroidEditorDialog(FredView *parent, EditorViewport* viewport) : QDialog(parent), @@ -23,93 +16,17 @@ AsteroidEditorDialog::AsteroidEditorDialog(FredView *parent, EditorViewport* vie ui(new Ui::AsteroidEditorDialog()), _model(new AsteroidEditorDialogModel(this, viewport)) { - connect(this, &QDialog::accepted, _model.get(), &AsteroidEditorDialogModel::apply); - connect(ui->dialogButtonBox, &QDialogButtonBox::rejected, this, &AsteroidEditorDialog::rejectHandler); + this->setFocus(); ui->setupUi(this); - _model->update_init(); - - // checkboxes - connect(ui->enabled, &QCheckBox::toggled, this, &AsteroidEditorDialog::toggleEnabled); - connect(ui->innerBoxEnabled, &QCheckBox::toggled, this, &AsteroidEditorDialog::toggleInnerBoxEnabled); - - connect(ui->enhancedFieldEnabled, &QCheckBox::toggled, this, &AsteroidEditorDialog::toggleEnhancedEnabled); - connect(ui->checkBoxBrown, &QCheckBox::toggled, this, - [this](bool enabled) { \ - AsteroidEditorDialog::toggleAsteroid(AsteroidEditorDialogModel::_AST_BROWN, enabled); }); - connect(ui->checkBoxBlue, &QCheckBox::toggled, this, - [this](bool enabled) { \ - AsteroidEditorDialog::toggleAsteroid(AsteroidEditorDialogModel::_AST_BLUE, enabled); }); - connect(ui->checkBoxOrange, &QCheckBox::toggled, this, - [this](bool enabled) { \ - AsteroidEditorDialog::toggleAsteroid(AsteroidEditorDialogModel::_AST_ORANGE, enabled); }); - - // (come in) spinners - ui->spinBoxNumber->setRange(1, MAX_ASTEROIDS); - ui->spinBoxNumber->setValue(_model->getNumAsteroids()); - // only connect once we're done setting values or unwanted signal's will be sent - connect(ui->spinBoxNumber, QOverload::of(&QSpinBox::valueChanged), this, \ - &AsteroidEditorDialog::asteroidNumberChanged); - - // setup values in ship debris combo boxes - // MFC let you set comboxbox item indexes, Qt doesn't so we'll need a lookup - debrisComboBoxes = ui->fieldProperties->findChildren(QString(), Qt::FindDirectChildrenOnly); - std::sort(debrisComboBoxes.begin(), debrisComboBoxes.end(), sort_qcombobox_by_name); - - QString debris_size[NUM_ASTEROID_SIZES] = { "Small", "Medium", "Large" }; - QStringList debris_names("None"); - for (const auto& i : Species_info) // each species - { - for (const auto& j : debris_size) // each size - { - debris_names += QString(i.species_name) + " " + j; - } - } - - // There are only 3 combo boxes.. FOR NOW - for (auto i = 0; i < 3; ++i) { - debrisComboBoxes.at(i)->addItems(debris_names); - // update debris combobox data on index changes - connect(debrisComboBoxes.at(i), QOverload::of(&QComboBox::currentIndexChanged), this, \ - [this, i](int debris_type) { AsteroidEditorDialog::updateComboBox(i,debris_type); }); - } - - - // radio buttons - connect(ui->radioButtonActiveField, &QRadioButton::toggled, this, &AsteroidEditorDialog::setFieldActive); - connect(ui->radioButtonPassiveField, &QRadioButton::toggled, this, &AsteroidEditorDialog::setFieldPassive); - connect(ui->radioButtonAsteroid, &QRadioButton::toggled, this, &AsteroidEditorDialog::setGenreAsteroid); - connect(ui->radioButtonShip, &QRadioButton::toggled, this, &AsteroidEditorDialog::setGenreDebris); - - // lineEdit signals/slots - connect(ui->lineEdit_obox_minX, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextOMinX); - connect(ui->lineEdit_obox_minY, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextOMinY); - connect(ui->lineEdit_obox_minZ, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextOMinZ); - connect(ui->lineEdit_obox_maxX, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextOMaxX); - connect(ui->lineEdit_obox_maxY, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextOMaxY); - connect(ui->lineEdit_obox_maxZ, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextOMaxZ); - connect(ui->lineEdit_ibox_minX, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextIMinX); - connect(ui->lineEdit_ibox_minY, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextIMinY); - connect(ui->lineEdit_ibox_minZ, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextIMinZ); - connect(ui->lineEdit_ibox_maxX, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextIMaxX); - connect(ui->lineEdit_ibox_maxY, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextIMaxY); - connect(ui->lineEdit_ibox_maxZ, &QLineEdit::textEdited, this, \ - &AsteroidEditorDialog::changedBoxTextIMaxZ); + // set our internal values, update the UI + initializeUi(); + updateUi(); // setup validators for text input _box_validator.setNotation(QDoubleValidator::StandardNotation); _box_validator.setDecimals(1); + ui->lineEdit_obox_minX->setValidator(&_box_validator); ui->lineEdit_obox_minY->setValidator(&_box_validator); ui->lineEdit_obox_minZ->setValidator(&_box_validator); @@ -124,228 +41,295 @@ AsteroidEditorDialog::AsteroidEditorDialog(FredView *parent, EditorViewport* vie ui->lineEdit_ibox_maxZ->setValidator(&_box_validator); ui->lineEditAvgSpeed->setValidator(&_speed_validator); - - updateUI(); } AsteroidEditorDialog::~AsteroidEditorDialog() = default; +void AsteroidEditorDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void AsteroidEditorDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close +} + void AsteroidEditorDialog::closeEvent(QCloseEvent* e) { - if (!rejectOrCloseHandler(this, _model.get(), _viewport)) { - e->ignore(); - }; + reject(); + e->ignore(); // Don't let the base class close the window } -void AsteroidEditorDialog::rejectHandler() +void AsteroidEditorDialog::initializeUi() { - this->close(); + util::SignalBlockers blockers(this); // block signals while we set up the UI + + // Checkboxes + ui->enabled->setChecked(_model->getFieldEnabled()); + ui->innerBoxEnabled->setChecked(_model->getInnerBoxEnabled()); + ui->enhancedFieldEnabled->setChecked(_model->getEnhancedEnabled()); + + // Radio buttons for field type + ui->radioButtonActiveField->setChecked(_model->getFieldType() == FT_ACTIVE); + ui->radioButtonPassiveField->setChecked(_model->getFieldType() == FT_PASSIVE); + + // Radio buttons for debris genre + ui->radioButtonAsteroid->setChecked(_model->getDebrisGenre() == DG_ASTEROID); + ui->radioButtonDebris->setChecked(_model->getDebrisGenre() == DG_DEBRIS); + + // Spin box + ui->spinBoxNumber->setValue(_model->getNumAsteroids()); + + // Average speed + ui->lineEditAvgSpeed->setText(_model->getAvgSpeed()); + + // Outer box + ui->lineEdit_obox_minX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MIN_X)); + ui->lineEdit_obox_minY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MIN_Y)); + ui->lineEdit_obox_minZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MIN_Z)); + ui->lineEdit_obox_maxX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MAX_X)); + ui->lineEdit_obox_maxY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MAX_Y)); + ui->lineEdit_obox_maxZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_O_MAX_Z)); + + // Inner box + ui->lineEdit_ibox_minX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MIN_X)); + ui->lineEdit_ibox_minY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MIN_Y)); + ui->lineEdit_ibox_minZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MIN_Z)); + ui->lineEdit_ibox_maxX->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MAX_X)); + ui->lineEdit_ibox_maxY->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MAX_Y)); + ui->lineEdit_ibox_maxZ->setText(_model->getBoxText(AsteroidEditorDialogModel::_box_line_edits::_I_MAX_Z)); + + // Housekeeping + ui->spinBoxNumber->setRange(1, MAX_ASTEROIDS); } -QString & AsteroidEditorDialog::getBoxText(AsteroidEditorDialogModel::_box_line_edits type) +void AsteroidEditorDialog::updateUi() { - return _model->getBoxText(type); + util::SignalBlockers blockers(this); // block signals while we update the UI + + bool overall_enabled = _model->getFieldEnabled(); + bool asteroids_enabled = overall_enabled && _model->getDebrisGenre() == DG_ASTEROID; + bool debris_enabled = overall_enabled && _model->getDebrisGenre() == DG_DEBRIS; + bool inner_box_enabled = _model->getInnerBoxEnabled(); + bool field_is_active = (_model->getFieldType() == FT_ACTIVE); + + // Checkboxes + ui->innerBoxEnabled->setEnabled(overall_enabled); + ui->enhancedFieldEnabled->setEnabled(overall_enabled); + + // Radio buttons for field type + ui->radioButtonActiveField->setEnabled(overall_enabled); + ui->radioButtonPassiveField->setEnabled(overall_enabled); + + // Radio buttons for debris genre + ui->radioButtonAsteroid->setEnabled(overall_enabled); + ui->radioButtonDebris->setEnabled(overall_enabled && !field_is_active); + + // Spin box + ui->spinBoxNumber->setEnabled(overall_enabled); + + // Average speed + ui->lineEditAvgSpeed->setEnabled(overall_enabled); + + // Outer box + ui->lineEdit_obox_minX->setEnabled(overall_enabled); + ui->lineEdit_obox_minY->setEnabled(overall_enabled); + ui->lineEdit_obox_minZ->setEnabled(overall_enabled); + ui->lineEdit_obox_maxX->setEnabled(overall_enabled); + ui->lineEdit_obox_maxY->setEnabled(overall_enabled); + ui->lineEdit_obox_maxZ->setEnabled(overall_enabled); + + // Inner box + ui->lineEdit_ibox_minX->setEnabled(overall_enabled && inner_box_enabled); + ui->lineEdit_ibox_minY->setEnabled(overall_enabled && inner_box_enabled); + ui->lineEdit_ibox_minZ->setEnabled(overall_enabled && inner_box_enabled); + ui->lineEdit_ibox_maxX->setEnabled(overall_enabled && inner_box_enabled); + ui->lineEdit_ibox_maxY->setEnabled(overall_enabled && inner_box_enabled); + ui->lineEdit_ibox_maxZ->setEnabled(overall_enabled && inner_box_enabled); + + // Push buttons for object types + ui->asteroidSelectButton->setEnabled(overall_enabled && asteroids_enabled); + ui->debrisSelectButton->setEnabled(overall_enabled && debris_enabled && !field_is_active); + + // Push buttons for ship targets + ui->shipSelectButton->setEnabled(overall_enabled && field_is_active); + + // Update the radio buttons as these do depend on the field type + ui->radioButtonAsteroid->setChecked(_model->getDebrisGenre() == DG_ASTEROID); + ui->radioButtonDebris->setChecked(_model->getDebrisGenre() == DG_DEBRIS); } -void AsteroidEditorDialog::changedBoxTextIMinX(const QString &text) +void AsteroidEditorDialog::on_okAndCancelButtons_accepted() { - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_X); + accept(); } -void AsteroidEditorDialog::changedBoxTextIMinY(const QString &text) +void AsteroidEditorDialog::on_okAndCancelButtons_rejected() { - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_Y); + reject(); } -void AsteroidEditorDialog::changedBoxTextIMinZ(const QString &text) +void AsteroidEditorDialog::on_enabled_toggled(bool enabled) { - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_Z); + _model->setFieldEnabled(enabled); + updateUi(); } -void AsteroidEditorDialog::changedBoxTextIMaxX(const QString &text) +void AsteroidEditorDialog::on_innerBoxEnabled_toggled(bool enabled) { - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_X); + _model->setInnerBoxEnabled(enabled); + updateUi(); } -void AsteroidEditorDialog::changedBoxTextIMaxY(const QString &text) +void AsteroidEditorDialog::on_enhancedFieldEnabled_toggled(bool enabled) { - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_Y); + _model->setEnhancedEnabled(enabled); } -void AsteroidEditorDialog::changedBoxTextIMaxZ(const QString &text) +void AsteroidEditorDialog::on_radioButtonActiveField_toggled(bool checked) { - _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_Z); + if (checked) { + _model->setFieldType(FT_ACTIVE); + _model->setDebrisGenre(DG_ASTEROID); // only allow asteroids in active fields + updateUi(); + } } -void AsteroidEditorDialog::changedBoxTextOMinX(const QString &text) +void AsteroidEditorDialog::on_radioButtonPassiveField_toggled(bool checked) { - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_X); + if (checked) { + _model->setFieldType(FT_PASSIVE); + updateUi(); + } } -void AsteroidEditorDialog::changedBoxTextOMinY(const QString &text) +void AsteroidEditorDialog::on_radioButtonAsteroid_toggled(bool checked) { - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_Y); + if (checked) { + _model->setDebrisGenre(DG_ASTEROID); + updateUi(); + } } -void AsteroidEditorDialog::changedBoxTextOMinZ(const QString &text) +void AsteroidEditorDialog::on_radioButtonDebris_toggled(bool checked) { - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_Z); + if (checked) { + _model->setDebrisGenre(DG_DEBRIS); + updateUi(); + } } -void AsteroidEditorDialog::changedBoxTextOMaxX(const QString &text) +void AsteroidEditorDialog::on_spinBoxNumber_valueChanged(int num_asteroids) { - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_X); + _model->setNumAsteroids(num_asteroids); } -void AsteroidEditorDialog::changedBoxTextOMaxY(const QString &text) +void AsteroidEditorDialog::on_lineEditAvgSpeed_textEdited(const QString& text) { - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_Y); + _model->setAvgSpeed(text); } -void AsteroidEditorDialog::changedBoxTextOMaxZ(const QString &text) +void AsteroidEditorDialog::on_lineEdit_obox_minX_textEdited(const QString& text) { - _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_Z); + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_X); } -void AsteroidEditorDialog::setFieldActive() +void AsteroidEditorDialog::on_lineEdit_obox_minY_textEdited(const QString& text) { - _model->setFieldType(FT_ACTIVE); - setGenreAsteroid(); // only allow asteroids in active fields - updateUI(); + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_Y); } -void AsteroidEditorDialog::setFieldPassive() +void AsteroidEditorDialog::on_lineEdit_obox_minZ_textEdited(const QString& text) { - _model->setFieldType(FT_PASSIVE); - updateUI(); + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MIN_Z); } -void AsteroidEditorDialog::setGenreAsteroid() +void AsteroidEditorDialog::on_lineEdit_obox_maxX_textEdited(const QString& text) { - _model->setDebrisGenre(DG_ASTEROID); - updateUI(); + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_X); } -void AsteroidEditorDialog::setGenreDebris() +void AsteroidEditorDialog::on_lineEdit_obox_maxY_textEdited(const QString& text) { - _model->setDebrisGenre(DG_DEBRIS); - updateUI(); + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_Y); } -void AsteroidEditorDialog::toggleEnabled(bool enabled) +void AsteroidEditorDialog::on_lineEdit_obox_maxZ_textEdited(const QString& text) { - _model->setEnabled(enabled); - updateUI(); + _model->setBoxText(text, AsteroidEditorDialogModel::_O_MAX_Z); } -void AsteroidEditorDialog::toggleInnerBoxEnabled(bool enabled) +void AsteroidEditorDialog::on_lineEdit_ibox_minX_textEdited(const QString& text) { - _model->setInnerBoxEnabled(enabled); - updateUI(); + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_X); } -void AsteroidEditorDialog::toggleEnhancedEnabled(bool enabled) +void AsteroidEditorDialog::on_lineEdit_ibox_minY_textEdited(const QString& text) { - _model->setEnhancedEnabled(enabled); - updateUI(); + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_Y); } -void AsteroidEditorDialog::toggleAsteroid(AsteroidEditorDialogModel::_roid_types colour, bool enabled) +void AsteroidEditorDialog::on_lineEdit_ibox_minZ_textEdited(const QString& text) { - _model->setAsteroidEnabled(colour, enabled); - updateUI(); + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MIN_Z); } -void AsteroidEditorDialog::asteroidNumberChanged(int num_asteroids) +void AsteroidEditorDialog::on_lineEdit_ibox_maxX_textEdited(const QString& text) { - _model->setNumAsteroids(num_asteroids); + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_X); } -void AsteroidEditorDialog::updateComboBox(int idx, int debris_type) +void AsteroidEditorDialog::on_lineEdit_ibox_maxY_textEdited(const QString& text) { - _model->setFieldDebrisType(idx, debris_type); + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_Y); } -void AsteroidEditorDialog::updateUI() +void AsteroidEditorDialog::on_lineEdit_ibox_maxZ_textEdited(const QString& text) { - util::SignalBlockers blockers(this); + _model->setBoxText(text, AsteroidEditorDialogModel::_I_MAX_Z); +} - // various useful states - bool asteroids_enabled = _model->getEnabled(); - bool inner_box_enabled = _model->getInnerBoxEnabled(); - bool field_is_active = (_model->getFieldType() == FT_ACTIVE); - bool debris_is_asteroid = (_model->getDebrisGenre() == DG_ASTEROID); - bool enhanced_is_active = _model->getEnhancedEnabled(); - - // checkboxes - ui->enabled->setChecked(asteroids_enabled); - ui->innerBoxEnabled->setChecked(inner_box_enabled); - ui->checkBoxBrown->setChecked(_model->getAsteroidEnabled(AsteroidEditorDialogModel::_AST_BROWN)); - ui->checkBoxBlue->setChecked(_model->getAsteroidEnabled(AsteroidEditorDialogModel::_AST_BLUE)); - ui->checkBoxOrange->setChecked(_model->getAsteroidEnabled(AsteroidEditorDialogModel::_AST_ORANGE)); - ui->enhancedFieldEnabled->setChecked(enhanced_is_active); - - // radio buttons (2x groups) - ui->radioButtonActiveField->setChecked(field_is_active); - ui->radioButtonPassiveField->setChecked(!field_is_active); - ui->radioButtonAsteroid->setChecked(debris_is_asteroid); - ui->radioButtonShip->setChecked(!debris_is_asteroid); - if (field_is_active) { - ui->radioButtonShip->setToolTip(QString("Ship Debris is only allowed in passive fields")); +void AsteroidEditorDialog::on_asteroidSelectButton_clicked() +{ + CheckBoxListDialog dlg(this); + dlg.setCaption("Select Asteroid Types"); + dlg.setOptions(_model->getAsteroidSelections()); + + if (dlg.exec() == QDialog::Accepted) { + _model->setAsteroidSelections(dlg.getCheckedStates()); } - else { - ui->radioButtonShip->setToolTip(QString("")); +} + +void AsteroidEditorDialog::on_debrisSelectButton_clicked() +{ + CheckBoxListDialog dlg(this); + dlg.setCaption("Select Debris Types"); + dlg.setOptions(_model->getDebrisSelections()); + + if (dlg.exec() == QDialog::Accepted) { + _model->setDebrisSelections(dlg.getCheckedStates()); } +} - // enable/disable sections of interface - ui->fieldProperties->setEnabled(asteroids_enabled); - ui->outerBox->setEnabled(asteroids_enabled); - ui->innerBox->setEnabled(asteroids_enabled); - - ui->innerBoxEnabled->setEnabled(asteroids_enabled && field_is_active); - ui->lineEdit_ibox_maxX->setEnabled(inner_box_enabled && field_is_active); - ui->lineEdit_ibox_maxY->setEnabled(inner_box_enabled && field_is_active); - ui->lineEdit_ibox_maxZ->setEnabled(inner_box_enabled && field_is_active); - ui->lineEdit_ibox_minX->setEnabled(inner_box_enabled && field_is_active); - ui->lineEdit_ibox_minY->setEnabled(inner_box_enabled && field_is_active); - ui->lineEdit_ibox_minZ->setEnabled(inner_box_enabled && field_is_active); - ui->label_ibox_maxX->setEnabled(inner_box_enabled && field_is_active); - ui->label_ibox_maxY->setEnabled(inner_box_enabled && field_is_active); - ui->label_ibox_maxZ->setEnabled(inner_box_enabled && field_is_active); - ui->label_ibox_minX->setEnabled(inner_box_enabled && field_is_active); - ui->label_ibox_minY->setEnabled(inner_box_enabled && field_is_active); - ui->label_ibox_minZ->setEnabled(inner_box_enabled && field_is_active); - - ui->radioButtonShip->setEnabled(!field_is_active); - - ui->checkBoxBrown->setEnabled(debris_is_asteroid); - ui->checkBoxBlue->setEnabled(debris_is_asteroid); - ui->checkBoxOrange->setEnabled(debris_is_asteroid); - - // speed text - ui->lineEditAvgSpeed->setText(_model->AsteroidEditorDialogModel::getAvgSpeed()); - - // ship debris comboboxes - for (auto i = 0; i < debrisComboBoxes.size(); ++i) { - debrisComboBoxes.at(i)->setCurrentIndex(_model->AsteroidEditorDialogModel::getFieldDebrisType(i)); - debrisComboBoxes.at(i)->setEnabled(!debris_is_asteroid); +void AsteroidEditorDialog::on_shipSelectButton_clicked() +{ + CheckBoxListDialog dlg(this); + dlg.setCaption("Select Ship Debris Types"); + dlg.setOptions(_model->getShipSelections()); + if (dlg.exec() == QDialog::Accepted) { + _model->setShipSelections(dlg.getCheckedStates()); } +} - // mix/max field bounding boxes text - ui->lineEdit_obox_minX->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MIN_X)); - ui->lineEdit_obox_minY->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MIN_Y)); - ui->lineEdit_obox_minZ->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MIN_Z)); - ui->lineEdit_obox_maxX->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MAX_X)); - ui->lineEdit_obox_maxY->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MAX_Y)); - ui->lineEdit_obox_maxZ->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_O_MAX_Z)); - ui->lineEdit_ibox_minX->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MIN_X)); - ui->lineEdit_ibox_minY->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MIN_Y)); - ui->lineEdit_ibox_minZ->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MIN_Z)); - ui->lineEdit_ibox_maxX->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MAX_X)); - ui->lineEdit_ibox_maxY->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MAX_Y)); - ui->lineEdit_ibox_maxZ->setText(_model->AsteroidEditorDialogModel::getBoxText(AsteroidEditorDialogModel::_I_MAX_Z)); -} - -} // namespace dialogs -} // namespace fred -} // namespace fso +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/AsteroidEditorDialog.h b/qtfred/src/ui/dialogs/AsteroidEditorDialog.h index 3ba6294c5ce..c0c9fced00c 100644 --- a/qtfred/src/ui/dialogs/AsteroidEditorDialog.h +++ b/qtfred/src/ui/dialogs/AsteroidEditorDialog.h @@ -5,9 +5,7 @@ #include #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class AsteroidEditorDialog; @@ -18,56 +16,70 @@ class AsteroidEditorDialog : public QDialog Q_OBJECT public: AsteroidEditorDialog(FredView* parent, EditorViewport* viewport); - ~AsteroidEditorDialog() override; - - protected: - void closeEvent(QCloseEvent* e) override; - void rejectHandler(); - -private: - - void toggleEnabled(bool enabled); - void toggleInnerBoxEnabled(bool enabled); - void toggleEnhancedEnabled(bool enabled); - void toggleAsteroid(AsteroidEditorDialogModel::_roid_types colour, bool enabled); - - void asteroidNumberChanged(int num_asteroids); - - void setFieldActive(); - void setFieldPassive(); - void setGenreAsteroid(); - void setGenreDebris(); - - void changedBoxTextIMinX(const QString &text); - void changedBoxTextIMinY(const QString &text); - void changedBoxTextIMinZ(const QString &text); - void changedBoxTextIMaxX(const QString &text); - void changedBoxTextIMaxY(const QString &text); - void changedBoxTextIMaxZ(const QString &text); - void changedBoxTextOMinX(const QString &text); - void changedBoxTextOMinY(const QString &text); - void changedBoxTextOMinZ(const QString &text); - void changedBoxTextOMaxX(const QString &text); - void changedBoxTextOMaxY(const QString &text); - void changedBoxTextOMaxZ(const QString &text); - QString & getBoxText(AsteroidEditorDialogModel::_box_line_edits type); - - void updateComboBox(int idx, int debris_type); - void updateUI(); - + ~AsteroidEditorDialog() override; + + void accept() override; + void reject() override; + +protected: + void closeEvent(QCloseEvent* e) override; // funnel all Window X presses through reject() + +// Utilize Qt's "slots" feature to automatically connect UI elements to functions with less code in the initializer +// As a benefit this also requires zero manual signal setup in the .ui file (which is less obvious to those unfamiliar with Qt) +// The naming convention here is on__(). Easy to read and understand. +private slots: + // dialog controls + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + // toggles + void on_enabled_toggled(bool enabled); + void on_innerBoxEnabled_toggled(bool enabled); + void on_enhancedFieldEnabled_toggled(bool enabled); + + // field types + void on_radioButtonActiveField_toggled(bool checked); + void on_radioButtonPassiveField_toggled(bool checked); + void on_radioButtonAsteroid_toggled(bool checked); + void on_radioButtonDebris_toggled(bool checked); + + // basic values + void on_spinBoxNumber_valueChanged(int num_asteroids); + void on_lineEditAvgSpeed_textEdited(const QString& text); + + // box values + void on_lineEdit_obox_minX_textEdited(const QString& text); + void on_lineEdit_obox_minY_textEdited(const QString& text); + void on_lineEdit_obox_minZ_textEdited(const QString& text); + void on_lineEdit_obox_maxX_textEdited(const QString& text); + void on_lineEdit_obox_maxY_textEdited(const QString& text); + void on_lineEdit_obox_maxZ_textEdited(const QString& text); + void on_lineEdit_ibox_minX_textEdited(const QString& text); + void on_lineEdit_ibox_minY_textEdited(const QString& text); + void on_lineEdit_ibox_minZ_textEdited(const QString& text); + void on_lineEdit_ibox_maxX_textEdited(const QString& text); + void on_lineEdit_ibox_maxY_textEdited(const QString& text); + void on_lineEdit_ibox_maxZ_textEdited(const QString& text); + + // object selections + void on_asteroidSelectButton_clicked(); + void on_debrisSelectButton_clicked(); + void on_shipSelectButton_clicked(); + +private: // NOLINT(readability-redundant-access-specifiers) + void initializeUi(); + void updateUi(); + + // Boilerplate EditorViewport* _viewport = nullptr; Editor* _editor = nullptr; - std::unique_ptr ui; std::unique_ptr _model; + // Validators QDoubleValidator _box_validator; QIntValidator _speed_validator; - - QList debrisComboBoxes; }; -} // namespace dialogs -} // namespace fred -} // namespace fso +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/AsteroidEditorDialog.ui b/qtfred/ui/AsteroidEditorDialog.ui index 25d11b03b28..69727785a13 100644 --- a/qtfred/ui/AsteroidEditorDialog.ui +++ b/qtfred/ui/AsteroidEditorDialog.ui @@ -6,8 +6,8 @@ 0 0 - 675 - 423 + 1033 + 377 @@ -35,6 +35,12 @@ true + + + 1 + 0 + + Field Properties @@ -42,96 +48,123 @@ Field Properties - - - - Asteroid - - - radioAsteroidDebris - - - - - - - - - - - - - Passive Field - - - radioActivePassive - - - - - - - Brown - - - - - - - Debris + + + + + 0 + 0 + - - radioAsteroidDebris - - - - - - - Avg Speed: + + + 0 + 0 + - - - - - - Number: + + Field Type + + + + + Active Field + + + radioActivePassive + + + + + + + Passive Field + + + radioActivePassive + + + + - - + + + + + + + + + Number: + + + + + + + Avg Speed: + + + + + + + - - - - Blue + + + + Object Type + + + + + Asteroid + + + radioAsteroidDebris + + + + + + + Debris + + + radioAsteroidDebris + + + + - - - - Orange + + + + Object Selection + + + + + Select Asteroid Types + + + + + + + Select Debris Types + + + + - - - - - - - Active Field - - - radioActivePassive - - - - - - @@ -141,10 +174,17 @@ Outer Box - - + + - Min X: + Min Z: + + + + + + + Max Y: @@ -155,8 +195,12 @@ - - + + + + Max Z: + + @@ -165,71 +209,62 @@ - - - - - + + - Max Y: + Min X: - - - - Max Z: - - + + - - - - Min Z: - - + + + + + - - - - - + + + + + 0 + 0 + + Inner Box + + + + + + - - - - Min Y: - - - - - + + - Min X: + Enabled - - - @@ -237,15 +272,6 @@ - - - - - - - - - @@ -253,8 +279,18 @@ - - + + + + Min Y: + + + + + + + + @@ -270,82 +306,115 @@ - - + + - Enabled + Min X: + + + + + + + + + + + 1 + 0 + + + + Targets + + + + + + (If no targets are specified, all rocks/chunks will target the first capital ship in the mission, even if that ship is not on th escort list.) + + + true + + + + + + + Select Target Ships + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + - - - - - - - - 20 - 10 - 653 - 20 - - - - - 0 - 0 - - - - Use ehnanced spawn checking - - - - - - 40 - 30 - 450 - 40 - - - - - 0 - 0 - - - - - 0 - 40 - - - - - 500 - 200 - - - - (By default player range is checked in addition to the player view cone for spawning asteroids. In small fields you may want to override this behavior.) - - - true - - - + + + + + + 0 + 0 + + + + Use ehnanced spawn checking + + + + + + + + 0 + 0 + + + + + 0 + 40 + + + + + 16777215 + 16777215 + + + + (By default player range is checked in addition to the player view cone for spawning asteroids. In small fields you may want to override this behavior.) + + + true + + + + - + QDialogButtonBox::Cancel|QDialogButtonBox::Ok @@ -361,26 +430,9 @@ - - - dialogButtonBox - accepted() - fso::fred::dialogs::AsteroidEditorDialog - accept() - - - 337 - 317 - - - 337 - 169 - - - - + - + From 5eb80f55bca3995b77f0f00af8b0d73a2084cd28 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Sat, 23 Aug 2025 12:31:39 -0400 Subject: [PATCH 407/466] Fix conversion of old muzzle flash to new type (#6972) Fixes the updated name of `Pixel Size At Emitter` --- code/weapon/muzzleflash.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/weapon/muzzleflash.cpp b/code/weapon/muzzleflash.cpp index a069701ad7b..3d9e8534ed1 100644 --- a/code/weapon/muzzleflash.cpp +++ b/code/weapon/muzzleflash.cpp @@ -194,7 +194,7 @@ static void convert_mflash_to_particle() { bm_load_animation(blob.name)); if (Min_pizel_size_muzzleflash > 0) { - subparticles.back().m_modular_curves.add_curve("Apparent Visual Size At Emitter", particle::ParticleEffect::ParticleCurvesOutput::RADIUS_MULT, scaling_curve); + subparticles.back().m_modular_curves.add_curve("Pixel Size At Emitter", particle::ParticleEffect::ParticleCurvesOutput::RADIUS_MULT, scaling_curve); } } From 9502af6f20c8375038d82e03a0191afc6621155b Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Sat, 23 Aug 2025 17:40:21 -0400 Subject: [PATCH 408/466] Fix gr.createParticle with parent velocity (#6970) --- code/scripting/api/libs/graphics.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/code/scripting/api/libs/graphics.cpp b/code/scripting/api/libs/graphics.cpp index bcb1af8dbb7..ec82897307e 100644 --- a/code/scripting/api/libs/graphics.cpp +++ b/code/scripting/api/libs/graphics.cpp @@ -2309,7 +2309,6 @@ static int spawnParticles(lua_State *L, bool persistent) { std::unique_ptr host; if (objh != nullptr && objh->isValid()) { host = std::make_unique(objh->objp(), pos); - vel += objh->objp()->phys_info.vel; } else { host = std::make_unique(pos, vmd_identity_matrix, vmd_zero_vector); From 9755d44c5fd87390e8de4ea4774ac714364bd2b7 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Sat, 23 Aug 2025 20:32:18 -0400 Subject: [PATCH 409/466] update mdns to v1.4.3 (#3973) --- code/network/multi_mdns.cpp | 168 ++++- lib/mdns/CHANGELOG | 20 + lib/mdns/CMakeLists.txt | 7 +- lib/mdns/README.md | 25 +- lib/mdns/mdns.c | 740 ++++++++++++++++--- lib/mdns/mdns.h | 1378 ++++++++++++++++++++++------------- 6 files changed, 1702 insertions(+), 636 deletions(-) create mode 100644 lib/mdns/CHANGELOG diff --git a/code/network/multi_mdns.cpp b/code/network/multi_mdns.cpp index 63209547882..1ff1d258a03 100644 --- a/code/network/multi_mdns.cpp +++ b/code/network/multi_mdns.cpp @@ -22,9 +22,9 @@ static SCP_string HOST_NAME = "fs2open"; static SCP_vector mSockets; -static in_addr IPv4_addr; +static SOCKADDR_IN IPv4_addr; static bool has_ipv4 = false; -static uint8_t IPv6_addr[sizeof(in6_addr)]; +static SOCKADDR_IN6 IPv6_addr; static bool has_ipv6 = false; @@ -60,38 +60,156 @@ static int query_callback(int sock __UNUSED, const struct sockaddr *from __UNUSE static int service_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry_type_t entry, uint16_t query_id, uint16_t rtype, uint16_t rclass, uint32_t ttl __UNUSED, const void* data, - size_t size, size_t name_offset __UNUSED, size_t name_length __UNUSED, size_t record_offset, - size_t record_length, void* user_data __UNUSED) + size_t size, size_t name_offset, size_t name_length __UNUSED, size_t record_offset __UNUSED, + size_t record_length __UNUSED, void* user_data __UNUSED) { if (entry != MDNS_ENTRYTYPE_QUESTION) { return 0; } - std::array BUFFER{}; + const SCP_string dns_sd = "_services._dns-sd._udp.local."; + const SCP_string SERVICE_INSTANCE = HOST_NAME + "." + SERVICE_NAME; + const SCP_string HOSTNAME_QUALIFIED = HOST_NAME + ".local."; - if (rtype == MDNS_RECORDTYPE_PTR) { - mdns_string_t service = mdns_record_parse_ptr(data, size, record_offset, record_length, BUFFER.data(), BUFFER.size()); + std::array NAME{}; + mdns_record_t answer; + SCP_vector records; + mdns_record_t n_record; - // check for special discovery record and reply accordingly - const SCP_string dns_sd = "_services._dns-sd._udp.local."; + answer.type = MDNS_RECORDTYPE_IGNORE; - if ( (service.length == dns_sd.size()) && (dns_sd == service.str) ) { - mdns_discovery_answer(sock, from, addrlen, BUFFER.data(), BUFFER.size(), SERVICE_NAME.c_str(), SERVICE_NAME.size()); - return 0; + size_t offset = name_offset; + const mdns_string_t name = mdns_string_extract(data, size, &offset, NAME.data(), NAME.size()); + + // check for special discovery record and reply accordingly + if ( (dns_sd.length() == name.length) && (dns_sd == name.str) ) { + if ( (rtype == MDNS_RECORDTYPE_PTR) || (rtype == MDNS_RECORDTYPE_ANY) ) { + answer.name = name; + answer.type = MDNS_RECORDTYPE_PTR; + answer.data.ptr.name = { SERVICE_NAME.c_str(), SERVICE_NAME.length() }; + } + } + // look for our service + else if ( (SERVICE_NAME.length() == name.length) && (SERVICE_NAME == name.str) ) { + if ( (rtype == MDNS_RECORDTYPE_PTR) || (rtype == MDNS_RECORDTYPE_ANY) ) { + // base answer is PTR reverse mapping (service) + answer.name = { SERVICE_NAME.c_str(), SERVICE_NAME.length() }; + answer.type = MDNS_RECORDTYPE_PTR; + answer.data.ptr.name = { SERVICE_INSTANCE.c_str(), SERVICE_INSTANCE.length() }; + + // additional records... + records.reserve(3); + + // SRV record mapping (service instance) + n_record.name = { SERVICE_INSTANCE.c_str(), SERVICE_INSTANCE.length() }; + n_record.type = MDNS_RECORDTYPE_SRV; + n_record.data.srv.name = { HOSTNAME_QUALIFIED.c_str(), HOSTNAME_QUALIFIED.length() }; + n_record.data.srv.port = Psnet_default_port; + n_record.data.srv.priority = 0; + n_record.data.srv.weight = 0; + + records.push_back(n_record); + + // add A record + if (has_ipv4) { + n_record.name = { HOSTNAME_QUALIFIED.c_str(), HOSTNAME_QUALIFIED.length() }; + n_record.type = MDNS_RECORDTYPE_A; + n_record.data.a.addr = IPv4_addr; + + records.push_back(n_record); + } + + // add AAAA record + if (has_ipv6) { + n_record.name = { HOSTNAME_QUALIFIED.c_str(), HOSTNAME_QUALIFIED.length() }; + n_record.type = MDNS_RECORDTYPE_AAAA; + n_record.data.aaaa.addr = IPv6_addr; + + records.push_back(n_record); + } + } + } + // look for direct service instance + else if ( (SERVICE_INSTANCE.length() == name.length) && (SERVICE_INSTANCE == name.str) ) { + if ( (rtype == MDNS_RECORDTYPE_SRV) || (rtype == MDNS_RECORDTYPE_ANY) ) { + // base answer is SRV record mapping (service instance) + answer.name = { SERVICE_INSTANCE.c_str(), SERVICE_INSTANCE.length() }; + answer.type = MDNS_RECORDTYPE_SRV; + answer.data.srv.name = { HOSTNAME_QUALIFIED.c_str(), HOSTNAME_QUALIFIED.length() }; + answer.data.srv.port = Psnet_default_port; + answer.data.srv.priority = 0; + answer.data.srv.weight = 0; + + // additional records ... + records.reserve(2); + + // add A record + if (has_ipv4) { + n_record.name = { HOSTNAME_QUALIFIED.c_str(), HOSTNAME_QUALIFIED.length() }; + n_record.type = MDNS_RECORDTYPE_A; + n_record.data.a.addr = IPv4_addr; + + records.push_back(n_record); + } + + // add AAAA record + if (has_ipv6) { + n_record.name = { HOSTNAME_QUALIFIED.c_str(), HOSTNAME_QUALIFIED.length() }; + n_record.type = MDNS_RECORDTYPE_AAAA; + n_record.data.aaaa.addr = IPv6_addr; + + records.push_back(n_record); + } } + } + // hostname A/AAAA records + else if ( (HOSTNAME_QUALIFIED.length() == name.length) && (HOSTNAME_QUALIFIED == name.str) ) { + if ( has_ipv4 && ((rtype == MDNS_RECORDTYPE_A) || (rtype == MDNS_RECORDTYPE_ANY)) ) { + // base answer is A record + answer.name = { HOSTNAME_QUALIFIED.c_str(), HOSTNAME_QUALIFIED.length() }; + answer.type = MDNS_RECORDTYPE_A; + answer.data.a.addr = IPv4_addr; + + // additional records ... + + // add AAAA record + if (has_ipv6) { + n_record.name = { HOSTNAME_QUALIFIED.c_str(), HOSTNAME_QUALIFIED.length() }; + n_record.type = MDNS_RECORDTYPE_AAAA; + n_record.data.aaaa.addr = IPv6_addr; + + records.push_back(n_record); + } + } else if ( has_ipv6 && ((rtype == MDNS_RECORDTYPE_AAAA) || (rtype == MDNS_RECORDTYPE_ANY)) ) { + // base answer is AAAA record + answer.name = { HOSTNAME_QUALIFIED.c_str(), HOSTNAME_QUALIFIED.length() }; + answer.type = MDNS_RECORDTYPE_AAAA; + answer.data.aaaa.addr = IPv6_addr; - // ignore anything not meant for us - if ( (service.length != SERVICE_NAME.size()) || !(SERVICE_NAME == service.str) ) { - return 0; + // additional records ... + + // add A record + if (has_ipv4) { + n_record.name = { HOSTNAME_QUALIFIED.c_str(), HOSTNAME_QUALIFIED.length() }; + n_record.type = MDNS_RECORDTYPE_A; + n_record.data.a.addr = IPv4_addr; + + records.push_back(n_record); + } } + } - uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + if (answer.type != MDNS_RECORDTYPE_IGNORE) { + const bool unicast = (rclass & MDNS_UNICAST_RESPONSE) == MDNS_UNICAST_RESPONSE; + mdns_record_t *records_ptr = records.empty() ? nullptr : &records.front(); + std::array buffer{}; - // this one call will send all required records - mdns_query_answer(sock, from, (unicast) ? addrlen : 0, BUFFER.data(), BUFFER.size(), query_id, - SERVICE_NAME.c_str(), SERVICE_NAME.size(), HOST_NAME.c_str(), HOST_NAME.size(), - has_ipv4 ? IPv4_addr.s_addr : 0, has_ipv6 ? IPv6_addr : nullptr, - Psnet_default_port, nullptr, 0); + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, buffer.data(), buffer.size(), query_id, static_cast(rtype), + name.str, name.length, answer, nullptr, 0, records_ptr, records.size()); + } else { + mdns_query_answer_multicast(sock, buffer.data(), buffer.size(), answer, nullptr, 0, records_ptr, records.size()); + } } return 0; @@ -235,7 +353,7 @@ bool multi_mdns_service_init() } // setup local ip info - IPv4_addr.s_addr = INADDR_ANY; + memset(&IPv4_addr, 0, sizeof(IPv4_addr)); memset(&IPv6_addr, 0, sizeof(IPv6_addr)); has_ipv4 = false; @@ -246,16 +364,16 @@ bool multi_mdns_service_init() if (ip_mode & PSNET_IP_MODE_V4) { auto *in6 = psnet_get_local_ip(AF_INET); - if (in6 && psnet_map6to4(in6, &IPv4_addr)) { + if (in6 && psnet_map6to4(in6, &IPv4_addr.sin_addr)) { has_ipv4 = true; } } if (ip_mode & PSNET_IP_MODE_V6) { - auto in6 = psnet_get_local_ip(AF_INET6); + auto *in6 = psnet_get_local_ip(AF_INET6); if (in6) { - memcpy(&IPv6_addr, in6, sizeof(in6_addr)); + memcpy(&IPv6_addr.sin6_addr, in6, sizeof(in6_addr)); has_ipv6 = true; } } diff --git a/lib/mdns/CHANGELOG b/lib/mdns/CHANGELOG new file mode 100644 index 00000000000..c6dc15f14a4 --- /dev/null +++ b/lib/mdns/CHANGELOG @@ -0,0 +1,20 @@ +1.4.1 + +Use const pointers in socket open and setup functions. + +Avoid null pointer arithmetics for standard compliance. + + +1.4 + +Returning non-zero from callback function during record parsing immedediately stops parsing and returns the number of records parsed so far. + +The function to send a query answer has been split in two, one for unicast answer and one for multicast. + +The functions to send query answers have been generalized to send any number of records. + +Added new function to do multicast announce on start/wake-up (unsolicited answer). + +Added parsing of ANY question records and DNS-SD queries with multiple questions + +Removed mdns_discovery_answer in favour of the new generalized answer functions, to handle both unicast and multicast response diff --git a/lib/mdns/CMakeLists.txt b/lib/mdns/CMakeLists.txt index 8d1ec7e2e1d..5fb62b244cb 100644 --- a/lib/mdns/CMakeLists.txt +++ b/lib/mdns/CMakeLists.txt @@ -1,3 +1,8 @@ -add_library(mdns INTERFACE) +add_library(mdns + INTERFACE + mdns.h +) target_include_directories(mdns SYSTEM INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}") + +set_target_properties(mdns PROPERTIES FOLDER "3rdparty") \ No newline at end of file diff --git a/lib/mdns/README.md b/lib/mdns/README.md index 38532910437..fdb9ea75faa 100644 --- a/lib/mdns/README.md +++ b/lib/mdns/README.md @@ -8,13 +8,15 @@ This library is put in the public domain; you can redistribute it and/or modify Created by Mattias Jansson ([@maniccoder](https://twitter.com/maniccoder)) +Discord server for discussions https://discord.gg/M8BwTQrt6c + ## Features The library does DNS-SD discovery and service as well as one-shot single record mDNS query and response. There are no memory allocations done by the library, all buffers used must be passed in by the caller. Custom data for use in processing can be passed along using a user data opaque pointer. ## Usage -The `mdns.c` test executable file demonstrates the use of all features, including discovery, query and service response. +The `mdns.c` test executable file demonstrates the use of all features, including discovery, query and service response. The documentation here is intentionally sparse, the example code is well documented and should provide all the details. ### Sockets @@ -24,9 +26,9 @@ Call `mdns_socket_close` to close a socket opened with `mdns_socket_open_ipv4` o #### Port -To open/setup the socket for one-shot queries you can pass a null pointer socket address, or set the port in the passed socket address to 0. This will bind the socket to a random ephemeral local UDP port as required by the RFCs for one-shot queries. +To open/setup the socket for one-shot queries you can pass a null pointer socket address, or set the port in the passed socket address to 0. This will bind the socket to a random ephemeral local UDP port as required by the RFCs for one-shot queries. You should NOT bind to port 5353 when doing one-shot queries (see the RFC for details)., -To open/setup the socket for service, responding to incoming queries, you need pass in a socket address structure with the port set to 5353 (defined by MDNS_PORT in the header). +To open/setup the socket for service, responding to incoming queries, you need pass in a socket address structure with the port set to 5353 (defined by MDNS_PORT in the header). You cannot pick any other port or you will not recieve any incoming queries. #### Network interface @@ -42,22 +44,30 @@ To read discovery responses use `mdns_discovery_recv`. All records received sinc ### Query -To send a one-shot mDNS query for a single record use `mdns_query_send`. This will send a single multicast packet for the given record (single PTR question record, for example `_http._tcp.local.`). You can optionally pass in a query ID for the query for later filtering of responses (even though this is discouraged by the RFC), or pass 0 to be fully compliant. The function returns the query ID associated with this query, which if non-zero can be used to filter responses in `mdns_query_recv`. If the socket is bound to port 5353 a multicast response is requested, otherwise a unicast response. +To send a one-shot mDNS query for a single record use `mdns_query_send`. This will send a single multicast packet for the given record and name (for example PTR record for `_http._tcp.local.`). You can optionally pass in a query ID for the query for later filtering of responses (even though this is discouraged by the RFC), or pass 0 to be fully compliant. The function returns the query ID associated with this query, which if non-zero can be used to filter responses in `mdns_query_recv`. If the socket is bound to port 5353 a multicast response is requested, otherwise a unicast response. To read query responses use `mdns_query_recv`. All records received since last call will be piped to the callback supplied in the function call. If `query_id` parameter is non-zero the function will filter out any response with a query ID that does not match the given query ID. The entry type will be one of `MDNS_ENTRYTYPE_ANSWER`, `MDNS_ENTRYTYPE_AUTHORITY` and `MDNS_ENTRYTYPE_ADDITIONAL`. +Note that a socket opened for one-shot queries from an emphemeral port will not recieve any unsolicited answers (announces) as these are sent as a multicast on port 5353. + +To send multiple queries in the same packet use `mdns_multiquery_send` which takes an array and count of service names and record types to query for. + ### Service To listen for incoming DNS-SD requests and mDNS queries the socket can be opened/setup on the default interface by passing 0 as socket address in the call to the socket open/setup functions (the socket will receive data from all network interfaces). Then call `mdns_socket_listen` either on notification of incoming data, or by setting blocking mode and calling `mdns_socket_listen` to block until data is available and parsed. -The entry type passed to the callback will be `MDNS_ENTRYTYPE_QUESTION` and record type `MDNS_RECORDTYPE_PTR`. Use the `mdns_record_parse_ptr` function to get the name string of the service record that was asked for. +The entry type passed to the callback will be `MDNS_ENTRYTYPE_QUESTION` and record type indicates which record to respond with. The example program responds to SRV, PTR, A and AAAA records. Use the `mdns_string_extract` function to get the name string of the service record that was asked for. If service record name is `_services._dns-sd._udp.local.` you should use `mdns_discovery_answer` to send the records of the services you provide (DNS-SD). -If the service record name is a service you provide, use `mdns_query_answer` to send the service details back in response to the query. +If the service record name is a service you provide, use `mdns_query_answer_unicast` or `mdns_query_answer_multicast` depending on the response type flag in the question to send the service details back in response to the query. See the test executable implementation for more details on how to handle the parameters to the given functions. +### Announce + +If you provide a mDNS service listening and answering queries on port 5353 it is encouraged to send announcement on startup of your service (as an unsolicited answer). Use the `mdns_announce_multicast` to announce the records for your service at startup, and `mdns_goodbye_multicast` to announce the end of service on termination. + ## Test executable The `mdns.c` file contains a test executable implementation using the library to do DNS-SD and mDNS queries. Compile into an executable and run to see command line options for discovery, query and service modes. @@ -74,7 +84,8 @@ The `mdns.c` file contains a test executable implementation using the library to #### clang `clang -o mdns mdns.c` -## Using with cmake or conan +## Using with cmake, conan or vcpkg * use cmake with `FetchContent` or install and `find_package` * use conan with dependency name `mdns/20200130`, and `find_package` -> https://conan.io/center/mdns/20200130 +* use with vcpkg and cmake: `vcpkg install mdns` and `find_package` diff --git a/lib/mdns/mdns.c b/lib/mdns/mdns.c index 53a806fa6b2..959453fd4b7 100644 --- a/lib/mdns/mdns.c +++ b/lib/mdns/mdns.c @@ -5,37 +5,61 @@ #include -#include "mdns.h" - #include +#include #ifdef _WIN32 +#include #include #define sleep(x) Sleep(x * 1000) #else #include #include +#include +#include +#endif + +// Alias some things to simulate recieving data to fuzz library +#if defined(MDNS_FUZZING) +#define recvfrom(sock, buffer, capacity, flags, src_addr, addrlen) ((mdns_ssize_t)capacity) +#define printf +#endif + +#include "mdns.h" + +#if defined(MDNS_FUZZING) +#undef recvfrom #endif static char addrbuffer[64]; static char entrybuffer[256]; static char namebuffer[256]; -static char sendbuffer[256]; +static char sendbuffer[1024]; static mdns_record_txt_t txtbuffer[128]; -static uint32_t service_address_ipv4; -static uint8_t service_address_ipv6[16]; +static struct sockaddr_in service_address_ipv4; +static struct sockaddr_in6 service_address_ipv6; static int has_ipv4; static int has_ipv6; +volatile sig_atomic_t running = 1; + +// Data for our service including the mDNS records typedef struct { - const char* service; - const char* hostname; - uint32_t address_ipv4; - uint8_t* address_ipv6; + mdns_string_t service; + mdns_string_t hostname; + mdns_string_t service_instance; + mdns_string_t hostname_qualified; + struct sockaddr_in address_ipv4; + struct sockaddr_in6 address_ipv6; int port; -} service_record_t; + mdns_record_t record_ptr; + mdns_record_t record_srv; + mdns_record_t record_a; + mdns_record_t record_aaaa; + mdns_record_t txt_record[2]; +} service_t; static mdns_string_t ipv4_address_to_string(char* buffer, size_t capacity, const struct sockaddr_in* addr, @@ -88,6 +112,7 @@ ip_address_to_string(char* buffer, size_t capacity, const struct sockaddr* addr, return ipv4_address_to_string(buffer, capacity, (const struct sockaddr_in*)addr, addrlen); } +// Callback handling parsing answers to queries sent static int query_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry_type_t entry, uint16_t query_id, uint16_t rtype, uint16_t rclass, uint32_t ttl, const void* data, @@ -99,8 +124,8 @@ query_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry (void)sizeof(user_data); mdns_string_t fromaddrstr = ip_address_to_string(addrbuffer, sizeof(addrbuffer), from, addrlen); const char* entrytype = (entry == MDNS_ENTRYTYPE_ANSWER) ? - "answer" : - ((entry == MDNS_ENTRYTYPE_AUTHORITY) ? "authority" : "additional"); + "answer" : + ((entry == MDNS_ENTRYTYPE_AUTHORITY) ? "authority" : "additional"); mdns_string_t entrystr = mdns_string_extract(data, size, &name_offset, entrybuffer, sizeof(entrybuffer)); if (rtype == MDNS_RECORDTYPE_PTR) { @@ -151,72 +176,280 @@ query_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry return 0; } +// Callback handling questions incoming on service sockets static int service_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry_type_t entry, uint16_t query_id, uint16_t rtype, uint16_t rclass, uint32_t ttl, const void* data, size_t size, size_t name_offset, size_t name_length, size_t record_offset, size_t record_length, void* user_data) { - (void)sizeof(name_offset); - (void)sizeof(name_length); (void)sizeof(ttl); if (entry != MDNS_ENTRYTYPE_QUESTION) return 0; + + const char dns_sd[] = "_services._dns-sd._udp.local."; + const service_t* service = (const service_t*)user_data; + mdns_string_t fromaddrstr = ip_address_to_string(addrbuffer, sizeof(addrbuffer), from, addrlen); - if (rtype == MDNS_RECORDTYPE_PTR) { - mdns_string_t service = mdns_record_parse_ptr(data, size, record_offset, record_length, - namebuffer, sizeof(namebuffer)); - printf("%.*s : question PTR %.*s\n", MDNS_STRING_FORMAT(fromaddrstr), - MDNS_STRING_FORMAT(service)); - - const char dns_sd[] = "_services._dns-sd._udp.local."; - const service_record_t* service_record = (const service_record_t*)user_data; - size_t service_length = strlen(service_record->service); - if ((service.length == (sizeof(dns_sd) - 1)) && - (strncmp(service.str, dns_sd, sizeof(dns_sd) - 1) == 0)) { - printf(" --> answer %s\n", service_record->service); - mdns_discovery_answer(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), - service_record->service, service_length); - } else if ((service.length == service_length) && - (strncmp(service.str, service_record->service, service_length) == 0)) { + + size_t offset = name_offset; + mdns_string_t name = mdns_string_extract(data, size, &offset, namebuffer, sizeof(namebuffer)); + + const char* record_name = 0; + if (rtype == MDNS_RECORDTYPE_PTR) + record_name = "PTR"; + else if (rtype == MDNS_RECORDTYPE_SRV) + record_name = "SRV"; + else if (rtype == MDNS_RECORDTYPE_A) + record_name = "A"; + else if (rtype == MDNS_RECORDTYPE_AAAA) + record_name = "AAAA"; + else if (rtype == MDNS_RECORDTYPE_TXT) + record_name = "TXT"; + else if (rtype == MDNS_RECORDTYPE_ANY) + record_name = "ANY"; + else + return 0; + printf("Query %s %.*s\n", record_name, MDNS_STRING_FORMAT(name)); + + if ((name.length == (sizeof(dns_sd) - 1)) && + (strncmp(name.str, dns_sd, sizeof(dns_sd) - 1) == 0)) { + if ((rtype == MDNS_RECORDTYPE_PTR) || (rtype == MDNS_RECORDTYPE_ANY)) { + // The PTR query was for the DNS-SD domain, send answer with a PTR record for the + // service name we advertise, typically on the "<_service-name>._tcp.local." format + + // Answer PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + mdns_record_t answer = { + .name = name, .type = MDNS_RECORDTYPE_PTR, .data.ptr.name = service->service}; + + // Send the answer, unicast or multicast depending on flag in query uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); - printf(" --> answer %s.%s port %d (%s)\n", service_record->hostname, - service_record->service, service_record->port, + printf(" --> answer %.*s (%s)\n", MDNS_STRING_FORMAT(answer.data.ptr.name), (unicast ? "unicast" : "multicast")); - if (!unicast) - addrlen = 0; - char txt_record[] = "test=1"; - mdns_query_answer(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), query_id, - service_record->service, service_length, service_record->hostname, - strlen(service_record->hostname), service_record->address_ipv4, - service_record->address_ipv6, (uint16_t)service_record->port, - txt_record, sizeof(txt_record)); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, 0, + 0); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, 0, + 0); + } } - } else if (rtype == MDNS_RECORDTYPE_SRV) { - mdns_record_srv_t service = mdns_record_parse_srv(data, size, record_offset, record_length, - namebuffer, sizeof(namebuffer)); - printf("%.*s : question SRV %.*s\n", MDNS_STRING_FORMAT(fromaddrstr), - MDNS_STRING_FORMAT(service.name)); -#if 0 - if ((service.length == service_length) && - (strncmp(service.str, service_record->service, service_length) == 0)) { + } else if ((name.length == service->service.length) && + (strncmp(name.str, service->service.str, name.length) == 0)) { + if ((rtype == MDNS_RECORDTYPE_PTR) || (rtype == MDNS_RECORDTYPE_ANY)) { + // The PTR query was for our service (usually "<_service-name._tcp.local"), answer a PTR + // record reverse mapping the queried service name to our service instance name + // (typically on the ".<_service-name>._tcp.local." format), and add + // additional records containing the SRV record mapping the service instance name to our + // qualified hostname (typically ".local.") and port, as well as any IPv4/IPv6 + // address for the hostname as A/AAAA records, and two test TXT records + + // Answer PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + mdns_record_t answer = service->record_ptr; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // SRV record mapping ".<_service-name>._tcp.local." to + // ".local." with port. Set weight & priority to 0. + additional[additional_count++] = service->record_srv; + + // A/AAAA records mapping ".local." to IPv4/IPv6 addresses + if (service->address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service->record_a; + if (service->address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service->record_aaaa; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service->txt_record[0]; + additional[additional_count++] = service->txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); - printf(" --> answer %s.%s port %d (%s)\n", service_record->hostname, - service_record->service, service_record->port, + printf(" --> answer %.*s (%s)\n", + MDNS_STRING_FORMAT(service->record_ptr.data.ptr.name), (unicast ? "unicast" : "multicast")); - if (!unicast) - addrlen = 0; - char txt_record[] = "test=1"; - mdns_query_answer(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), query_id, - service_record->service, service_length, service_record->hostname, - strlen(service_record->hostname), service_record->address_ipv4, - service_record->address_ipv6, (uint16_t)service_record->port, - txt_record, sizeof(txt_record)); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } + } else if ((name.length == service->service_instance.length) && + (strncmp(name.str, service->service_instance.str, name.length) == 0)) { + if ((rtype == MDNS_RECORDTYPE_SRV) || (rtype == MDNS_RECORDTYPE_ANY)) { + // The SRV query was for our service instance (usually + // ".<_service-name._tcp.local"), answer a SRV record mapping the service + // instance name to our qualified hostname (typically ".local.") and port, as + // well as any IPv4/IPv6 address for the hostname as A/AAAA records, and two test TXT + // records + + // Answer PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + mdns_record_t answer = service->record_srv; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // A/AAAA records mapping ".local." to IPv4/IPv6 addresses + if (service->address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service->record_a; + if (service->address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service->record_aaaa; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service->txt_record[0]; + additional[additional_count++] = service->txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + printf(" --> answer %.*s port %d (%s)\n", + MDNS_STRING_FORMAT(service->record_srv.data.srv.name), service->port, + (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } + } else if ((name.length == service->hostname_qualified.length) && + (strncmp(name.str, service->hostname_qualified.str, name.length) == 0)) { + if (((rtype == MDNS_RECORDTYPE_A) || (rtype == MDNS_RECORDTYPE_ANY)) && + (service->address_ipv4.sin_family == AF_INET)) { + // The A query was for our qualified hostname (typically ".local.") and we + // have an IPv4 address, answer with an A record mappiing the hostname to an IPv4 + // address, as well as any IPv6 address for the hostname, and two test TXT records + + // Answer A records mapping ".local." to IPv4 address + mdns_record_t answer = service->record_a; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // AAAA record mapping ".local." to IPv6 addresses + if (service->address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service->record_aaaa; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service->txt_record[0]; + additional[additional_count++] = service->txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + mdns_string_t addrstr = ip_address_to_string( + addrbuffer, sizeof(addrbuffer), (struct sockaddr*)&service->record_a.data.a.addr, + sizeof(service->record_a.data.a.addr)); + printf(" --> answer %.*s IPv4 %.*s (%s)\n", MDNS_STRING_FORMAT(service->record_a.name), + MDNS_STRING_FORMAT(addrstr), (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } + } else if (((rtype == MDNS_RECORDTYPE_AAAA) || (rtype == MDNS_RECORDTYPE_ANY)) && + (service->address_ipv6.sin6_family == AF_INET6)) { + // The AAAA query was for our qualified hostname (typically ".local.") and we + // have an IPv6 address, answer with an AAAA record mappiing the hostname to an IPv6 + // address, as well as any IPv4 address for the hostname, and two test TXT records + + // Answer AAAA records mapping ".local." to IPv6 address + mdns_record_t answer = service->record_aaaa; + + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + + // A record mapping ".local." to IPv4 addresses + if (service->address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service->record_a; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + additional[additional_count++] = service->txt_record[0]; + additional[additional_count++] = service->txt_record[1]; + + // Send the answer, unicast or multicast depending on flag in query + uint16_t unicast = (rclass & MDNS_UNICAST_RESPONSE); + mdns_string_t addrstr = + ip_address_to_string(addrbuffer, sizeof(addrbuffer), + (struct sockaddr*)&service->record_aaaa.data.aaaa.addr, + sizeof(service->record_aaaa.data.aaaa.addr)); + printf(" --> answer %.*s IPv6 %.*s (%s)\n", + MDNS_STRING_FORMAT(service->record_aaaa.name), MDNS_STRING_FORMAT(addrstr), + (unicast ? "unicast" : "multicast")); + + if (unicast) { + mdns_query_answer_unicast(sock, from, addrlen, sendbuffer, sizeof(sendbuffer), + query_id, rtype, name.str, name.length, answer, 0, 0, + additional, additional_count); + } else { + mdns_query_answer_multicast(sock, sendbuffer, sizeof(sendbuffer), answer, 0, 0, + additional, additional_count); + } } -#endif } return 0; } +// Callback handling questions and answers dump +static int +dump_callback(int sock, const struct sockaddr* from, size_t addrlen, mdns_entry_type_t entry, + uint16_t query_id, uint16_t rtype, uint16_t rclass, uint32_t ttl, const void* data, + size_t size, size_t name_offset, size_t name_length, size_t record_offset, + size_t record_length, void* user_data) { + mdns_string_t fromaddrstr = ip_address_to_string(addrbuffer, sizeof(addrbuffer), from, addrlen); + + size_t offset = name_offset; + mdns_string_t name = mdns_string_extract(data, size, &offset, namebuffer, sizeof(namebuffer)); + + const char* record_name = 0; + if (rtype == MDNS_RECORDTYPE_PTR) + record_name = "PTR"; + else if (rtype == MDNS_RECORDTYPE_SRV) + record_name = "SRV"; + else if (rtype == MDNS_RECORDTYPE_A) + record_name = "A"; + else if (rtype == MDNS_RECORDTYPE_AAAA) + record_name = "AAAA"; + else if (rtype == MDNS_RECORDTYPE_TXT) + record_name = "TXT"; + else if (rtype == MDNS_RECORDTYPE_ANY) + record_name = "ANY"; + else + record_name = ""; + + const char* entry_type = "Question"; + if (entry == MDNS_ENTRYTYPE_ANSWER) + entry_type = "Answer"; + else if (entry == MDNS_ENTRYTYPE_AUTHORITY) + entry_type = "Authority"; + else if (entry == MDNS_ENTRYTYPE_ADDITIONAL) + entry_type = "Additional"; + + printf("%.*s: %s %s %.*s rclass 0x%x ttl %u\n", MDNS_STRING_FORMAT(fromaddrstr), entry_type, + record_name, MDNS_STRING_FORMAT(name), (unsigned int)rclass, ttl); + + return 0; +} + +// Open sockets for sending one-shot multicast queries from an ephemeral port static int open_client_sockets(int* sockets, int max_sockets, int port) { // When sending, each socket can only send to one network interface @@ -230,12 +463,13 @@ open_client_sockets(int* sockets, int max_sockets, int port) { unsigned int ret; unsigned int num_retries = 4; do { - adapter_address = malloc(address_size); + adapter_address = (IP_ADAPTER_ADDRESSES*)malloc(address_size); ret = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_ANYCAST, 0, adapter_address, &address_size); if (ret == ERROR_BUFFER_OVERFLOW) { free(adapter_address); adapter_address = 0; + address_size *= 2; } else { break; } @@ -265,7 +499,7 @@ open_client_sockets(int* sockets, int max_sockets, int port) { (saddr->sin_addr.S_un.S_un_b.s_b4 != 1)) { int log_addr = 0; if (first_ipv4) { - service_address_ipv4 = saddr->sin_addr.S_un.S_addr; + service_address_ipv4 = *saddr; first_ipv4 = 0; log_addr = 1; } @@ -289,6 +523,9 @@ open_client_sockets(int* sockets, int max_sockets, int port) { } } else if (unicast->Address.lpSockaddr->sa_family == AF_INET6) { struct sockaddr_in6* saddr = (struct sockaddr_in6*)unicast->Address.lpSockaddr; + // Ignore link-local addresses + if (saddr->sin6_scope_id) + continue; static const unsigned char localhost[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; static const unsigned char localhost_mapped[] = {0, 0, 0, 0, 0, 0, 0, 0, @@ -298,7 +535,7 @@ open_client_sockets(int* sockets, int max_sockets, int port) { memcmp(saddr->sin6_addr.s6_addr, localhost_mapped, 16)) { int log_addr = 0; if (first_ipv6) { - memcpy(service_address_ipv6, &saddr->sin6_addr, 16); + service_address_ipv6 = *saddr; first_ipv6 = 0; log_addr = 1; } @@ -339,13 +576,17 @@ open_client_sockets(int* sockets, int max_sockets, int port) { for (ifa = ifaddr; ifa; ifa = ifa->ifa_next) { if (!ifa->ifa_addr) continue; + if (!(ifa->ifa_flags & IFF_UP) || !(ifa->ifa_flags & IFF_MULTICAST)) + continue; + if ((ifa->ifa_flags & IFF_LOOPBACK) || (ifa->ifa_flags & IFF_POINTOPOINT)) + continue; if (ifa->ifa_addr->sa_family == AF_INET) { struct sockaddr_in* saddr = (struct sockaddr_in*)ifa->ifa_addr; if (saddr->sin_addr.s_addr != htonl(INADDR_LOOPBACK)) { int log_addr = 0; if (first_ipv4) { - service_address_ipv4 = saddr->sin_addr.s_addr; + service_address_ipv4 = *saddr; first_ipv4 = 0; log_addr = 1; } @@ -369,6 +610,9 @@ open_client_sockets(int* sockets, int max_sockets, int port) { } } else if (ifa->ifa_addr->sa_family == AF_INET6) { struct sockaddr_in6* saddr = (struct sockaddr_in6*)ifa->ifa_addr; + // Ignore link-local addresses + if (saddr->sin6_scope_id) + continue; static const unsigned char localhost[] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}; static const unsigned char localhost_mapped[] = {0, 0, 0, 0, 0, 0, 0, 0, @@ -377,7 +621,7 @@ open_client_sockets(int* sockets, int max_sockets, int port) { memcmp(saddr->sin6_addr.s6_addr, localhost_mapped, 16)) { int log_addr = 0; if (first_ipv6) { - memcpy(service_address_ipv6, &saddr->sin6_addr, 16); + service_address_ipv6 = *saddr; first_ipv6 = 0; log_addr = 1; } @@ -409,6 +653,7 @@ open_client_sockets(int* sockets, int max_sockets, int port) { return num_sockets; } +// Open sockets to listen to incoming mDNS queries on port 5353 static int open_service_sockets(int* sockets, int max_sockets) { // When recieving, each socket can recieve data from all network interfaces @@ -454,6 +699,7 @@ open_service_sockets(int* sockets, int max_sockets) { return num_sockets; } +// Send a DNS-SD query static int send_dns_sd(void) { int sockets[32]; @@ -462,7 +708,7 @@ send_dns_sd(void) { printf("Failed to open any client sockets\n"); return -1; } - printf("Opened %d socket%s for DNS-SD\n", num_sockets, num_sockets ? "s" : ""); + printf("Opened %d socket%s for DNS-SD\n", num_sockets, num_sockets > 1 ? "s" : ""); printf("Sending DNS-SD discovery\n"); for (int isock = 0; isock < num_sockets; ++isock) { @@ -513,8 +759,9 @@ send_dns_sd(void) { return 0; } +// Send a mDNS query static int -send_mdns_query(const char* service) { +send_mdns_query(mdns_query_t* query, size_t count) { int sockets[32]; int query_id[32]; int num_sockets = open_client_sockets(sockets, sizeof(sockets) / sizeof(sockets[0]), 0); @@ -527,12 +774,24 @@ send_mdns_query(const char* service) { size_t capacity = 2048; void* buffer = malloc(capacity); void* user_data = 0; - size_t records; - printf("Sending mDNS query: %s\n", service); + printf("Sending mDNS query"); + for (size_t iq = 0; iq < count; ++iq) { + const char* record_name = "PTR"; + if (query[iq].type == MDNS_RECORDTYPE_SRV) + record_name = "SRV"; + else if (query[iq].type == MDNS_RECORDTYPE_A) + record_name = "A"; + else if (query[iq].type == MDNS_RECORDTYPE_AAAA) + record_name = "AAAA"; + else + query[iq].type = MDNS_RECORDTYPE_PTR; + printf(" : %s %s", query[iq].name, record_name); + } + printf("\n"); for (int isock = 0; isock < num_sockets; ++isock) { - query_id[isock] = mdns_query_send(sockets[isock], MDNS_RECORDTYPE_PTR, service, - strlen(service), buffer, capacity, 0); + query_id[isock] = + mdns_multiquery_send(sockets[isock], query, count, buffer, capacity, 0); if (query_id[isock] < 0) printf("Failed to send mDNS query: %s\n", strerror(errno)); } @@ -540,9 +799,10 @@ send_mdns_query(const char* service) { // This is a simple implementation that loops for 5 seconds or as long as we get replies int res; printf("Reading mDNS query replies\n"); + int records = 0; do { struct timeval timeout; - timeout.tv_sec = 5; + timeout.tv_sec = 10; timeout.tv_usec = 0; int nfds = 0; @@ -554,19 +814,22 @@ send_mdns_query(const char* service) { FD_SET(sockets[isock], &readfs); } - records = 0; res = select(nfds, &readfs, 0, 0, &timeout); if (res > 0) { for (int isock = 0; isock < num_sockets; ++isock) { if (FD_ISSET(sockets[isock], &readfs)) { - records += mdns_query_recv(sockets[isock], buffer, capacity, query_callback, - user_data, query_id[isock]); + size_t rec = mdns_query_recv(sockets[isock], buffer, capacity, query_callback, + user_data, query_id[isock]); + if (rec > 0) + records += rec; } FD_SET(sockets[isock], &readfs); } } } while (res > 0); + printf("Read %d records\n", records); + free(buffer); for (int isock = 0; isock < num_sockets; ++isock) @@ -576,8 +839,9 @@ send_mdns_query(const char* service) { return 0; } +// Provide a mDNS service, answering incoming DNS-SD and mDNS queries static int -service_mdns(const char* hostname, const char* service, int service_port) { +service_mdns(const char* hostname, const char* service_name, int service_port) { int sockets[32]; int num_sockets = open_service_sockets(sockets, sizeof(sockets) / sizeof(sockets[0])); if (num_sockets <= 0) { @@ -586,21 +850,120 @@ service_mdns(const char* hostname, const char* service, int service_port) { } printf("Opened %d socket%s for mDNS service\n", num_sockets, num_sockets ? "s" : ""); - printf("Service mDNS: %s:%d\n", service, service_port); + size_t service_name_length = strlen(service_name); + if (!service_name_length) { + printf("Invalid service name\n"); + return -1; + } + + char* service_name_buffer = malloc(service_name_length + 2); + memcpy(service_name_buffer, service_name, service_name_length); + if (service_name_buffer[service_name_length - 1] != '.') + service_name_buffer[service_name_length++] = '.'; + service_name_buffer[service_name_length] = 0; + service_name = service_name_buffer; + + printf("Service mDNS: %s:%d\n", service_name, service_port); printf("Hostname: %s\n", hostname); size_t capacity = 2048; void* buffer = malloc(capacity); - service_record_t service_record; - service_record.service = service; - service_record.hostname = hostname; - service_record.address_ipv4 = has_ipv4 ? service_address_ipv4 : 0; - service_record.address_ipv6 = has_ipv6 ? service_address_ipv6 : 0; - service_record.port = service_port; + mdns_string_t service_string = (mdns_string_t){service_name, strlen(service_name)}; + mdns_string_t hostname_string = (mdns_string_t){hostname, strlen(hostname)}; + + // Build the service instance ".<_service-name>._tcp.local." string + char service_instance_buffer[256] = {0}; + snprintf(service_instance_buffer, sizeof(service_instance_buffer) - 1, "%.*s.%.*s", + MDNS_STRING_FORMAT(hostname_string), MDNS_STRING_FORMAT(service_string)); + mdns_string_t service_instance_string = + (mdns_string_t){service_instance_buffer, strlen(service_instance_buffer)}; + + // Build the ".local." string + char qualified_hostname_buffer[256] = {0}; + snprintf(qualified_hostname_buffer, sizeof(qualified_hostname_buffer) - 1, "%.*s.local.", + MDNS_STRING_FORMAT(hostname_string)); + mdns_string_t hostname_qualified_string = + (mdns_string_t){qualified_hostname_buffer, strlen(qualified_hostname_buffer)}; + + service_t service = {0}; + service.service = service_string; + service.hostname = hostname_string; + service.service_instance = service_instance_string; + service.hostname_qualified = hostname_qualified_string; + service.address_ipv4 = service_address_ipv4; + service.address_ipv6 = service_address_ipv6; + service.port = service_port; + + // Setup our mDNS records + + // PTR record reverse mapping "<_service-name>._tcp.local." to + // ".<_service-name>._tcp.local." + service.record_ptr = (mdns_record_t){.name = service.service, + .type = MDNS_RECORDTYPE_PTR, + .data.ptr.name = service.service_instance, + .rclass = 0, + .ttl = 0}; + + // SRV record mapping ".<_service-name>._tcp.local." to + // ".local." with port. Set weight & priority to 0. + service.record_srv = (mdns_record_t){.name = service.service_instance, + .type = MDNS_RECORDTYPE_SRV, + .data.srv.name = service.hostname_qualified, + .data.srv.port = service.port, + .data.srv.priority = 0, + .data.srv.weight = 0, + .rclass = 0, + .ttl = 0}; + + // A/AAAA records mapping ".local." to IPv4/IPv6 addresses + service.record_a = (mdns_record_t){.name = service.hostname_qualified, + .type = MDNS_RECORDTYPE_A, + .data.a.addr = service.address_ipv4, + .rclass = 0, + .ttl = 0}; + + service.record_aaaa = (mdns_record_t){.name = service.hostname_qualified, + .type = MDNS_RECORDTYPE_AAAA, + .data.aaaa.addr = service.address_ipv6, + .rclass = 0, + .ttl = 0}; + + // Add two test TXT records for our service instance name, will be coalesced into + // one record with both key-value pair strings by the library + service.txt_record[0] = (mdns_record_t){.name = service.service_instance, + .type = MDNS_RECORDTYPE_TXT, + .data.txt.key = {MDNS_STRING_CONST("test")}, + .data.txt.value = {MDNS_STRING_CONST("1")}, + .rclass = 0, + .ttl = 0}; + service.txt_record[1] = (mdns_record_t){.name = service.service_instance, + .type = MDNS_RECORDTYPE_TXT, + .data.txt.key = {MDNS_STRING_CONST("other")}, + .data.txt.value = {MDNS_STRING_CONST("value")}, + .rclass = 0, + .ttl = 0}; + + // Send an announcement on startup of service + { + printf("Sending announce\n"); + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + additional[additional_count++] = service.record_srv; + if (service.address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service.record_a; + if (service.address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service.record_aaaa; + additional[additional_count++] = service.txt_record[0]; + additional[additional_count++] = service.txt_record[1]; + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_announce_multicast(sockets[isock], buffer, capacity, service.record_ptr, 0, 0, + additional, additional_count); + } // This is a crude implementation that checks for incoming queries - while (1) { + while (running) { int nfds = 0; fd_set readfs; FD_ZERO(&readfs); @@ -610,11 +973,15 @@ service_mdns(const char* hostname, const char* service, int service_port) { FD_SET(sockets[isock], &readfs); } - if (select(nfds, &readfs, 0, 0, 0) >= 0) { + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + + if (select(nfds, &readfs, 0, 0, &timeout) >= 0) { for (int isock = 0; isock < num_sockets; ++isock) { if (FD_ISSET(sockets[isock], &readfs)) { mdns_socket_listen(sockets[isock], buffer, capacity, service_callback, - &service_record); + &service); } FD_SET(sockets[isock], &readfs); } @@ -623,7 +990,26 @@ service_mdns(const char* hostname, const char* service, int service_port) { } } + // Send a goodbye on end of service + { + printf("Sending goodbye\n"); + mdns_record_t additional[5] = {0}; + size_t additional_count = 0; + additional[additional_count++] = service.record_srv; + if (service.address_ipv4.sin_family == AF_INET) + additional[additional_count++] = service.record_a; + if (service.address_ipv6.sin6_family == AF_INET6) + additional[additional_count++] = service.record_aaaa; + additional[additional_count++] = service.txt_record[0]; + additional[additional_count++] = service.txt_record[1]; + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_goodbye_multicast(sockets[isock], buffer, capacity, service.record_ptr, 0, 0, + additional, additional_count); + } + free(buffer); + free(service_name_buffer); for (int isock = 0; isock < num_sockets; ++isock) mdns_socket_close(sockets[isock]); @@ -632,11 +1018,154 @@ service_mdns(const char* hostname, const char* service, int service_port) { return 0; } + +// Dump all incoming mDNS queries and answers +static int +dump_mdns(void) { + int sockets[32]; + int num_sockets = open_service_sockets(sockets, sizeof(sockets) / sizeof(sockets[0])); + if (num_sockets <= 0) { + printf("Failed to open any client sockets\n"); + return -1; + } + printf("Opened %d socket%s for mDNS dump\n", num_sockets, num_sockets ? "s" : ""); + + size_t capacity = 2048; + void* buffer = malloc(capacity); + + // This is a crude implementation that checks for incoming queries and answers + while (running) { + int nfds = 0; + fd_set readfs; + FD_ZERO(&readfs); + for (int isock = 0; isock < num_sockets; ++isock) { + if (sockets[isock] >= nfds) + nfds = sockets[isock] + 1; + FD_SET(sockets[isock], &readfs); + } + + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 100000; + + if (select(nfds, &readfs, 0, 0, &timeout) >= 0) { + for (int isock = 0; isock < num_sockets; ++isock) { + if (FD_ISSET(sockets[isock], &readfs)) { + mdns_socket_listen(sockets[isock], buffer, capacity, dump_callback, 0); + } + FD_SET(sockets[isock], &readfs); + } + } else { + break; + } + } + + free(buffer); + + for (int isock = 0; isock < num_sockets; ++isock) + mdns_socket_close(sockets[isock]); + printf("Closed socket%s\n", num_sockets ? "s" : ""); + + return 0; +} + +#ifdef MDNS_FUZZING + +#undef printf + +// Fuzzing by piping random data into the recieve functions +static void +fuzz_mdns(void) { +#define MAX_FUZZ_SIZE 4096 +#define MAX_PASSES (1024 * 1024 * 1024) + + static uint8_t fuzz_mdns_services_query[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, '_', + 's', 'e', 'r', 'v', 'i', 'c', 'e', 's', 0x07, '_', 'd', 'n', 's', '-', + 's', 'd', 0x04, '_', 'u', 'd', 'p', 0x05, 'l', 'o', 'c', 'a', 'l', 0x00}; + + uint8_t* buffer = malloc(MAX_FUZZ_SIZE); + uint8_t* strbuffer = malloc(MAX_FUZZ_SIZE); + for (int ipass = 0; ipass < MAX_PASSES; ++ipass) { + size_t size = rand() % MAX_FUZZ_SIZE; + for (size_t i = 0; i < size; ++i) + buffer[i] = rand() & 0xFF; + + if (ipass % 4) { + // Crafted fuzzing, make sure header is reasonable + memcpy(buffer, fuzz_mdns_services_query, sizeof(fuzz_mdns_services_query)); + uint16_t* header = (uint16_t*)buffer; + header[0] = 0; + header[1] = htons(0x8400); + for (int ival = 2; ival < 6; ++ival) + header[ival] = rand() & 0xFF; + } + mdns_discovery_recv(0, (void*)buffer, size, query_callback, 0); + + mdns_socket_listen(0, (void*)buffer, size, service_callback, 0); + + if (ipass % 4) { + // Crafted fuzzing, make sure header is reasonable (1 question claimed). + // Earlier passes will have done completely random data + uint16_t* header = (uint16_t*)buffer; + header[2] = htons(1); + } + mdns_query_recv(0, (void*)buffer, size, query_callback, 0, 0); + + // Fuzzing by piping random data into the parse functions + size_t offset = size ? (rand() % size) : 0; + size_t length = size ? (rand() % (size - offset)) : 0; + mdns_record_parse_ptr(buffer, size, offset, length, strbuffer, MAX_FUZZ_SIZE); + + offset = size ? (rand() % size) : 0; + length = size ? (rand() % (size - offset)) : 0; + mdns_record_parse_srv(buffer, size, offset, length, strbuffer, MAX_FUZZ_SIZE); + + struct sockaddr_in addr_ipv4; + offset = size ? (rand() % size) : 0; + length = size ? (rand() % (size - offset)) : 0; + mdns_record_parse_a(buffer, size, offset, length, &addr_ipv4); + + struct sockaddr_in6 addr_ipv6; + offset = size ? (rand() % size) : 0; + length = size ? (rand() % (size - offset)) : 0; + mdns_record_parse_aaaa(buffer, size, offset, length, &addr_ipv6); + + offset = size ? (rand() % size) : 0; + length = size ? (rand() % (size - offset)) : 0; + mdns_record_parse_txt(buffer, size, offset, length, (mdns_record_txt_t*)strbuffer, + MAX_FUZZ_SIZE); + + if (ipass && !(ipass % 10000)) + printf("Completed fuzzing pass %d\n", ipass); + } + + free(buffer); + free(strbuffer); +} + +#endif + +#ifdef _WIN32 +BOOL console_handler(DWORD signal) { + if (signal == CTRL_C_EVENT) { + running = 0; + } + return TRUE; +} +#else +void signal_handler(int signal) { + running = 0; +} +#endif + int main(int argc, const char* const* argv) { int mode = 0; const char* service = "_test-mdns._tcp.local."; const char* hostname = "dummy-host"; + mdns_query_t query[16]; + size_t query_count = 0; int service_port = 42424; #ifdef _WIN32 @@ -653,28 +1182,55 @@ main(int argc, const char* const* argv) { if (GetComputerNameA(hostname_buffer, &hostname_size)) hostname = hostname_buffer; + SetConsoleCtrlHandler(console_handler, TRUE); #else char hostname_buffer[256]; size_t hostname_size = sizeof(hostname_buffer); if (gethostname(hostname_buffer, hostname_size) == 0) hostname = hostname_buffer; - + signal(SIGINT, signal_handler); #endif for (int iarg = 0; iarg < argc; ++iarg) { if (strcmp(argv[iarg], "--discovery") == 0) { mode = 0; } else if (strcmp(argv[iarg], "--query") == 0) { + // Each query is either a service name, or a pair of record type and a service name + // For example: + // mdns --query _foo._tcp.local. + // mdns --query SRV myhost._foo._tcp.local. + // mdns --query A myhost._tcp.local. _service._tcp.local. mode = 1; ++iarg; - if (iarg < argc) - service = argv[iarg]; + while ((iarg < argc) && (query_count < 16)) { + query[query_count].name = argv[iarg++]; + query[query_count].type = MDNS_RECORDTYPE_PTR; + if (iarg < argc) { + mdns_record_type_t record_type = 0; + if (strcmp(query[query_count].name, "PTR") == 0) + record_type = MDNS_RECORDTYPE_PTR; + else if (strcmp(query[query_count].name, "SRV") == 0) + record_type = MDNS_RECORDTYPE_SRV; + else if (strcmp(query[query_count].name, "A") == 0) + record_type = MDNS_RECORDTYPE_A; + else if (strcmp(query[query_count].name, "AAAA") == 0) + record_type = MDNS_RECORDTYPE_AAAA; + if (record_type != 0) { + query[query_count].type = record_type; + query[query_count].name = argv[iarg++]; + } + } + query[query_count].length = strlen(query[query_count].name); + ++query_count; + } } else if (strcmp(argv[iarg], "--service") == 0) { mode = 2; ++iarg; if (iarg < argc) service = argv[iarg]; + } else if (strcmp(argv[iarg], "--dump") == 0) { + mode = 3; } else if (strcmp(argv[iarg], "--hostname") == 0) { ++iarg; if (iarg < argc) @@ -686,13 +1242,19 @@ main(int argc, const char* const* argv) { } } +#ifdef MDNS_FUZZING + fuzz_mdns(); +#else int ret; if (mode == 0) ret = send_dns_sd(); else if (mode == 1) - ret = send_mdns_query(service); + ret = send_mdns_query(query, query_count); else if (mode == 2) ret = service_mdns(hostname, service, service_port); + else if (mode == 3) + ret = dump_mdns(); +#endif #ifdef _WIN32 WSACleanup(); diff --git a/lib/mdns/mdns.h b/lib/mdns/mdns.h index 1a0e7bc70c3..fc0725e0f9d 100644 --- a/lib/mdns/mdns.h +++ b/lib/mdns/mdns.h @@ -21,8 +21,8 @@ #include #ifdef _WIN32 -#include -#include +#include +#include #define strncasecmp _strnicmp #else #include @@ -37,6 +37,7 @@ extern "C" { #define MDNS_INVALID_POS ((size_t)-1) #define MDNS_STRING_CONST(s) (s), (sizeof((s)) - 1) +#define MDNS_STRING_ARGS(s) s.str, s.length #define MDNS_STRING_FORMAT(s) (int)((s).length), s.str #define MDNS_POINTER_OFFSET(p, ofs) ((void*)((char*)(p) + (ptrdiff_t)(ofs))) @@ -46,6 +47,7 @@ extern "C" { #define MDNS_PORT 5353 #define MDNS_UNICAST_RESPONSE 0x8000U #define MDNS_CACHE_FLUSH 0x8000U +#define MDNS_MAX_SUBSTRINGS 64 enum mdns_record_type { MDNS_RECORDTYPE_IGNORE = 0, @@ -58,7 +60,9 @@ enum mdns_record_type { // IP6 Address [Thomson] MDNS_RECORDTYPE_AAAA = 28, // Server Selection [RFC2782] - MDNS_RECORDTYPE_SRV = 33 + MDNS_RECORDTYPE_SRV = 33, + // Any available records + MDNS_RECORDTYPE_ANY = 255 }; enum mdns_entry_type { @@ -68,7 +72,7 @@ enum mdns_entry_type { MDNS_ENTRYTYPE_ADDITIONAL = 3 }; -enum mdns_class { MDNS_CLASS_IN = 1 }; +enum mdns_class { MDNS_CLASS_IN = 1, MDNS_CLASS_ANY = 255 }; typedef enum mdns_record_type mdns_record_type_t; typedef enum mdns_entry_type mdns_entry_type_t; @@ -82,13 +86,22 @@ typedef int (*mdns_record_callback_fn)(int sock, const struct sockaddr* from, si typedef struct mdns_string_t mdns_string_t; typedef struct mdns_string_pair_t mdns_string_pair_t; +typedef struct mdns_string_table_item_t mdns_string_table_item_t; +typedef struct mdns_string_table_t mdns_string_table_t; +typedef struct mdns_record_t mdns_record_t; typedef struct mdns_record_srv_t mdns_record_srv_t; +typedef struct mdns_record_ptr_t mdns_record_ptr_t; +typedef struct mdns_record_a_t mdns_record_a_t; +typedef struct mdns_record_aaaa_t mdns_record_aaaa_t; typedef struct mdns_record_txt_t mdns_record_txt_t; +typedef struct mdns_query_t mdns_query_t; #ifdef _WIN32 typedef int mdns_size_t; +typedef int mdns_ssize_t; #else typedef size_t mdns_size_t; +typedef ssize_t mdns_ssize_t; #endif struct mdns_string_t { @@ -102,6 +115,12 @@ struct mdns_string_pair_t { int ref; }; +struct mdns_string_table_t { + size_t offset[16]; + size_t count; + size_t next; +}; + struct mdns_record_srv_t { uint16_t priority; uint16_t weight; @@ -109,11 +128,37 @@ struct mdns_record_srv_t { mdns_string_t name; }; +struct mdns_record_ptr_t { + mdns_string_t name; +}; + +struct mdns_record_a_t { + struct sockaddr_in addr; +}; + +struct mdns_record_aaaa_t { + struct sockaddr_in6 addr; +}; + struct mdns_record_txt_t { mdns_string_t key; mdns_string_t value; }; +struct mdns_record_t { + mdns_string_t name; + mdns_record_type_t type; + union mdns_record_data { + mdns_record_ptr_t ptr; + mdns_record_srv_t srv; + mdns_record_a_t a; + mdns_record_aaaa_t aaaa; + mdns_record_txt_t txt; + } data; + uint16_t rclass; + uint32_t ttl; +}; + struct mdns_header_t { uint16_t query_id; uint16_t flags; @@ -123,143 +168,223 @@ struct mdns_header_t { uint16_t additional_rrs; }; +struct mdns_query_t { + mdns_record_type_t type; + const char* name; + size_t length; +}; + // mDNS/DNS-SD public API -//! Open and setup a IPv4 socket for mDNS/DNS-SD. To bind the socket to a specific interface, -// pass in the appropriate socket address in saddr, otherwise pass a null pointer for INADDR_ANY. -// To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign -// a random user level ephemeral port. To run discovery service listening for incoming -// discoveries and queries, you must set MDNS_PORT as port. -static int -mdns_socket_open_ipv4(struct sockaddr_in* saddr); +//! Open and setup a IPv4 socket for mDNS/DNS-SD. To bind the socket to a specific interface, pass +//! in the appropriate socket address in saddr, otherwise pass a null pointer for INADDR_ANY. To +//! send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_open_ipv4(const struct sockaddr_in* saddr); //! Setup an already opened IPv4 socket for mDNS/DNS-SD. To bind the socket to a specific interface, -// pass in the appropriate socket address in saddr, otherwise pass a null pointer for INADDR_ANY. -// To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign -// a random user level ephemeral port. To run discovery service listening for incoming -// discoveries and queries, you must set MDNS_PORT as port. -static int -mdns_socket_setup_ipv4(int sock, struct sockaddr_in* saddr); - -//! Open and setup a IPv6 socket for mDNS/DNS-SD. To bind the socket to a specific interface, -// pass in the appropriate socket address in saddr, otherwise pass a null pointer for in6addr_any. -// To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign -// a random user level ephemeral port. To run discovery service listening for incoming -// discoveries and queries, you must set MDNS_PORT as port. -static int -mdns_socket_open_ipv6(struct sockaddr_in6* saddr); +//! pass in the appropriate socket address in saddr, otherwise pass a null pointer for INADDR_ANY. +//! To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_setup_ipv4(int sock, const struct sockaddr_in* saddr); + +//! Open and setup a IPv6 socket for mDNS/DNS-SD. To bind the socket to a specific interface, pass +//! in the appropriate socket address in saddr, otherwise pass a null pointer for in6addr_any. To +//! send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_open_ipv6(const struct sockaddr_in6* saddr); //! Setup an already opened IPv6 socket for mDNS/DNS-SD. To bind the socket to a specific interface, -// pass in the appropriate socket address in saddr, otherwise pass a null pointer for in6addr_any. -// To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign -// a random user level ephemeral port. To run discovery service listening for incoming -// discoveries and queries, you must set MDNS_PORT as port. -static int -mdns_socket_setup_ipv6(int sock, struct sockaddr_in6* saddr); +//! pass in the appropriate socket address in saddr, otherwise pass a null pointer for in6addr_any. +//! To send one-shot discovery requests and queries pass a null pointer or set 0 as port to assign a +//! random user level ephemeral port. To run discovery service listening for incoming discoveries +//! and queries, you must set MDNS_PORT as port. +static inline int +mdns_socket_setup_ipv6(int sock, const struct sockaddr_in6* saddr); //! Close a socket opened with mdns_socket_open_ipv4 and mdns_socket_open_ipv6. -static void +static inline void mdns_socket_close(int sock); -//! Listen for incoming multicast DNS-SD and mDNS query requests. The socket should have been -// opened on port MDNS_PORT using one of the mdns open or setup socket functions. Returns the -// number of queries parsed. -static size_t +//! Listen for incoming multicast DNS-SD and mDNS query requests. The socket should have been opened +//! on port MDNS_PORT using one of the mdns open or setup socket functions. Buffer must be 32 bit +//! aligned. Parsing is stopped when callback function returns non-zero. Returns the number of +//! queries parsed. +static inline size_t mdns_socket_listen(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, void* user_data); -//! Send a multicast DNS-SD reqeuest on the given socket to discover available services. Returns -// 0 on success, or <0 if error. -static int +//! Send a multicast DNS-SD reqeuest on the given socket to discover available services. Returns 0 +//! on success, or <0 if error. +static inline int mdns_discovery_send(int sock); //! Recieve unicast responses to a DNS-SD sent with mdns_discovery_send. Any data will be piped to -// the given callback for parsing. Returns the number of responses parsed. -static size_t +//! the given callback for parsing. Buffer must be 32 bit aligned. Parsing is stopped when callback +//! function returns non-zero. Returns the number of responses parsed. +static inline size_t mdns_discovery_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, void* user_data); -//! Send a unicast DNS-SD answer with a single record to the given address. Returns 0 if success, -// or <0 if error. -static int -mdns_discovery_answer(int sock, const void* address, size_t address_size, void* buffer, - size_t capacity, const char* record, size_t length); - //! Send a multicast mDNS query on the given socket for the given service name. The supplied buffer -// will be used to build the query packet. The query ID can be set to non-zero to filter responses, -// however the RFC states that the query ID SHOULD be set to 0 for multicast queries. The query -// will request a unicast response if the socket is bound to an ephemeral port, or a multicast -// response if the socket is bound to mDNS port 5353. -// Returns the used query ID, or <0 if error. -static int +//! will be used to build the query packet and must be 32 bit aligned. The query ID can be set to +//! non-zero to filter responses, however the RFC states that the query ID SHOULD be set to 0 for +//! multicast queries. The query will request a unicast response if the socket is bound to an +//! ephemeral port, or a multicast response if the socket is bound to mDNS port 5353. Returns the +//! used query ID, or <0 if error. +static inline int mdns_query_send(int sock, mdns_record_type_t type, const char* name, size_t length, void* buffer, size_t capacity, uint16_t query_id); +//! Send a multicast mDNS query on the given socket for the given service names. The supplied buffer +//! will be used to build the query packet and must be 32 bit aligned. The query ID can be set to +//! non-zero to filter responses, however the RFC states that the query ID SHOULD be set to 0 for +//! multicast queries. Each additional service name query consists of a triplet - a record type +//! (mdns_record_type_t), a name string pointer (const char*) and a name length (size_t). The list +//! of variable arguments should be terminated with a record type of 0. The query will request a +//! unicast response if the socket is bound to an ephemeral port, or a multicast response if the +//! socket is bound to mDNS port 5353. Returns the used query ID, or <0 if error. +static inline int +mdns_multiquery_send(int sock, const mdns_query_t* query, size_t count, void* buffer, + size_t capacity, uint16_t query_id); + //! Receive unicast responses to a mDNS query sent with mdns_discovery_recv, optionally filtering -// out any responses not matching the given query ID. Set the query ID to 0 to parse -// all responses, even if it is not matching the query ID set in a specific query. Any data will -// be piped to the given callback for parsing. Returns the number of responses parsed. -static size_t +//! out any responses not matching the given query ID. Set the query ID to 0 to parse all responses, +//! even if it is not matching the query ID set in a specific query. Any data will be piped to the +//! given callback for parsing. Buffer must be 32 bit aligned. Parsing is stopped when callback +//! function returns non-zero. Returns the number of responses parsed. +static inline size_t mdns_query_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, void* user_data, int query_id); -//! Send a unicast or multicast mDNS query answer with a single record to the given address. The -// answer will be sent multicast if address size is 0, otherwise it will be sent unicast to the -// given address. Use the top bit of the query class field (MDNS_UNICAST_RESPONSE) to determine -// if the answer should be sent unicast (bit set) or multicast (bit not set). -// Returns 0 if success, or <0 if error. -static int -mdns_query_answer(int sock, const void* address, size_t address_size, void* buffer, size_t capacity, - uint16_t query_id, const char* service, size_t service_length, - const char* hostname, size_t hostname_length, uint32_t ipv4, const uint8_t* ipv6, - uint16_t port, const char* txt, size_t txt_length); - -// Internal functions - -static mdns_string_t -mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, size_t capacity); - -static int -mdns_string_skip(const void* buffer, size_t size, size_t* offset); - -static int -mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, const void* buffer_rhs, - size_t size_rhs, size_t* ofs_rhs); - -static void* -mdns_string_make(void* data, size_t capacity, const char* name, size_t length); - -static void* -mdns_string_make_ref(void* data, size_t capacity, size_t ref_offset); - -static void* -mdns_string_make_with_ref(void* data, size_t capacity, const char* name, size_t length, - size_t ref_offset); - -static mdns_string_t +//! Send a variable unicast mDNS query answer to any question with variable number of records to the +//! given address. Use the top bit of the query class field (MDNS_UNICAST_RESPONSE) in the query +//! recieved to determine if the answer should be sent unicast (bit set) or multicast (bit not set). +//! Buffer must be 32 bit aligned. The record type and name should match the data from the query +//! recieved. Returns 0 if success, or <0 if error. +static inline int +mdns_query_answer_unicast(int sock, const void* address, size_t address_size, void* buffer, + size_t capacity, uint16_t query_id, mdns_record_type_t record_type, + const char* name, size_t name_length, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS query answer to any question with variable number of records. Use +//! the top bit of the query class field (MDNS_UNICAST_RESPONSE) in the query recieved to determine +//! if the answer should be sent unicast (bit set) or multicast (bit not set). Buffer must be 32 bit +//! aligned. Returns 0 if success, or <0 if error. +static inline int +mdns_query_answer_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS announcement (as an unsolicited answer) with variable number of +//! records.Buffer must be 32 bit aligned. Returns 0 if success, or <0 if error. Use this on service +//! startup to announce your instance to the local network. +static inline int +mdns_announce_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +//! Send a variable multicast mDNS announcement. Use this on service end for removing the resource +//! from the local network. The records must be identical to the according announcement. +static inline int +mdns_goodbye_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count); + +// Parse records functions + +//! Parse a PTR record, returns the name in the record +static inline mdns_string_t mdns_record_parse_ptr(const void* buffer, size_t size, size_t offset, size_t length, char* strbuffer, size_t capacity); -static mdns_record_srv_t +//! Parse a SRV record, returns the priority, weight, port and name in the record +static inline mdns_record_srv_t mdns_record_parse_srv(const void* buffer, size_t size, size_t offset, size_t length, char* strbuffer, size_t capacity); -static struct sockaddr_in* +//! Parse an A record, returns the IPv4 address in the record +static inline struct sockaddr_in* mdns_record_parse_a(const void* buffer, size_t size, size_t offset, size_t length, struct sockaddr_in* addr); -static struct sockaddr_in6* +//! Parse an AAAA record, returns the IPv6 address in the record +static inline struct sockaddr_in6* mdns_record_parse_aaaa(const void* buffer, size_t size, size_t offset, size_t length, struct sockaddr_in6* addr); -static size_t +//! Parse a TXT record, returns the number of key=value records parsed and stores the key-value +//! pairs in the supplied buffer +static inline size_t mdns_record_parse_txt(const void* buffer, size_t size, size_t offset, size_t length, mdns_record_txt_t* records, size_t capacity); +// Internal functions + +static inline mdns_string_t +mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, size_t capacity); + +static inline int +mdns_string_skip(const void* buffer, size_t size, size_t* offset); + +static inline size_t +mdns_string_find(const char* str, size_t length, char c, size_t offset); + +//! Compare if two strings are equal. If the strings are equal it returns >0 and the offset variables are +//! updated to the end of the corresponding strings. If the strings are not equal it returns 0 and +//! the offset variables are NOT updated. +static inline int +mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, const void* buffer_rhs, + size_t size_rhs, size_t* ofs_rhs); + +static inline void* +mdns_string_make(void* buffer, size_t capacity, void* data, const char* name, size_t length, + mdns_string_table_t* string_table); + +static inline size_t +mdns_string_table_find(mdns_string_table_t* string_table, const void* buffer, size_t capacity, + const char* str, size_t first_length, size_t total_length); + // Implementations -static int -mdns_socket_open_ipv4(struct sockaddr_in* saddr) { +static inline uint16_t +mdns_ntohs(const void* data) { + uint16_t aligned; + memcpy(&aligned, data, sizeof(uint16_t)); + return ntohs(aligned); +} + +static inline uint32_t +mdns_ntohl(const void* data) { + uint32_t aligned; + memcpy(&aligned, data, sizeof(uint32_t)); + return ntohl(aligned); +} + +static inline void* +mdns_htons(void* data, uint16_t val) { + val = htons(val); + memcpy(data, &val, sizeof(uint16_t)); + return MDNS_POINTER_OFFSET(data, sizeof(uint16_t)); +} + +static inline void* +mdns_htonl(void* data, uint32_t val) { + val = htonl(val); + memcpy(data, &val, sizeof(uint32_t)); + return MDNS_POINTER_OFFSET(data, sizeof(uint32_t)); +} + +static inline int +mdns_socket_open_ipv4(const struct sockaddr_in* saddr) { int sock = (int)socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (sock < 0) return -1; @@ -270,8 +395,8 @@ mdns_socket_open_ipv4(struct sockaddr_in* saddr) { return sock; } -static int -mdns_socket_setup_ipv4(int sock, struct sockaddr_in* saddr) { +static inline int +mdns_socket_setup_ipv4(int sock, const struct sockaddr_in* saddr) { unsigned char ttl = 1; unsigned char loopback = 1; unsigned int reuseaddr = 1; @@ -293,22 +418,22 @@ mdns_socket_setup_ipv4(int sock, struct sockaddr_in* saddr) { struct sockaddr_in sock_addr; if (!saddr) { - saddr = &sock_addr; - memset(saddr, 0, sizeof(struct sockaddr_in)); - saddr->sin_family = AF_INET; - saddr->sin_addr.s_addr = INADDR_ANY; + memset(&sock_addr, 0, sizeof(struct sockaddr_in)); + sock_addr.sin_family = AF_INET; + sock_addr.sin_addr.s_addr = INADDR_ANY; #ifdef __APPLE__ - saddr->sin_len = sizeof(struct sockaddr_in); + sock_addr.sin_len = sizeof(struct sockaddr_in); #endif } else { - setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, (const char*)&saddr->sin_addr, - sizeof(saddr->sin_addr)); + memcpy(&sock_addr, saddr, sizeof(struct sockaddr_in)); + setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, (const char*)&sock_addr.sin_addr, + sizeof(sock_addr.sin_addr)); #ifndef _WIN32 - saddr->sin_addr.s_addr = INADDR_ANY; + sock_addr.sin_addr.s_addr = INADDR_ANY; #endif } - if (bind(sock, (struct sockaddr*)saddr, sizeof(struct sockaddr_in))) + if (bind(sock, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in))) return -1; #ifdef _WIN32 @@ -322,8 +447,8 @@ mdns_socket_setup_ipv4(int sock, struct sockaddr_in* saddr) { return 0; } -static int -mdns_socket_open_ipv6(struct sockaddr_in6* saddr) { +static inline int +mdns_socket_open_ipv6(const struct sockaddr_in6* saddr) { int sock = (int)socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP); if (sock < 0) return -1; @@ -334,8 +459,8 @@ mdns_socket_open_ipv6(struct sockaddr_in6* saddr) { return sock; } -static int -mdns_socket_setup_ipv6(int sock, struct sockaddr_in6* saddr) { +static inline int +mdns_socket_setup_ipv6(int sock, const struct sockaddr_in6* saddr) { int hops = 1; unsigned int loopback = 1; unsigned int reuseaddr = 1; @@ -357,22 +482,22 @@ mdns_socket_setup_ipv6(int sock, struct sockaddr_in6* saddr) { struct sockaddr_in6 sock_addr; if (!saddr) { - saddr = &sock_addr; - memset(saddr, 0, sizeof(struct sockaddr_in6)); - saddr->sin6_family = AF_INET6; - saddr->sin6_addr = in6addr_any; + memset(&sock_addr, 0, sizeof(struct sockaddr_in6)); + sock_addr.sin6_family = AF_INET6; + sock_addr.sin6_addr = in6addr_any; #ifdef __APPLE__ - saddr->sin6_len = sizeof(struct sockaddr_in6); + sock_addr.sin6_len = sizeof(struct sockaddr_in6); #endif } else { + memcpy(&sock_addr, saddr, sizeof(struct sockaddr_in6)); unsigned int ifindex = 0; setsockopt(sock, IPPROTO_IPV6, IPV6_MULTICAST_IF, (const char*)&ifindex, sizeof(ifindex)); #ifndef _WIN32 - saddr->sin6_addr = in6addr_any; + sock_addr.sin6_addr = in6addr_any; #endif } - if (bind(sock, (struct sockaddr*)saddr, sizeof(struct sockaddr_in6))) + if (bind(sock, (struct sockaddr*)&sock_addr, sizeof(struct sockaddr_in6))) return -1; #ifdef _WIN32 @@ -386,7 +511,7 @@ mdns_socket_setup_ipv6(int sock, struct sockaddr_in6* saddr) { return 0; } -static void +static inline void mdns_socket_close(int sock) { #ifdef _WIN32 closesocket(sock); @@ -395,28 +520,33 @@ mdns_socket_close(int sock) { #endif } -static int +static inline int mdns_is_string_ref(uint8_t val) { return (0xC0 == (val & 0xC0)); } -static mdns_string_pair_t +static inline mdns_string_pair_t mdns_get_next_substring(const void* rawdata, size_t size, size_t offset) { const uint8_t* buffer = (const uint8_t*)rawdata; mdns_string_pair_t pair = {MDNS_INVALID_POS, 0, 0}; + if (offset >= size) + return pair; if (!buffer[offset]) { pair.offset = offset; return pair; } - if (mdns_is_string_ref(buffer[offset])) { + int recursion = 0; + while (mdns_is_string_ref(buffer[offset])) { if (size < offset + 2) return pair; - offset = 0x3fff & ntohs(*(uint16_t*)MDNS_POINTER_OFFSET(buffer, offset)); + offset = mdns_ntohs(MDNS_POINTER_OFFSET(buffer, offset)) & 0x3fff; if (offset >= size) return pair; pair.ref = 1; + if (++recursion > 16) + return pair; } size_t length = (size_t)buffer[offset++]; @@ -429,13 +559,14 @@ mdns_get_next_substring(const void* rawdata, size_t size, size_t offset) { return pair; } -static int +static inline int mdns_string_skip(const void* buffer, size_t size, size_t* offset) { size_t cur = *offset; mdns_string_pair_t substr; + unsigned int counter = 0; do { substr = mdns_get_next_substring(buffer, size, cur); - if (substr.offset == MDNS_INVALID_POS) + if ((substr.offset == MDNS_INVALID_POS) || (counter++ > MDNS_MAX_SUBSTRINGS)) return 0; if (substr.ref) { *offset = cur + 2; @@ -448,7 +579,7 @@ mdns_string_skip(const void* buffer, size_t size, size_t* offset) { return 1; } -static int +static inline int mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, const void* buffer_rhs, size_t size_rhs, size_t* ofs_rhs) { size_t lhs_cur = *ofs_lhs; @@ -457,15 +588,18 @@ mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, cons size_t rhs_end = MDNS_INVALID_POS; mdns_string_pair_t lhs_substr; mdns_string_pair_t rhs_substr; + unsigned int counter = 0; do { lhs_substr = mdns_get_next_substring(buffer_lhs, size_lhs, lhs_cur); rhs_substr = mdns_get_next_substring(buffer_rhs, size_rhs, rhs_cur); - if ((lhs_substr.offset == MDNS_INVALID_POS) || (rhs_substr.offset == MDNS_INVALID_POS)) + if ((lhs_substr.offset == MDNS_INVALID_POS) || (rhs_substr.offset == MDNS_INVALID_POS) || + (counter++ > MDNS_MAX_SUBSTRINGS)) return 0; if (lhs_substr.length != rhs_substr.length) return 0; - if (strncasecmp((const char*)buffer_rhs + rhs_substr.offset, - (const char*)buffer_lhs + lhs_substr.offset, rhs_substr.length)) + if (strncasecmp((const char*)MDNS_POINTER_OFFSET_CONST(buffer_rhs, rhs_substr.offset), + (const char*)MDNS_POINTER_OFFSET_CONST(buffer_lhs, lhs_substr.offset), + rhs_substr.length)) return 0; if (lhs_substr.ref && (lhs_end == MDNS_INVALID_POS)) lhs_end = lhs_cur + 2; @@ -486,7 +620,7 @@ mdns_string_equal(const void* buffer_lhs, size_t size_lhs, size_t* ofs_lhs, cons return 1; } -static mdns_string_t +static inline mdns_string_t mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, size_t capacity) { size_t cur = *offset; size_t end = MDNS_INVALID_POS; @@ -495,10 +629,11 @@ mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, result.str = str; result.length = 0; char* dst = str; + unsigned int counter = 0; size_t remain = capacity; do { substr = mdns_get_next_substring(buffer, size, cur); - if (substr.offset == MDNS_INVALID_POS) + if ((substr.offset == MDNS_INVALID_POS) || (counter++ > MDNS_MAX_SUBSTRINGS)) return result; if (substr.ref && (end == MDNS_INVALID_POS)) end = cur + 2; @@ -523,97 +658,147 @@ mdns_string_extract(const void* buffer, size_t size, size_t* offset, char* str, return result; } -static size_t +static inline size_t +mdns_string_table_find(mdns_string_table_t* string_table, const void* buffer, size_t capacity, + const char* str, size_t first_length, size_t total_length) { + if (!string_table) + return MDNS_INVALID_POS; + + for (size_t istr = 0; istr < string_table->count; ++istr) { + if (string_table->offset[istr] >= capacity) + continue; + size_t offset = 0; + mdns_string_pair_t sub_string = + mdns_get_next_substring(buffer, capacity, string_table->offset[istr]); + if (!sub_string.length || (sub_string.length != first_length)) + continue; + if (memcmp(str, MDNS_POINTER_OFFSET(buffer, sub_string.offset), sub_string.length)) + continue; + + // Initial substring matches, now match all remaining substrings + offset += first_length + 1; + while (offset < total_length) { + size_t dot_pos = mdns_string_find(str, total_length, '.', offset); + if (dot_pos == MDNS_INVALID_POS) + dot_pos = total_length; + size_t current_length = dot_pos - offset; + + sub_string = + mdns_get_next_substring(buffer, capacity, sub_string.offset + sub_string.length); + if (!sub_string.length || (sub_string.length != current_length)) + break; + if (memcmp(str + offset, MDNS_POINTER_OFFSET(buffer, sub_string.offset), + sub_string.length)) + break; + + offset = dot_pos + 1; + } + + // Return reference offset if entire string matches + if (offset >= total_length) + return string_table->offset[istr]; + } + + return MDNS_INVALID_POS; +} + +static inline void +mdns_string_table_add(mdns_string_table_t* string_table, size_t offset) { + if (!string_table) + return; + + string_table->offset[string_table->next] = offset; + + size_t table_capacity = sizeof(string_table->offset) / sizeof(string_table->offset[0]); + if (++string_table->count > table_capacity) + string_table->count = table_capacity; + if (++string_table->next >= table_capacity) + string_table->next = 0; +} + +static inline size_t mdns_string_find(const char* str, size_t length, char c, size_t offset) { const void* found; if (offset >= length) return MDNS_INVALID_POS; found = memchr(str + offset, c, length - offset); if (found) - return (size_t)((const char*)found - str); + return (size_t)MDNS_POINTER_DIFF(found, str); return MDNS_INVALID_POS; } -static void* -mdns_string_make(void* data, size_t capacity, const char* name, size_t length) { - size_t pos = 0; - size_t last_pos = 0; - size_t remain = capacity; - unsigned char* dest = (unsigned char*)data; - while ((last_pos < length) && - ((pos = mdns_string_find(name, length, '.', last_pos)) != MDNS_INVALID_POS)) { - size_t sublength = pos - last_pos; - if (sublength < remain) { - *dest = (unsigned char)sublength; - memcpy(dest + 1, name + last_pos, sublength); - dest += sublength + 1; - remain -= sublength + 1; - } else { - return 0; - } - last_pos = pos + 1; - } - if (last_pos < length) { - size_t sublength = length - last_pos; - if (sublength < remain) { - *dest = (unsigned char)sublength; - memcpy(dest + 1, name + last_pos, sublength); - dest += sublength + 1; - remain -= sublength + 1; - } else { - return 0; - } - } - if (!remain) - return 0; - *dest++ = 0; - return dest; -} - -static void* +static inline void* mdns_string_make_ref(void* data, size_t capacity, size_t ref_offset) { if (capacity < 2) return 0; - uint16_t* udata = (uint16_t*)data; - *udata++ = htons(0xC000 | (uint16_t)ref_offset); - return udata; + return mdns_htons(data, 0xC000 | (uint16_t)ref_offset); } -static void* -mdns_string_make_with_ref(void* data, size_t capacity, const char* name, size_t length, - size_t ref_offset) { - void* remaindata = mdns_string_make(data, capacity, name, length); - capacity -= MDNS_POINTER_DIFF(remaindata, data); - if (!data || !capacity) +static inline void* +mdns_string_make(void* buffer, size_t capacity, void* data, const char* name, size_t length, + mdns_string_table_t* string_table) { + size_t last_pos = 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (name[length - 1] == '.') + --length; + while (last_pos < length) { + size_t pos = mdns_string_find(name, length, '.', last_pos); + size_t sub_length = ((pos != MDNS_INVALID_POS) ? pos : length) - last_pos; + size_t total_length = length - last_pos; + + size_t ref_offset = + mdns_string_table_find(string_table, buffer, capacity, + (char*)MDNS_POINTER_OFFSET(name, last_pos), sub_length, + total_length); + if (ref_offset != MDNS_INVALID_POS) + return mdns_string_make_ref(data, remain, ref_offset); + + if (remain <= (sub_length + 1)) + return 0; + + *(unsigned char*)data = (unsigned char)sub_length; + memcpy(MDNS_POINTER_OFFSET(data, 1), name + last_pos, sub_length); + mdns_string_table_add(string_table, MDNS_POINTER_DIFF(data, buffer)); + + data = MDNS_POINTER_OFFSET(data, sub_length + 1); + last_pos = ((pos != MDNS_INVALID_POS) ? pos + 1 : length); + remain = capacity - MDNS_POINTER_DIFF(data, buffer); + } + + if (!remain) return 0; - return mdns_string_make_ref(MDNS_POINTER_OFFSET(remaindata, -1), capacity + 1, ref_offset); + + *(unsigned char*)data = 0; + return MDNS_POINTER_OFFSET(data, 1); } -static size_t +static inline size_t mdns_records_parse(int sock, const struct sockaddr* from, size_t addrlen, const void* buffer, size_t size, size_t* offset, mdns_entry_type_t type, uint16_t query_id, size_t records, mdns_record_callback_fn callback, void* user_data) { size_t parsed = 0; - int do_callback = (callback ? 1 : 0); for (size_t i = 0; i < records; ++i) { size_t name_offset = *offset; mdns_string_skip(buffer, size, offset); + if (((*offset) + 10) > size) + return parsed; size_t name_length = (*offset) - name_offset; - const uint16_t* data = (const uint16_t*)((const char*)buffer + (*offset)); + const uint16_t* data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, *offset); - uint16_t rtype = ntohs(*data++); - uint16_t rclass = ntohs(*data++); - uint32_t ttl = ntohl(*(const uint32_t*)(const void*)data); + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint32_t ttl = mdns_ntohl(data); data += 2; - uint16_t length = ntohs(*data++); + uint16_t length = mdns_ntohs(data++); *offset += 10; - if (do_callback) { + if (length <= (size - (*offset))) { ++parsed; - if (callback(sock, from, addrlen, type, query_id, rtype, rclass, ttl, buffer, size, + if (callback && + callback(sock, from, addrlen, type, query_id, rtype, rclass, ttl, buffer, size, name_offset, name_length, *offset, length, user_data)) - do_callback = 0; + break; } *offset += length; @@ -621,7 +806,7 @@ mdns_records_parse(int sock, const struct sockaddr* from, size_t addrlen, const return parsed; } -static int +static inline int mdns_unicast_send(int sock, const void* address, size_t address_size, const void* buffer, size_t size) { if (sendto(sock, (const char*)buffer, (mdns_size_t)size, 0, (const struct sockaddr*)address, @@ -630,7 +815,7 @@ mdns_unicast_send(int sock, const void* address, size_t address_size, const void return 0; } -static int +static inline int mdns_multicast_send(int sock, const void* buffer, size_t size) { struct sockaddr_storage addr_storage; struct sockaddr_in addr; @@ -685,12 +870,12 @@ static const uint8_t mdns_services_query[] = { // QU (unicast response) and class IN 0x80, MDNS_CLASS_IN}; -static int +static inline int mdns_discovery_send(int sock) { return mdns_multicast_send(sock, mdns_services_query, sizeof(mdns_services_query)); } -static size_t +static inline size_t mdns_discovery_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, void* user_data) { struct sockaddr_in6 addr; @@ -700,20 +885,20 @@ mdns_discovery_recv(int sock, void* buffer, size_t capacity, mdns_record_callbac #ifdef __APPLE__ saddr->sa_len = sizeof(addr); #endif - int ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); if (ret <= 0) return 0; size_t data_size = (size_t)ret; size_t records = 0; - uint16_t* data = (uint16_t*)buffer; + const uint16_t* data = (uint16_t*)buffer; - uint16_t query_id = ntohs(*data++); - uint16_t flags = ntohs(*data++); - uint16_t questions = ntohs(*data++); - uint16_t answer_rrs = ntohs(*data++); - uint16_t authority_rrs = ntohs(*data++); - uint16_t additional_rrs = ntohs(*data++); + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); // According to RFC 6762 the query ID MUST match the sent query ID (which is 0 in our case) if (query_id || (flags != 0x8400)) @@ -721,70 +906,80 @@ mdns_discovery_recv(int sock, void* buffer, size_t capacity, mdns_record_callbac // It seems some implementations do not fill the correct questions field, // so ignore this check for now and only validate answer string - /* - if (questions != 1) - return 0; - */ + // if (questions != 1) + // return 0; int i; for (i = 0; i < questions; ++i) { - size_t ofs = (size_t)((char*)data - (char*)buffer); - size_t verify_ofs = 12; + size_t offset = MDNS_POINTER_DIFF(data, buffer); + size_t verify_offset = 12; // Verify it's our question, _services._dns-sd._udp.local. - if (!mdns_string_equal(buffer, data_size, &ofs, mdns_services_query, - sizeof(mdns_services_query), &verify_ofs)) + if (!mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_offset)) return 0; - data = (uint16_t*)((char*)buffer + ofs); + data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, offset); - uint16_t rtype = ntohs(*data++); - uint16_t rclass = ntohs(*data++); + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); // Make sure we get a reply based on our PTR question for class IN if ((rtype != MDNS_RECORDTYPE_PTR) || ((rclass & 0x7FFF) != MDNS_CLASS_IN)) return 0; } - int do_callback = 1; for (i = 0; i < answer_rrs; ++i) { - size_t ofs = (size_t)((char*)data - (char*)buffer); - size_t verify_ofs = 12; + size_t offset = MDNS_POINTER_DIFF(data, buffer); + size_t verify_offset = 12; // Verify it's an answer to our question, _services._dns-sd._udp.local. - size_t name_offset = ofs; - int is_answer = mdns_string_equal(buffer, data_size, &ofs, mdns_services_query, - sizeof(mdns_services_query), &verify_ofs); - size_t name_length = ofs - name_offset; - data = (uint16_t*)((char*)buffer + ofs); - - uint16_t rtype = ntohs(*data++); - uint16_t rclass = ntohs(*data++); - uint32_t ttl = ntohl(*(uint32_t*)(void*)data); + size_t name_offset = offset; + int is_answer = mdns_string_equal(buffer, data_size, &offset, mdns_services_query, + sizeof(mdns_services_query), &verify_offset); + if (!is_answer && !mdns_string_skip(buffer, data_size, &offset)) + break; + size_t name_length = offset - name_offset; + if ((offset + 10) > data_size) + return records; + data = (const uint16_t*)MDNS_POINTER_OFFSET(buffer, offset); + + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint32_t ttl = mdns_ntohl(data); data += 2; - uint16_t length = ntohs(*data++); - if (length >= (data_size - ofs)) + uint16_t length = mdns_ntohs(data++); + if (length > (data_size - offset)) return 0; - if (is_answer && do_callback) { + if (is_answer) { ++records; - ofs = (size_t)((char*)data - (char*)buffer); - if (callback(sock, saddr, addrlen, MDNS_ENTRYTYPE_ANSWER, query_id, rtype, rclass, ttl, - buffer, data_size, name_offset, name_length, ofs, length, user_data)) - do_callback = 0; + offset = MDNS_POINTER_DIFF(data, buffer); + if (callback && + callback(sock, saddr, addrlen, MDNS_ENTRYTYPE_ANSWER, query_id, rtype, rclass, ttl, + buffer, data_size, name_offset, name_length, offset, length, user_data)) + return records; } - data = (uint16_t*)((char*)data + length); + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(data, length); } - size_t offset = (size_t)((char*)data - (char*)buffer); - records += + size_t total_records = records; + size_t offset = MDNS_POINTER_DIFF(data, buffer); + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); - records += mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, - MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, - user_data); - - return records; + total_records += records; + if (records != authority_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + total_records += records; + if (records != additional_rrs) + return total_records; + + return total_records; } -static size_t +static inline size_t mdns_socket_listen(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, void* user_data) { struct sockaddr_in6 addr; @@ -794,104 +989,94 @@ mdns_socket_listen(int sock, void* buffer, size_t capacity, mdns_record_callback #ifdef __APPLE__ saddr->sa_len = sizeof(addr); #endif - int ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); if (ret <= 0) return 0; size_t data_size = (size_t)ret; - uint16_t* data = (uint16_t*)buffer; - - uint16_t query_id = ntohs(*data++); - uint16_t flags = ntohs(*data++); - uint16_t questions = ntohs(*data++); - /* - This data is unused at the moment, skip - uint16_t answer_rrs = ntohs(*data++); - uint16_t authority_rrs = ntohs(*data++); - uint16_t additional_rrs = ntohs(*data++); - */ - data += 3; + const uint16_t* data = (const uint16_t*)buffer; - size_t parsed = 0; + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); + + size_t records; + size_t total_records = 0; for (int iquestion = 0; iquestion < questions; ++iquestion) { - size_t question_offset = (size_t)((char*)data - (char*)buffer); + size_t question_offset = MDNS_POINTER_DIFF(data, buffer); size_t offset = question_offset; - size_t verify_ofs = 12; + size_t verify_offset = 12; + int dns_sd = 0; if (mdns_string_equal(buffer, data_size, &offset, mdns_services_query, - sizeof(mdns_services_query), &verify_ofs)) { - if (flags || (questions != 1)) - return 0; - } else { - offset = question_offset; - if (!mdns_string_skip(buffer, data_size, &offset)) - break; + sizeof(mdns_services_query), &verify_offset)) { + dns_sd = 1; + } else if (!mdns_string_skip(buffer, data_size, &offset)) { + break; } size_t length = offset - question_offset; - data = (uint16_t*)((char*)buffer + offset); + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); - uint16_t rtype = ntohs(*data++); - uint16_t rclass = ntohs(*data++); + uint16_t rtype = mdns_ntohs(data++); + uint16_t rclass = mdns_ntohs(data++); + uint16_t class_without_flushbit = rclass & ~MDNS_CACHE_FLUSH; - // Make sure we get a question of class IN - if ((rclass & 0x7FFF) != MDNS_CLASS_IN) - return 0; + // Make sure we get a question of class IN or ANY + if (!((class_without_flushbit == MDNS_CLASS_IN) || + (class_without_flushbit == MDNS_CLASS_ANY))) { + break; + } - if (callback) - callback(sock, saddr, addrlen, MDNS_ENTRYTYPE_QUESTION, query_id, rtype, rclass, 0, - buffer, data_size, question_offset, length, question_offset, length, - user_data); + if (dns_sd && flags) + continue; - ++parsed; + ++total_records; + if (callback && callback(sock, saddr, addrlen, MDNS_ENTRYTYPE_QUESTION, query_id, rtype, + rclass, 0, buffer, data_size, question_offset, length, + question_offset, length, user_data)) + return total_records; } - return parsed; -} + size_t offset = MDNS_POINTER_DIFF(data, buffer); + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ANSWER, query_id, answer_rrs, callback, user_data); + total_records += records; + if (records != answer_rrs) + return total_records; -static int -mdns_discovery_answer(int sock, const void* address, size_t address_size, void* buffer, - size_t capacity, const char* record, size_t length) { - if (capacity < (sizeof(mdns_services_query) + 32 + length)) - return -1; + records = + mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); + total_records += records; + if (records != authority_rrs) + return total_records; - uint16_t* data = (uint16_t*)buffer; - // Basic reply structure - memcpy(data, mdns_services_query, sizeof(mdns_services_query)); - // Flags - uint16_t* flags = data + 1; - *flags = htons(0x8400U); - // One answer - uint16_t* answers = data + 3; - *answers = htons(1); - - // Fill in answer PTR record - data = (uint16_t*)((char*)buffer + sizeof(mdns_services_query)); - // Reference _services._dns-sd._udp.local. string in question - *data++ = htons(0xC000U | 12U); - // Type - *data++ = htons(MDNS_RECORDTYPE_PTR); - // Rclass - *data++ = htons(MDNS_CLASS_IN); - // TTL - *(uint32_t*)data = htonl(10); - data += 2; - // Record string length - uint16_t* record_length = data++; - uint8_t* record_data = (uint8_t*)data; - size_t remain = capacity - (sizeof(mdns_services_query) + 10); - record_data = (uint8_t*)mdns_string_make(record_data, remain, record, length); - *record_length = htons((uint16_t)(record_data - (uint8_t*)data)); - *record_data++ = 0; - - ptrdiff_t tosend = (char*)record_data - (char*)buffer; - return mdns_unicast_send(sock, address, address_size, buffer, (size_t)tosend); + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + + return total_records; } -static int +static inline int mdns_query_send(int sock, mdns_record_type_t type, const char* name, size_t length, void* buffer, size_t capacity, uint16_t query_id) { - if (capacity < (17 + length)) + mdns_query_t query; + query.type = type; + query.name = name; + query.length = length; + return mdns_multiquery_send(sock, &query, 1, buffer, capacity, query_id); +} + +static inline int +mdns_multiquery_send(int sock, const mdns_query_t* query, size_t count, void* buffer, size_t capacity, + uint16_t query_id) { + if (!count || (capacity < (sizeof(struct mdns_header_t) + (6 * count)))) return -1; + // Ask for a unicast response since it's a one-shot query uint16_t rclass = MDNS_CLASS_IN | MDNS_UNICAST_RESPONSE; struct sockaddr_storage addr_storage; @@ -906,34 +1091,37 @@ mdns_query_send(int sock, mdns_record_type_t type, const char* name, size_t leng rclass &= ~MDNS_UNICAST_RESPONSE; } - uint16_t* data = (uint16_t*)buffer; + struct mdns_header_t* header = (struct mdns_header_t*)buffer; // Query ID - *data++ = htons(query_id); + header->query_id = htons((unsigned short)query_id); // Flags - *data++ = 0; + header->flags = 0; // Questions - *data++ = htons(1); + header->questions = htons((unsigned short)count); // No answer, authority or additional RRs - *data++ = 0; - *data++ = 0; - *data++ = 0; - // Fill in question - // Name string - data = (uint16_t*)mdns_string_make(data, capacity - 17, name, length); - if (!data) - return -1; - // Record type - *data++ = htons(type); - //! Optional unicast response based on local port, class IN - *data++ = htons(rclass); + header->answer_rrs = 0; + header->authority_rrs = 0; + header->additional_rrs = 0; + // Fill in questions + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + for (size_t iq = 0; iq < count; ++iq) { + // Name string + data = mdns_string_make(buffer, capacity, data, query[iq].name, query[iq].length, 0); + if (!data) + return -1; + // Record type + data = mdns_htons(data, query[iq].type); + //! Optional unicast response based on local port, class IN + data = mdns_htons(data, rclass); + } - ptrdiff_t tosend = (char*)data - (char*)buffer; + size_t tosend = MDNS_POINTER_DIFF(data, buffer); if (mdns_multicast_send(sock, buffer, (size_t)tosend)) return -1; return query_id; } -static size_t +static inline size_t mdns_query_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn callback, void* user_data, int only_query_id) { struct sockaddr_in6 addr; @@ -943,217 +1131,373 @@ mdns_query_recv(int sock, void* buffer, size_t capacity, mdns_record_callback_fn #ifdef __APPLE__ saddr->sa_len = sizeof(addr); #endif - int ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); + mdns_ssize_t ret = recvfrom(sock, (char*)buffer, (mdns_size_t)capacity, 0, saddr, &addrlen); if (ret <= 0) return 0; size_t data_size = (size_t)ret; - uint16_t* data = (uint16_t*)buffer; - - uint16_t query_id = ntohs(*data++); - uint16_t flags = ntohs(*data++); - uint16_t questions = ntohs(*data++); - uint16_t answer_rrs = ntohs(*data++); - uint16_t authority_rrs = ntohs(*data++); - uint16_t additional_rrs = ntohs(*data++); + const uint16_t* data = (const uint16_t*)buffer; + + uint16_t query_id = mdns_ntohs(data++); + uint16_t flags = mdns_ntohs(data++); + uint16_t questions = mdns_ntohs(data++); + uint16_t answer_rrs = mdns_ntohs(data++); + uint16_t authority_rrs = mdns_ntohs(data++); + uint16_t additional_rrs = mdns_ntohs(data++); (void)sizeof(flags); if ((only_query_id > 0) && (query_id != only_query_id)) return 0; // Not a reply to the wanted one-shot query - if (questions > 1) - return 0; - // Skip questions part int i; for (i = 0; i < questions; ++i) { - size_t ofs = (size_t)((char*)data - (char*)buffer); - if (!mdns_string_skip(buffer, data_size, &ofs)) + size_t offset = MDNS_POINTER_DIFF(data, buffer); + if (!mdns_string_skip(buffer, data_size, &offset)) return 0; - data = (uint16_t*)((char*)buffer + ofs); - uint16_t rtype = ntohs(*data++); - uint16_t rclass = ntohs(*data++); - (void)sizeof(rtype); - (void)sizeof(rclass); + data = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); + // Record type and class not used, skip + // uint16_t rtype = mdns_ntohs(data++); + // uint16_t rclass = mdns_ntohs(data++); + data += 2; } size_t records = 0; + size_t total_records = 0; size_t offset = MDNS_POINTER_DIFF(data, buffer); - records += mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, - MDNS_ENTRYTYPE_ANSWER, query_id, answer_rrs, callback, user_data); - records += + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ANSWER, query_id, answer_rrs, callback, user_data); + total_records += records; + if (records != answer_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, MDNS_ENTRYTYPE_AUTHORITY, query_id, authority_rrs, callback, user_data); - records += mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, - MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, - user_data); - return records; + total_records += records; + if (records != authority_rrs) + return total_records; + + records = mdns_records_parse(sock, saddr, addrlen, buffer, data_size, &offset, + MDNS_ENTRYTYPE_ADDITIONAL, query_id, additional_rrs, callback, + user_data); + total_records += records; + if (records != additional_rrs) + return total_records; + + return total_records; } -static int -mdns_query_answer(int sock, const void* address, size_t address_size, void* buffer, size_t capacity, - uint16_t query_id, const char* service, size_t service_length, - const char* hostname, size_t hostname_length, uint32_t ipv4, const uint8_t* ipv6, - uint16_t port, const char* txt, size_t txt_length) { - if (capacity < (sizeof(struct mdns_header_t) + 32 + service_length + hostname_length)) - return -1; +static inline void* +mdns_answer_add_question_unicast(void* buffer, size_t capacity, void* data, + mdns_record_type_t record_type, const char* name, + size_t name_length, mdns_string_table_t* string_table) { + data = mdns_string_make(buffer, capacity, data, name, name_length, string_table); + if (!data) + return 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (remain < 4) + return 0; + + data = mdns_htons(data, record_type); + data = mdns_htons(data, MDNS_UNICAST_RESPONSE | MDNS_CLASS_IN); + + return data; +} + +static inline void* +mdns_answer_add_record_header(void* buffer, size_t capacity, void* data, mdns_record_t record, + mdns_string_table_t* string_table) { + data = mdns_string_make(buffer, capacity, data, record.name.str, record.name.length, string_table); + if (!data) + return 0; + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if (remain < 10) + return 0; + + data = mdns_htons(data, record.type); + data = mdns_htons(data, record.rclass); + data = mdns_htonl(data, record.ttl); + data = mdns_htons(data, 0); // Length, to be filled later + return data; +} + +static inline void* +mdns_answer_add_record(void* buffer, size_t capacity, void* data, mdns_record_t record, + mdns_string_table_t* string_table) { + // TXT records will be coalesced into one record later + if (!data || (record.type == MDNS_RECORDTYPE_TXT)) + return data; + + data = mdns_answer_add_record_header(buffer, capacity, data, record, string_table); + if (!data) + return 0; + + // Pointer to length of record to be filled at end + void* record_length = MDNS_POINTER_OFFSET(data, -2); + void* record_data = data; + + size_t remain = capacity - MDNS_POINTER_DIFF(data, buffer); + switch (record.type) { + case MDNS_RECORDTYPE_PTR: + data = mdns_string_make(buffer, capacity, data, record.data.ptr.name.str, + record.data.ptr.name.length, string_table); + break; + + case MDNS_RECORDTYPE_SRV: + if (remain <= 6) + return 0; + data = mdns_htons(data, record.data.srv.priority); + data = mdns_htons(data, record.data.srv.weight); + data = mdns_htons(data, record.data.srv.port); + data = mdns_string_make(buffer, capacity, data, record.data.srv.name.str, + record.data.srv.name.length, string_table); + break; + + case MDNS_RECORDTYPE_A: + if (remain < 4) + return 0; + memcpy(data, &record.data.a.addr.sin_addr.s_addr, 4); + data = MDNS_POINTER_OFFSET(data, 4); + break; - int unicast = (address_size ? 1 : 0); - int use_ipv4 = (ipv4 != 0); - int use_ipv6 = (ipv6 != 0); - int use_txt = (txt && txt_length && (txt_length <= 255)); + case MDNS_RECORDTYPE_AAAA: + if (remain < 16) + return 0; + memcpy(data, &record.data.aaaa.addr.sin6_addr, 16); // ipv6 address + data = MDNS_POINTER_OFFSET(data, 16); + break; + + default: + break; + } + + if (!data) + return 0; + + // Fill record length + mdns_htons(record_length, (uint16_t)MDNS_POINTER_DIFF(data, record_data)); + return data; +} + +static inline void +mdns_record_update_rclass_ttl(mdns_record_t* record, uint16_t rclass, uint32_t ttl) { + if (!record->rclass) + record->rclass = rclass; + if (!record->ttl || !ttl) + record->ttl = ttl; + record->rclass &= (uint16_t)(MDNS_CLASS_IN | MDNS_CACHE_FLUSH); + // Never flush PTR record + if (record->type == MDNS_RECORDTYPE_PTR) + record->rclass &= ~(uint16_t)MDNS_CACHE_FLUSH; +} - uint16_t question_rclass = (unicast ? MDNS_UNICAST_RESPONSE : 0) | MDNS_CLASS_IN; - uint16_t rclass = (unicast ? MDNS_CACHE_FLUSH : 0) | MDNS_CLASS_IN; - uint32_t ttl = (unicast ? 10 : 60); - uint32_t a_ttl = ttl; +static inline void* +mdns_answer_add_txt_record(void* buffer, size_t capacity, void* data, const mdns_record_t* records, + size_t record_count, uint16_t rclass, uint32_t ttl, + mdns_string_table_t* string_table) { + // Pointer to length of record to be filled at end + void* record_length = 0; + void* record_data = 0; + + size_t remain = 0; + for (size_t irec = 0; data && (irec < record_count); ++irec) { + if (records[irec].type != MDNS_RECORDTYPE_TXT) + continue; + + mdns_record_t record = records[irec]; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + if (!record_data) { + data = mdns_answer_add_record_header(buffer, capacity, data, record, string_table); + if (!data) + return data; + record_length = MDNS_POINTER_OFFSET(data, -2); + record_data = data; + } + + // TXT strings are unlikely to be shared, just make then raw. Also need one byte for + // termination, thus the <= check + size_t string_length = record.data.txt.key.length + record.data.txt.value.length + 1; + if (!data) + return 0; + remain = capacity - MDNS_POINTER_DIFF(data, buffer); + if ((remain <= string_length) || (string_length > 0x3FFF)) + return 0; + + unsigned char* strdata = (unsigned char*)data; + *strdata++ = (unsigned char)string_length; + memcpy(strdata, record.data.txt.key.str, record.data.txt.key.length); + strdata += record.data.txt.key.length; + *strdata++ = '='; + memcpy(strdata, record.data.txt.value.str, record.data.txt.value.length); + strdata += record.data.txt.value.length; + + data = strdata; + } + + // Fill record length + if (record_data) + mdns_htons(record_length, (uint16_t)MDNS_POINTER_DIFF(data, record_data)); + + return data; +} + +static inline uint16_t +mdns_answer_get_record_count(const mdns_record_t* records, size_t record_count) { + // TXT records will be coalesced into one record + uint16_t total_count = 0; + uint16_t txt_record = 0; + for (size_t irec = 0; irec < record_count; ++irec) { + if (records[irec].type == MDNS_RECORDTYPE_TXT) + txt_record = 1; + else + ++total_count; + } + return total_count + txt_record; +} + +static inline int +mdns_query_answer_unicast(int sock, const void* address, size_t address_size, void* buffer, + size_t capacity, uint16_t query_id, mdns_record_type_t record_type, + const char* name, size_t name_length, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + if (capacity < (sizeof(struct mdns_header_t) + 32 + 4)) + return -1; + + // According to RFC 6762: + // The cache-flush bit MUST NOT be set in any resource records in a response message + // sent in legacy unicast responses to UDP ports other than 5353. + uint16_t rclass = MDNS_CLASS_IN; + uint32_t ttl = 10; // Basic answer structure struct mdns_header_t* header = (struct mdns_header_t*)buffer; - header->query_id = (address_size ? htons(query_id) : 0); + header->query_id = htons(query_id); header->flags = htons(0x8400); - header->questions = htons(unicast ? 1 : 0); + header->questions = htons(1); header->answer_rrs = htons(1); - header->authority_rrs = 0; - header->additional_rrs = htons((unsigned short)(1 + use_ipv4 + use_ipv6 + use_txt)); + header->authority_rrs = htons(mdns_answer_get_record_count(authority, authority_count)); + header->additional_rrs = htons(mdns_answer_get_record_count(additional, additional_count)); + mdns_string_table_t string_table = {{0}, 0, 0}; void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); - uint16_t* udata; - size_t remain, service_offset = 0, local_offset = 0, full_offset, host_offset; - - // Fill in question if unicast - if (unicast) { - service_offset = MDNS_POINTER_DIFF(data, buffer); - remain = capacity - service_offset; - data = mdns_string_make(data, remain, service, service_length); - local_offset = MDNS_POINTER_DIFF(data, buffer) - 7; - remain = capacity - MDNS_POINTER_DIFF(data, buffer); - if (!data || (remain <= 4)) - return -1; - udata = (uint16_t*)data; - *udata++ = htons(MDNS_RECORDTYPE_PTR); - *udata++ = htons(question_rclass); - data = udata; + // Fill in question + data = mdns_answer_add_question_unicast(buffer, capacity, data, record_type, name, name_length, + &string_table); + + // Fill in answer + answer.rclass = rclass; + answer.ttl = ttl; + data = mdns_answer_add_record(buffer, capacity, data, answer, &string_table); + + // Fill in authority records + for (size_t irec = 0; data && (irec < authority_count); ++irec) { + mdns_record_t record = authority[irec]; + record.rclass = rclass; + if (!record.ttl) + record.ttl = ttl; + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); } - remain = capacity - MDNS_POINTER_DIFF(data, buffer); + data = mdns_answer_add_txt_record(buffer, capacity, data, authority, authority_count, + rclass, ttl, &string_table); - // Fill in answers - // PTR record for service - if (unicast) { - data = mdns_string_make_ref(data, remain, service_offset); - } else { - service_offset = MDNS_POINTER_DIFF(data, buffer); - remain = capacity - service_offset; - data = mdns_string_make(data, remain, service, service_length); - local_offset = MDNS_POINTER_DIFF(data, buffer) - 7; + // Fill in additional records + for (size_t irec = 0; data && (irec < additional_count); ++irec) { + mdns_record_t record = additional[irec]; + record.rclass = rclass; + if (!record.ttl) + record.ttl = ttl; + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); } - remain = capacity - MDNS_POINTER_DIFF(data, buffer); - if (!data || (remain <= 10)) - return -1; - udata = (uint16_t*)data; - *udata++ = htons(MDNS_RECORDTYPE_PTR); - *udata++ = htons(rclass); - *(uint32_t*)udata = htonl(ttl); - udata += 2; - uint16_t* record_length = udata++; // length - data = udata; - // Make a string ..local. - full_offset = MDNS_POINTER_DIFF(data, buffer); - remain = capacity - full_offset; - data = mdns_string_make_with_ref(data, remain, hostname, hostname_length, service_offset); - remain = capacity - MDNS_POINTER_DIFF(data, buffer); - if (!data || (remain <= 10)) + data = mdns_answer_add_txt_record(buffer, capacity, data, additional, additional_count, + rclass, ttl, &string_table); + if (!data) return -1; - *record_length = htons((uint16_t)MDNS_POINTER_DIFF(data, record_length + 1)); - // Fill in additional records - // SRV record for ..local. - data = mdns_string_make_ref(data, remain, full_offset); - remain = capacity - MDNS_POINTER_DIFF(data, buffer); - if (!data || (remain <= 10)) - return -1; - udata = (uint16_t*)data; - *udata++ = htons(MDNS_RECORDTYPE_SRV); - *udata++ = htons(rclass); - *(uint32_t*)udata = htonl(ttl); - udata += 2; - record_length = udata++; // length - *udata++ = htons(0); // priority - *udata++ = htons(0); // weight - *udata++ = htons(port); // port - // Make a string .local. - data = udata; - host_offset = MDNS_POINTER_DIFF(data, buffer); - remain = capacity - host_offset; - data = mdns_string_make_with_ref(data, remain, hostname, hostname_length, local_offset); - remain = capacity - MDNS_POINTER_DIFF(data, buffer); - if (!data || (remain <= 10)) + size_t tosend = MDNS_POINTER_DIFF(data, buffer); + return mdns_unicast_send(sock, address, address_size, buffer, tosend); +} + +static inline int +mdns_answer_multicast_rclass_ttl(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count, + uint16_t rclass, uint32_t ttl) { + if (capacity < (sizeof(struct mdns_header_t) + 32 + 4)) return -1; - *record_length = htons((uint16_t)MDNS_POINTER_DIFF(data, record_length + 1)); - // A record for .local. - if (use_ipv4) { - data = mdns_string_make_ref(data, remain, host_offset); - remain = capacity - MDNS_POINTER_DIFF(data, buffer); - if (!data || (remain <= 14)) - return -1; - udata = (uint16_t*)data; - *udata++ = htons(MDNS_RECORDTYPE_A); - *udata++ = htons(rclass); - *(uint32_t*)udata = htonl(a_ttl); - udata += 2; - *udata++ = htons(4); // length - *(uint32_t*)udata = ipv4; // ipv4 address - udata += 2; - data = udata; - remain = capacity - MDNS_POINTER_DIFF(data, buffer); - } + // Basic answer structure + struct mdns_header_t* header = (struct mdns_header_t*)buffer; + header->query_id = 0; + header->flags = htons(0x8400); + header->questions = 0; + header->answer_rrs = htons(1); + header->authority_rrs = htons(mdns_answer_get_record_count(authority, authority_count)); + header->additional_rrs = htons(mdns_answer_get_record_count(additional, additional_count)); - // AAAA record for .local. - if (use_ipv6) { - data = mdns_string_make_ref(data, remain, host_offset); - remain = capacity - MDNS_POINTER_DIFF(data, buffer); - if (!data || (remain <= 26)) - return -1; - udata = (uint16_t*)data; - *udata++ = htons(MDNS_RECORDTYPE_AAAA); - *udata++ = htons(rclass); - *(uint32_t*)udata = htonl(a_ttl); - udata += 2; - *udata++ = htons(16); // length - memcpy(udata, ipv6, 16); // ipv6 address - data = MDNS_POINTER_OFFSET(udata, 16); - remain = capacity - MDNS_POINTER_DIFF(data, buffer); + mdns_string_table_t string_table = {{0}, 0, 0}; + void* data = MDNS_POINTER_OFFSET(buffer, sizeof(struct mdns_header_t)); + + // Fill in answer + mdns_record_t record = answer; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); + + // Fill in authority records + for (size_t irec = 0; data && (irec < authority_count); ++irec) { + record = authority[irec]; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); } + data = mdns_answer_add_txt_record(buffer, capacity, data, authority, authority_count, + rclass, ttl, &string_table); - // TXT record for ..local. - if (use_txt) { - data = mdns_string_make_ref(data, remain, full_offset); - remain = capacity - MDNS_POINTER_DIFF(data, buffer); - if (!data || (remain <= (11 + txt_length))) - return -1; - udata = (uint16_t*)data; - *udata++ = htons(MDNS_RECORDTYPE_TXT); - *udata++ = htons(rclass); - *(uint32_t*)udata = htonl(ttl); - udata += 2; - *udata++ = htons((unsigned short)(txt_length + 1)); // length - char* txt_record = (char*)udata; - *txt_record++ = (char)txt_length; - memcpy(txt_record, txt, txt_length); // txt record - data = MDNS_POINTER_OFFSET(txt_record, txt_length); - // Unused until multiple txt records are supported - // remain = capacity - MDNS_POINTER_DIFF(data, buffer); + // Fill in additional records + for (size_t irec = 0; data && (irec < additional_count); ++irec) { + record = additional[irec]; + mdns_record_update_rclass_ttl(&record, rclass, ttl); + data = mdns_answer_add_record(buffer, capacity, data, record, &string_table); } + data = mdns_answer_add_txt_record(buffer, capacity, data, additional, additional_count, + rclass, ttl, &string_table); + if (!data) + return -1; size_t tosend = MDNS_POINTER_DIFF(data, buffer); - if (address_size) - return mdns_unicast_send(sock, address, address_size, buffer, tosend); return mdns_multicast_send(sock, buffer, tosend); } -static mdns_string_t +static inline int +mdns_query_answer_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, answer, authority, + authority_count, additional, additional_count, + MDNS_CLASS_IN, 60); +} + +static inline int +mdns_announce_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, answer, authority, + authority_count, additional, additional_count, + MDNS_CLASS_IN | MDNS_CACHE_FLUSH, 60); +} + +static inline int +mdns_goodbye_multicast(int sock, void* buffer, size_t capacity, mdns_record_t answer, + const mdns_record_t* authority, size_t authority_count, + const mdns_record_t* additional, size_t additional_count) { + // Goodbye should have ttl of 0 + return mdns_answer_multicast_rclass_ttl(sock, buffer, capacity, answer, authority, + authority_count, additional, additional_count, + MDNS_CLASS_IN, 0); +} + +static inline mdns_string_t mdns_record_parse_ptr(const void* buffer, size_t size, size_t offset, size_t length, char* strbuffer, size_t capacity) { // PTR record is just a string @@ -1163,29 +1507,29 @@ mdns_record_parse_ptr(const void* buffer, size_t size, size_t offset, size_t len return empty; } -static mdns_record_srv_t +static inline mdns_record_srv_t mdns_record_parse_srv(const void* buffer, size_t size, size_t offset, size_t length, char* strbuffer, size_t capacity) { mdns_record_srv_t srv; memset(&srv, 0, sizeof(mdns_record_srv_t)); - // Read the priority, weight, port number and the discovery name + // Read the service priority, weight, port number and the discovery name // SRV record format (http://www.ietf.org/rfc/rfc2782.txt): // 2 bytes network-order unsigned priority // 2 bytes network-order unsigned weight // 2 bytes network-order unsigned port // string: discovery (domain) name, minimum 2 bytes when compressed if ((size >= offset + length) && (length >= 8)) { - const uint16_t* recorddata = (const uint16_t*)((const char*)buffer + offset); - srv.priority = ntohs(*recorddata++); - srv.weight = ntohs(*recorddata++); - srv.port = ntohs(*recorddata++); + const uint16_t* recorddata = (const uint16_t*)MDNS_POINTER_OFFSET_CONST(buffer, offset); + srv.priority = mdns_ntohs(recorddata++); + srv.weight = mdns_ntohs(recorddata++); + srv.port = mdns_ntohs(recorddata++); offset += 6; srv.name = mdns_string_extract(buffer, size, &offset, strbuffer, capacity); } return srv; } -static struct sockaddr_in* +static inline struct sockaddr_in* mdns_record_parse_a(const void* buffer, size_t size, size_t offset, size_t length, struct sockaddr_in* addr) { memset(addr, 0, sizeof(struct sockaddr_in)); @@ -1194,11 +1538,11 @@ mdns_record_parse_a(const void* buffer, size_t size, size_t offset, size_t lengt addr->sin_len = sizeof(struct sockaddr_in); #endif if ((size >= offset + length) && (length == 4)) - addr->sin_addr.s_addr = *(const uint32_t*)((const char*)buffer + offset); + memcpy(&addr->sin_addr.s_addr, MDNS_POINTER_OFFSET(buffer, offset), 4); return addr; } -static struct sockaddr_in6* +static inline struct sockaddr_in6* mdns_record_parse_aaaa(const void* buffer, size_t size, size_t offset, size_t length, struct sockaddr_in6* addr) { memset(addr, 0, sizeof(struct sockaddr_in6)); @@ -1207,33 +1551,37 @@ mdns_record_parse_aaaa(const void* buffer, size_t size, size_t offset, size_t le addr->sin6_len = sizeof(struct sockaddr_in6); #endif if ((size >= offset + length) && (length == 16)) - addr->sin6_addr = *(const struct in6_addr*)((const char*)buffer + offset); + memcpy(&addr->sin6_addr, MDNS_POINTER_OFFSET(buffer, offset), 16); return addr; } -static size_t +static inline size_t mdns_record_parse_txt(const void* buffer, size_t size, size_t offset, size_t length, mdns_record_txt_t* records, size_t capacity) { size_t parsed = 0; const char* strdata; - size_t separator, sublength; size_t end = offset + length; if (size < end) end = size; while ((offset < end) && (parsed < capacity)) { - strdata = (const char*)buffer + offset; - sublength = *(const unsigned char*)strdata; + strdata = (const char*)MDNS_POINTER_OFFSET(buffer, offset); + size_t sublength = *(const unsigned char*)strdata; + + if (sublength >= (end - offset)) + break; ++strdata; offset += sublength + 1; - separator = 0; + size_t separator = sublength; for (size_t c = 0; c < sublength; ++c) { // DNS-SD TXT record keys MUST be printable US-ASCII, [0x20, 0x7E] - if ((strdata[c] < 0x20) || (strdata[c] > 0x7E)) + if ((strdata[c] < 0x20) || (strdata[c] > 0x7E)) { + separator = 0; break; + } if (strdata[c] == '=') { separator = c; break; @@ -1251,6 +1599,8 @@ mdns_record_parse_txt(const void* buffer, size_t size, size_t offset, size_t len } else { records[parsed].key.str = strdata; records[parsed].key.length = sublength; + records[parsed].value.str = 0; + records[parsed].value.length = 0; } ++parsed; From bc083dc61b8f6fdac20c8b6a23fb2057a06becdf Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sun, 24 Aug 2025 09:10:22 -0500 Subject: [PATCH 410/466] QtFRED Mission Specs Dialog (#6945) * refactor mission specs dialog * clang * Stub in all the sub dialog files * custom strings dialog * custom strings dialog cleanup * custom data dialog * sound environment dialog * move custom wing names into mission specs subfolder * custom wings dialog * unused * clang tidy * clean return * refactor mission specs dialog * move custom wing names into mission specs subfolder * custom wings dialog * rebase error * remove a couple empty functions * select after creating --- code/mission/missionparse.cpp | 71 ++++ code/mission/missionparse.h | 13 + code/sound/sound.h | 5 + qtfred/resources/images/next.bmp | Bin 0 -> 208 bytes qtfred/resources/images/play.bmp | Bin 0 -> 162 bytes qtfred/resources/images/prev.bmp | Bin 0 -> 208 bytes qtfred/resources/images/stop.bmp | Bin 0 -> 164 bytes qtfred/resources/resources.qrc | 132 +++--- qtfred/source_groups.cmake | 27 +- .../dialogs/CustomWingNamesDialogModel.h | 35 -- .../dialogs/MissionSpecDialogModel.cpp | 161 +++++++- .../mission/dialogs/MissionSpecDialogModel.h | 45 +- .../MissionSpecs/CustomDataDialogModel.cpp | 169 ++++++++ .../MissionSpecs/CustomDataDialogModel.h | 40 ++ .../MissionSpecs/CustomStringsDialogModel.cpp | 152 +++++++ .../MissionSpecs/CustomStringsDialogModel.h | 42 ++ .../CustomWingNamesDialogModel.cpp | 98 ++--- .../MissionSpecs/CustomWingNamesDialogModel.h | 35 ++ .../SoundEnvironmentDialogModel.cpp | 128 ++++++ .../SoundEnvironmentDialogModel.h | 34 ++ .../src/ui/dialogs/CustomWingNamesDialog.cpp | 86 ---- qtfred/src/ui/dialogs/CustomWingNamesDialog.h | 48 --- .../src/ui/dialogs/JumpNodeEditorDialog.cpp | 2 - qtfred/src/ui/dialogs/MissionSpecDialog.cpp | 383 ++++++++++-------- qtfred/src/ui/dialogs/MissionSpecDialog.h | 69 +++- .../dialogs/MissionSpecs/CustomDataDialog.cpp | 243 +++++++++++ .../dialogs/MissionSpecs/CustomDataDialog.h | 60 +++ .../MissionSpecs/CustomStringsDialog.cpp | 241 +++++++++++ .../MissionSpecs/CustomStringsDialog.h | 58 +++ .../MissionSpecs/CustomWingNamesDialog.cpp | 180 ++++++++ .../MissionSpecs/CustomWingNamesDialog.h | 62 +++ .../MissionSpecs/SoundEnvironmentDialog.cpp | 225 ++++++++++ .../MissionSpecs/SoundEnvironmentDialog.h | 60 +++ qtfred/ui/CustomDataDialog.ui | 159 ++++++++ qtfred/ui/CustomStringsDialog.ui | 208 ++++++++++ qtfred/ui/CustomWingNamesDialog.ui | 28 +- qtfred/ui/MissionSpecDialog.ui | 291 ++++--------- qtfred/ui/SoundEnvironmentDialog.ui | 189 +++++++++ 38 files changed, 3069 insertions(+), 710 deletions(-) create mode 100644 qtfred/resources/images/next.bmp create mode 100644 qtfred/resources/images/play.bmp create mode 100644 qtfred/resources/images/prev.bmp create mode 100644 qtfred/resources/images/stop.bmp delete mode 100644 qtfred/src/mission/dialogs/CustomWingNamesDialogModel.h create mode 100644 qtfred/src/mission/dialogs/MissionSpecs/CustomDataDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/MissionSpecs/CustomDataDialogModel.h create mode 100644 qtfred/src/mission/dialogs/MissionSpecs/CustomStringsDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/MissionSpecs/CustomStringsDialogModel.h rename qtfred/src/mission/dialogs/{ => MissionSpecs}/CustomWingNamesDialogModel.cpp (55%) create mode 100644 qtfred/src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.h create mode 100644 qtfred/src/mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.h delete mode 100644 qtfred/src/ui/dialogs/CustomWingNamesDialog.cpp delete mode 100644 qtfred/src/ui/dialogs/CustomWingNamesDialog.h create mode 100644 qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.cpp create mode 100644 qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.h create mode 100644 qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.cpp create mode 100644 qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.h create mode 100644 qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.cpp create mode 100644 qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.h create mode 100644 qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.cpp create mode 100644 qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.h create mode 100644 qtfred/ui/CustomDataDialog.ui create mode 100644 qtfred/ui/CustomStringsDialog.ui create mode 100644 qtfred/ui/SoundEnvironmentDialog.ui diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index b81c1d626d0..101eab7aec1 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -267,6 +267,77 @@ const char *Old_game_types[OLD_MAX_GAME_TYPES] = { "Training mission" }; +// These are a little different than the object flags as they aren't used in traditional flag sexps or parsed flag lists +// Instead, this list is used to popuplate QtFRED's mission specs flag checkboxes. As such the names can be more descriptive than other flag def lists +// NOTE: Inactive flags and special flags are not added to the UI flag list. It is assumed that special flags exist in some other UI form +flag_def_list_new Parse_mission_flags[] = { + {"Mission Takes Place In Subspace", Mission::Mission_Flags::Subspace, true, true}, + {"Disallow Promotions/Badges", Mission::Mission_Flags::No_promotion, true, false}, + {"Mission Takes Place In Full Nebula", Mission::Mission_Flags::Fullneb, true, true}, + {"Disable Built-in Messages", Mission::Mission_Flags::No_builtin_msgs, true, false}, + {"No Traitor", Mission::Mission_Flags::No_traitor, true, false}, + {"Toggle Ship Trails", Mission::Mission_Flags::Toggle_ship_trails, true, true}, + {"Support Ship Repairs Hull", Mission::Mission_Flags::Support_repairs_hull, true, true}, + {"All Ships Beam-Freed By Default", Mission::Mission_Flags::Beam_free_all_by_default, true, false}, + {"UNUSED 1", Mission::Mission_Flags::Unused_1, false, false}, + {"UNUSED 2", Mission::Mission_Flags::Unused_2, false, false}, + {"No Briefing", Mission::Mission_Flags::No_briefing, true, false}, + {"Toggle Debriefing (On/Off)", Mission::Mission_Flags::Toggle_debriefing, true, false}, + {"UNUSED 3", Mission::Mission_Flags::Unused_3, false, false}, + {"UNUSED 4", Mission::Mission_Flags::Unused_4, false, false}, + {"2D Mission", Mission::Mission_Flags::Mission_2d, true, false}, + {"UNUSED 5", Mission::Mission_Flags::Unused_5, false, false}, + {"Red Alert Mission", Mission::Mission_Flags::Red_alert, true, false}, + {"Scramble Mission", Mission::Mission_Flags::Scramble, true, false}, + {"Disable Built-in Command Messages", Mission::Mission_Flags::No_builtin_command, true, false}, + {"Player Starts under AI Control (NO MULTI)", Mission::Mission_Flags::Player_start_ai, true, false}, + {"All Teams at War", Mission::Mission_Flags::All_attack, true, false}, + {"Use Autopilot Cinematics", Mission::Mission_Flags::Use_ap_cinematics, true, false}, + {"Deactivate Hardcoded Autopilot", Mission::Mission_Flags::Deactivate_ap, true, false}, + {"Toggle Showing Goals In Briefing", Mission::Mission_Flags::Toggle_showing_goals, true, false}, + {"Mission End to Mainhall", Mission::Mission_Flags::End_to_mainhall, true, false}, + {"Override #Command with Command Info", Mission::Mission_Flags::Override_hashcommand, true, true}, + {"Toggle Starting in Chase View", Mission::Mission_Flags::Toggle_start_chase_view, true, false}, + {"Nebula Fog Color Override", Mission::Mission_Flags::Neb2_fog_color_override, true, true}, + {"Full Nebula Background Bitmaps", Mission::Mission_Flags::Fullneb_background_bitmaps, true, true}, + {"Preload Subspace Tunnel", Mission::Mission_Flags::Preload_subspace, true, false} +}; + +parse_object_flag_description Parse_mission_flag_descriptions[] = { + {Mission::Mission_Flags::Subspace, "Mission takes place in subspace"}, + {Mission::Mission_Flags::No_promotion, "Cannot get promoted or badges in this mission"}, + {Mission::Mission_Flags::Fullneb, "Mission is a full nebula mission"}, + {Mission::Mission_Flags::No_builtin_msgs, "Disables all builtin messages except Command"}, + {Mission::Mission_Flags::No_traitor, "Player cannot become a traitor"}, + {Mission::Mission_Flags::Toggle_ship_trails, "Toggles ship trails (off in nebula, on outside nebula)"}, + {Mission::Mission_Flags::Support_repairs_hull, "Toggles support ship repair of ship hulls"}, + {Mission::Mission_Flags::Beam_free_all_by_default, "All ships are beam-freed by default"}, + {Mission::Mission_Flags::Unused_1, "UNUSED 1"}, // Necessary to not break parsing. Is this still true?? + {Mission::Mission_Flags::Unused_2, "UNUSED 2"}, // Necessary to not break parsing. Is this still true?? + {Mission::Mission_Flags::No_briefing, "No briefing, mission starts immediately"}, + {Mission::Mission_Flags::Toggle_debriefing, "Toggles debriefing on for dogfight. Off for everything else"}, + {Mission::Mission_Flags::Unused_3, "UNUSED 3"}, // Necessary to not break parsing. Is this still true?? + {Mission::Mission_Flags::Unused_4, "UNUSED 4"}, // Necessary to not break parsing. Is this still true?? + {Mission::Mission_Flags::Mission_2d, "Mission is meant to be played top-down style; 2D physics and movement."}, + {Mission::Mission_Flags::Unused_5, "UNUSED 5"}, // Necessary to not break parsing. Is this still true?? + {Mission::Mission_Flags::Red_alert, "A red-alert mission"}, + {Mission::Mission_Flags::Scramble, "A scramble mission"}, + {Mission::Mission_Flags::No_builtin_command, "Disables builtin Command messages"}, + {Mission::Mission_Flags::Player_start_ai, "Player starts mission under AI Control"}, + {Mission::Mission_Flags::All_attack, "All teams target each other"}, + {Mission::Mission_Flags::Use_ap_cinematics, "Use autopilot cinematics"}, + {Mission::Mission_Flags::Deactivate_ap, "Deactivate hardcoded autopilot"}, + {Mission::Mission_Flags::Toggle_showing_goals, "Show mission goals for training missions, hide otherwise"}, + {Mission::Mission_Flags::End_to_mainhall, "Return to the mainhall after debrief instead of starting the next mission"}, + {Mission::Mission_Flags::Override_hashcommand, "Override #Command with the Command info in Mission Specs"}, + {Mission::Mission_Flags::Toggle_start_chase_view, "Toggles whether the player starts the mission in chase view"}, + {Mission::Mission_Flags::Neb2_fog_color_override, "Whether to use explicit fog colors instead of checking the palette"}, + {Mission::Mission_Flags::Fullneb_background_bitmaps, "Show background bitmaps despite full nebula"}, + {Mission::Mission_Flags::Preload_subspace, "Preload the subspace tunnel for both the sexp and specs checkbox"}, +}; + +const size_t Num_parse_mission_flags = sizeof(Parse_mission_flags) / sizeof(flag_def_list_new); + flag_def_list_new Parse_object_flags[] = { { "cargo-known", Mission::Parse_Object_Flags::SF_Cargo_known, true, false }, { "ignore-count", Mission::Parse_Object_Flags::SF_Ignore_count, true, false }, diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index c9e4fa99eed..d00deb761a5 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -156,6 +156,16 @@ typedef struct custom_string { SCP_string text; } custom_string; +inline bool operator==(const custom_string& a, const custom_string& b) +{ + return a.name == b.name && a.value == b.value && a.text == b.text; +} + +inline bool operator!=(const custom_string& a, const custom_string& b) +{ + return !(a == b); +} + // descriptions of flags for FRED template struct parse_object_flag_description { @@ -290,6 +300,9 @@ extern const char *Departure_location_names[MAX_DEPARTURE_NAMES]; extern const char *Goal_type_names[MAX_GOAL_TYPE_NAMES]; extern const char *Reinforcement_type_names[]; +extern flag_def_list_new Parse_mission_flags[]; +extern parse_object_flag_description Parse_mission_flag_descriptions[]; +extern const size_t Num_parse_mission_flags; extern char *Object_flags[]; extern flag_def_list_new Parse_object_flags[]; extern parse_object_flag_description Parse_object_flag_descriptions[]; diff --git a/code/sound/sound.h b/code/sound/sound.h index 7765c169792..18712092bd7 100644 --- a/code/sound/sound.h +++ b/code/sound/sound.h @@ -112,6 +112,11 @@ typedef struct sound_env float decay; } sound_env; +inline bool operator==(const sound_env& a, const sound_env& b) { + return a.id == b.id && a.volume == b.volume && a.damping == b.damping && a.decay == b.decay; +} +inline bool operator!=(const sound_env& a, const sound_env& b) { return !(a == b); } + extern int Sound_enabled; extern float Default_sound_volume; // 0 -> 1.0 extern float Default_voice_volume; // 0 -> 1.0 diff --git a/qtfred/resources/images/next.bmp b/qtfred/resources/images/next.bmp new file mode 100644 index 0000000000000000000000000000000000000000..cfc4592eb5f56bf65c63dcec48ee6eba0b01b41f GIT binary patch literal 208 zcmY+5yA6Oa3`7qJ$r5x-!5s7~z!a`Xo6#}?8-?d26r6K@{=~=msOtthUhs-1&1C7q z8-ws)S>h?@4l<=mDKHkbJ=Gc&CSpWNA!ZJY&MoN4siCJQ3H literal 0 HcmV?d00001 diff --git a/qtfred/resources/images/prev.bmp b/qtfred/resources/images/prev.bmp new file mode 100644 index 0000000000000000000000000000000000000000..877144d0db48e411932322b2197d3eccc0fe76c9 GIT binary patch literal 208 zcmY+6$qj%o3`ag*W)9}H1Dcf!H7HD<3=r6 zTJXRi{Fh0D%qqx~Dy6_!_!V-G3Ugva+Ct1SNOUkKI;hHgQ*VEK_s@Oa$1eyJQ3rkyR#U6_RWQ g!M7?n)H+OcrM0HVp}#}#v56g#QcB4geu+PL00j*q4*&oF literal 0 HcmV?d00001 diff --git a/qtfred/resources/resources.qrc b/qtfred/resources/resources.qrc index 0c5036915d9..c7a35e37666 100644 --- a/qtfred/resources/resources.qrc +++ b/qtfred/resources/resources.qrc @@ -1,66 +1,70 @@ - - images/bitmap1.png - images/black_do.png - images/bmp00001.png - images/chained.png - images/chained_directive.png - images/constx.png - images/constxy.png - images/constxz.png - images/consty.png - images/constyz.png - images/constz.png - images/container_data.png - images/container_name.png - images/cursor_rotate.png - images/data.png - images/data00.png - images/data05.png - images/data10.png - images/data15.png - images/data20.png - images/data25.png - images/data30.png - images/data35.png - images/data40.png - images/data45.png - images/data50.png - images/data55.png - images/data60.png - images/data65.png - images/data70.png - images/data75.png - images/data80.png - images/data85.png - images/data90.png - images/data95.png - images/fred.ico - images/fred_app.png - images/fred_debug.png - images/freddoc.ico - images/fredknows.png - images/fred_splash.png - images/green_do.png - images/orbitsel.png - images/play.png - images/root.png - images/root_directive.png - images/rotlocal.png - images/select.png - images/selectlist.png - images/selectlock.png - images/selectmove.png - images/selectrot.png - images/showdist.png - images/splash.png - images/toolbar.png - images/toolbar1.png - images/V_fred.ico - images/variable.png - images/wingdisband.png - images/wingform.png - images/zoomext.png - images/zoomsel.png - + + images/next.bmp + images/play.bmp + images/prev.bmp + images/stop.bmp + images/bitmap1.png + images/black_do.png + images/bmp00001.png + images/chained.png + images/chained_directive.png + images/constx.png + images/constxy.png + images/constxz.png + images/consty.png + images/constyz.png + images/constz.png + images/container_data.png + images/container_name.png + images/cursor_rotate.png + images/data.png + images/data00.png + images/data05.png + images/data10.png + images/data15.png + images/data20.png + images/data25.png + images/data30.png + images/data35.png + images/data40.png + images/data45.png + images/data50.png + images/data55.png + images/data60.png + images/data65.png + images/data70.png + images/data75.png + images/data80.png + images/data85.png + images/data90.png + images/data95.png + images/fred.ico + images/fred_app.png + images/fred_debug.png + images/freddoc.ico + images/fredknows.png + images/fred_splash.png + images/green_do.png + images/orbitsel.png + images/play.png + images/root.png + images/root_directive.png + images/rotlocal.png + images/select.png + images/selectlist.png + images/selectlock.png + images/selectmove.png + images/selectrot.png + images/showdist.png + images/splash.png + images/toolbar.png + images/toolbar1.png + images/V_fred.ico + images/variable.png + images/wingdisband.png + images/wingform.png + images/zoomext.png + images/zoomsel.png + diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 1c723ad5fb4..1851f629110 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -46,8 +46,6 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/CampaignEditorDialogModel.h src/mission/dialogs/CommandBriefingDialogModel.cpp src/mission/dialogs/CommandBriefingDialogModel.h - src/mission/dialogs/CustomWingNamesDialogModel.cpp - src/mission/dialogs/CustomWingNamesDialogModel.h src/mission/dialogs/FictionViewerDialogModel.cpp src/mission/dialogs/FictionViewerDialogModel.h src/mission/dialogs/FormWingDialogModel.cpp @@ -77,6 +75,16 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/WingEditorDialogModel.cpp src/mission/dialogs/WingEditorDialogModel.h ) +add_file_folder("Source/Mission/Dialogs/MissionSpecs" + src/mission/dialogs/MissionSpecs/CustomDataDialogModel.cpp + src/mission/dialogs/MissionSpecs/CustomDataDialogModel.h + src/mission/dialogs/MissionSpecs/CustomStringsDialogModel.cpp + src/mission/dialogs/MissionSpecs/CustomStringsDialogModel.h + src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.cpp + src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.h + src/mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.cpp + src/mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.h +) add_file_folder("Source/Mission/Dialogs/ShipEditor" src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp @@ -126,8 +134,6 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/CampaignEditorDialog.cpp src/ui/dialogs/CommandBriefingDialog.cpp src/ui/dialogs/CommandBriefingDialog.h - src/ui/dialogs/CustomWingNamesDialog.cpp - src/ui/dialogs/CustomWingNamesDialog.h src/ui/dialogs/EventEditorDialog.cpp src/ui/dialogs/EventEditorDialog.h src/ui/dialogs/FictionViewerDialog.cpp @@ -161,6 +167,16 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/WingEditorDialog.cpp src/ui/dialogs/WingEditorDialog.h ) +add_file_folder("Source/UI/Dialogs/MissionSpecs" + src/ui/dialogs/MissionSpecs/CustomDataDialog.cpp + src/ui/dialogs/MissionSpecs/CustomDataDialog.h + src/ui/dialogs/MissionSpecs/CustomStringsDialog.cpp + src/ui/dialogs/MissionSpecs/CustomStringsDialog.h + src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.cpp + src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.h + src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.cpp + src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.h +) add_file_folder("Source/UI/Dialogs/ShipEditor" src/ui/dialogs/ShipEditor/ShipEditorDialog.h src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -232,6 +248,8 @@ add_file_folder("UI" ui/CampaignEditorDialog.ui ui/CheckBoxListDialog.ui ui/CommandBriefingDialog.ui + ui/CustomDataDialog.ui + ui/CustomStringsDialog.ui ui/CustomWingNamesDialog.ui ui/EventEditorDialog.ui ui/FictionViewerDialog.ui @@ -246,6 +264,7 @@ add_file_folder("UI" ui/ReinforcementsDialog.ui ui/SelectionDialog.ui ui/ShieldSystemDialog.ui + ui/SoundEnvironmentDialog.ui ui/VoiceActingManager.ui ui/WaypointEditorDialog.ui ui/ShipEditorDialog.ui diff --git a/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.h b/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.h deleted file mode 100644 index 87b7d6340b8..00000000000 --- a/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "AbstractDialogModel.h" - -namespace fso { -namespace fred { -namespace dialogs { - -class CustomWingNamesDialogModel : public AbstractDialogModel { -public: - CustomWingNamesDialogModel(QObject* parent, EditorViewport* viewport); - - bool apply() override; - void reject() override; - - void setStartingWing(SCP_string, int); - void setSquadronWing(SCP_string, int); - void setTvTWing(SCP_string, int); - SCP_string getStartingWing(int); - SCP_string getSquadronWing(int); - SCP_string getTvTWing(int); - - bool query_modified(); -private: - void initializeData(); - - - SCP_string _m_starting[3]; - SCP_string _m_squadron[5]; - SCP_string _m_tvt[2]; -}; - -} -} -} diff --git a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp index abfef30eb54..f9c2cac3fcd 100644 --- a/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionSpecDialogModel.cpp @@ -13,12 +13,11 @@ #include "cfile/cfile.h" #include "localization/localize.h" #include "mission/missionmessage.h" +#include "mission/mission_flags.h" #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { MissionSpecDialogModel::MissionSpecDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { @@ -26,6 +25,8 @@ MissionSpecDialogModel::MissionSpecDialogModel(QObject* parent, EditorViewport* } void MissionSpecDialogModel::initializeData() { + prepareSquadLogoList(); + _m_mission_title = The_mission.name; _m_designer_name = The_mission.author; _m_created = The_mission.created; @@ -68,9 +69,37 @@ void MissionSpecDialogModel::initializeData() { _m_contrail_threshold = The_mission.contrail_threshold; _m_contrail_threshold_flag = (_m_contrail_threshold != CONTRAIL_THRESHOLD_DEFAULT); + _m_custom_data = The_mission.custom_data; + _m_custom_strings = The_mission.custom_strings; + _m_sound_env = The_mission.sound_environment; + + // init starting wings + for (int i = 0; i < MAX_STARTING_WINGS; i++) { + _m_custom_starting_wings[i] = Starting_wing_names[i]; + } + + // init squadron wings + for (int i = 0; i < MAX_SQUADRON_WINGS; i++) { + _m_custom_squadron_wings[i] = Squadron_wing_names[i]; + } + + // init tvt wings + for (int i = 0; i < MAX_TVT_WINGS; i++) { + _m_custom_tvt_wings[i] = TVT_wing_names[i]; + } + modelChanged(); } +void MissionSpecDialogModel::prepareSquadLogoList() +{ + pilot_load_squad_pic_list(); + + for (int i = 0; i < Num_pilot_squad_images; i++) { + _m_squadLogoList.emplace_back(Pilot_squad_image_names[i]); + } +} + bool MissionSpecDialogModel::apply() { int new_m_type; @@ -149,6 +178,28 @@ bool MissionSpecDialogModel::apply() { Num_teams = 2; } + The_mission.custom_data = _m_custom_data; + The_mission.custom_strings = _m_custom_strings; + + The_mission.sound_environment = _m_sound_env; + + // copy starting wings + for (int i = 0; i < MAX_STARTING_WINGS; i++) { + strcpy_s(Starting_wing_names[i], _m_custom_starting_wings[i].c_str()); + } + + // copy squadron wings + for (int i = 0; i < MAX_SQUADRON_WINGS; i++) { + strcpy_s(Squadron_wing_names[i], _m_custom_squadron_wings[i].c_str()); + } + + // copy tvt wings + for (int i = 0; i < MAX_TVT_WINGS; i++) { + strcpy_s(TVT_wing_names[i], _m_custom_tvt_wings[i].c_str()); + } + + Editor::update_custom_wing_indexes(); + return true; } @@ -322,7 +373,23 @@ SCP_string MissionSpecDialogModel::getSubEventMusic() { return _m_substitute_event_music; } -void MissionSpecDialogModel::setMissionFlag(Mission::Mission_Flags flag, bool enabled) { +void MissionSpecDialogModel::setMissionFlag(const SCP_string& flag_name, bool enabled) +{ + // Find the matching flagDef by name + for (size_t i = 0; i < Num_parse_mission_flags; ++i) { + if (!stricmp(flag_name.c_str(), Parse_mission_flags[i].name)) { + if (enabled) + _m_flags.set(Parse_mission_flags[i].def); + else + _m_flags.remove(Parse_mission_flags[i].def); + break; + } + } + + set_modified(); +} + +void MissionSpecDialogModel::setMissionFlagDirect(Mission::Mission_Flags flag, bool enabled) { if (_m_flags[flag] != enabled) { _m_flags.set(flag, enabled); set_modified(); @@ -330,8 +397,25 @@ void MissionSpecDialogModel::setMissionFlag(Mission::Mission_Flags flag, bool en } } -const flagset& MissionSpecDialogModel::getMissionFlags() const { - return _m_flags; +bool MissionSpecDialogModel::getMissionFlag(Mission::Mission_Flags flag) const { + return _m_flags[flag]; +} + +const SCP_vector>& MissionSpecDialogModel::getMissionFlagsList() { + if (_m_flag_data.empty()) { + for (size_t i = 0; i < Num_parse_mission_flags; ++i) { + auto flagDef = Parse_mission_flags[i]; + + // Skip flags that have checkboxes elsewhere than the flag list or are inactive + if (flagDef.is_special || !flagDef.in_use) { + continue; + } + + bool checked = _m_flags[flagDef.def]; + _m_flag_data.emplace_back(flagDef.name, checked); + } + } + return _m_flag_data; } void MissionSpecDialogModel::setMissionFullWar(bool enabled) { @@ -367,6 +451,71 @@ SCP_string MissionSpecDialogModel::getDesignerNoteText() { return _m_mission_notes; } +void MissionSpecDialogModel::setCustomData(const SCP_map& custom_data) +{ + modify(_m_custom_data, custom_data); + set_modified(); +} + +SCP_map MissionSpecDialogModel::getCustomData() const +{ + return _m_custom_data; +} + +void MissionSpecDialogModel::setCustomStrings(const SCP_vector& custom_strings) +{ + modify(_m_custom_strings, custom_strings); +} + +SCP_vector MissionSpecDialogModel::getCustomStrings() const +{ + return _m_custom_strings; +} + +void MissionSpecDialogModel::setSoundEnvironmentParams(const sound_env& snd_env) +{ + modify(_m_sound_env, snd_env); +} + +sound_env MissionSpecDialogModel::getSoundEnvironmentParams() const +{ + return _m_sound_env; } + +void MissionSpecDialogModel::setCustomStartingWings(const std::array& starting_wings) +{ + for (int i = 0; i < MAX_STARTING_WINGS; i++) { + modify(_m_custom_starting_wings[i], starting_wings[i]); + } +} + +std::array MissionSpecDialogModel::getCustomStartingWings() const +{ + return _m_custom_starting_wings; +} + +void MissionSpecDialogModel::setCustomSquadronWings(const std::array& squadron_wings) +{ + for (int i = 0; i < MAX_SQUADRON_WINGS; i++) { + modify(_m_custom_squadron_wings[i], squadron_wings[i]); + } } + +std::array MissionSpecDialogModel::getCustomSquadronWings() const +{ + return _m_custom_squadron_wings; +} + +void MissionSpecDialogModel::setCustomTvTWings(const std::array& tvt_wings) +{ + for (int i = 0; i < MAX_TVT_WINGS; i++) { + modify(_m_custom_tvt_wings[i], tvt_wings[i]); + } } + +std::array MissionSpecDialogModel::getCustomTvTWings() const +{ + return _m_custom_tvt_wings; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/MissionSpecDialogModel.h b/qtfred/src/mission/dialogs/MissionSpecDialogModel.h index ceaf9b05637..560eae69d42 100644 --- a/qtfred/src/mission/dialogs/MissionSpecDialogModel.h +++ b/qtfred/src/mission/dialogs/MissionSpecDialogModel.h @@ -6,15 +6,16 @@ #include "gamesnd/eventmusic.h" #include "mission/missionparse.h" #include "mission/missionmessage.h" +#include "playerman/managepilot.h" // for squad logos +#include "sound/sound.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { class MissionSpecDialogModel : public AbstractDialogModel { private: void initializeData(); + void prepareSquadLogoList(); SCP_string _m_created; @@ -40,8 +41,17 @@ class MissionSpecDialogModel : public AbstractDialogModel { float _m_max_subsys_repair_val; bool _m_contrail_threshold_flag; int _m_contrail_threshold; + SCP_map _m_custom_data; + SCP_vector _m_custom_strings; + sound_env _m_sound_env; + + std::array _m_custom_starting_wings; + std::array _m_custom_squadron_wings; + std::array _m_custom_tvt_wings; flagset _m_flags; + SCP_vector> _m_flag_data; + SCP_vector _m_squadLogoList; int _m_type; @@ -74,6 +84,7 @@ class MissionSpecDialogModel : public AbstractDialogModel { SCP_string getSquadronName(); void setSquadronLogo(const SCP_string&); SCP_string getSquadronLogo(); + std::vector getSquadLogoList() const { return _m_squadLogoList; }; void setLowResLoadingScreen(const SCP_string&); SCP_string getLowResLoadingScren(); @@ -102,8 +113,10 @@ class MissionSpecDialogModel : public AbstractDialogModel { void setSubEventMusic(const SCP_string&); SCP_string getSubEventMusic(); - void setMissionFlag(Mission::Mission_Flags flag, bool enabled); - const flagset& getMissionFlags() const; + void setMissionFlag(const SCP_string& flag_name, bool enabled); + void setMissionFlagDirect(Mission::Mission_Flags flag, bool enabled); + bool getMissionFlag(Mission::Mission_Flags flag) const; + const SCP_vector>& getMissionFlagsList(); void setMissionFullWar(bool enabled); @@ -116,8 +129,24 @@ class MissionSpecDialogModel : public AbstractDialogModel { void setDesignerNoteText(const SCP_string&); SCP_string getDesignerNoteText(); + void setCustomData(const SCP_map& custom_data); + SCP_map getCustomData() const; + + void setCustomStrings(const SCP_vector& custom_strings); + SCP_vector getCustomStrings() const; + + void setSoundEnvironmentParams(const sound_env& env); + sound_env getSoundEnvironmentParams() const; + + void setCustomStartingWings(const std::array& starting_wings); + std::array getCustomStartingWings() const; + + void setCustomSquadronWings(const std::array& squadron_wings); + std::array getCustomSquadronWings() const; + + void setCustomTvTWings(const std::array& tvt_wings); + std::array getCustomTvTWings() const; + }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/MissionSpecs/CustomDataDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecs/CustomDataDialogModel.cpp new file mode 100644 index 00000000000..dd02b77581f --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionSpecs/CustomDataDialogModel.cpp @@ -0,0 +1,169 @@ +#include "CustomDataDialogModel.h" + +using namespace fso::fred::dialogs; + +CustomDataDialogModel::CustomDataDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ +} + +bool CustomDataDialogModel::apply() +{ + // No direct application; this model is used to collect custom strings + // and the actual application is handled by the MissionSpecDialogModel. + return true; +} + +void CustomDataDialogModel::reject() +{ + // No direct rejection; this model is used to collect custom strings + // and the actual rejection is handled by the MissionSpecDialogModel. +} + +void CustomDataDialogModel::setInitial(const SCP_map& in) +{ + _items = in; +} + +bool CustomDataDialogModel::add(const std::pair& e, SCP_string* errorOut) +{ + // validation + if (!validateKeySyntax(e.first, errorOut)) + return false; + if (!validateValue(e.second, errorOut)) + return false; + + if (!keyIsUnique(e.first)) { + if (errorOut) + *errorOut = "Key must be unique."; + return false; + } + + _items.emplace(e); + set_modified(); + return true; +} + +static inline bool +advance_to_index(SCP_map& m, size_t index, SCP_map::iterator& out) +{ + if (index >= m.size()) + return false; + out = m.begin(); + std::advance(out, static_cast(index)); + return true; +} + +bool CustomDataDialogModel::updateAt(size_t index, const std::pair& e, SCP_string* errorOut) +{ + // Bounds check + SCP_map::iterator it; + if (!advance_to_index(_items, index, it)) { + if (errorOut) + *errorOut = "Invalid index."; + return false; + } + + // validation + if (!validateKeySyntax(e.first, errorOut)) + return false; + if (!validateValue(e.second, errorOut)) + return false; + + // uniqueness (case-insensitive) ignoring this index + if (!keyIsUnique(e.first, index)) { + if (errorOut) + *errorOut = "Key must be unique."; + return false; + } + + // No change? + if (stricmp(it->first.c_str(), e.first.c_str()) == 0 && it->second == e.second) { + return true; // no change + } + + // If the key is unchanged (case-insensitive), just update the value + if (stricmp(it->first.c_str(), e.first.c_str()) == 0) { + if (it->second != e.second) { + it->second = e.second; + set_modified(); + } + return true; + } + + // Key changed: erase old and insert new pair + _items.erase(it); + _items.emplace(e); + set_modified(); + return true; +} + +bool CustomDataDialogModel::removeAt(size_t index) +{ + SCP_map::iterator it; + if (!advance_to_index(_items, index, it)) + return false; + + _items.erase(it); + set_modified(); + return true; +} + +bool CustomDataDialogModel::hasKey(const SCP_string& key) const +{ + return std::any_of(_items.begin(), _items.end(), [&key](const auto& kv) { + return stricmp(kv.first.c_str(), key.c_str()) == 0; + }); +} + +std::optional CustomDataDialogModel::indexOfKey(const SCP_string& key) const +{ + size_t i = 0; + for (const auto& kv : _items) { + if (stricmp(kv.first.c_str(), key.c_str()) == 0) + return i; + ++i; + } + return std::nullopt; +} + +bool CustomDataDialogModel::validateKeySyntax(const SCP_string& key, SCP_string* errorOut) +{ + if (key.empty()) { + if (errorOut) + *errorOut = "Key cannot be empty."; + return false; + } + // No whitespace allowed + if (key.find_first_of(" \t\r\n") != SCP_string::npos) { + if (errorOut) + *errorOut = "Key cannot contain whitespace."; + return false; + } + return true; +} + +bool CustomDataDialogModel::validateValue(const SCP_string& value, SCP_string* errorOut) +{ + if (value.empty()) { + if (errorOut) + *errorOut = "Value cannot be empty."; + return false; + } + return true; +} + +bool CustomDataDialogModel::keyIsUnique(const SCP_string& key, std::optional ignoreIndex) const +{ + size_t i = 0; + for (const auto& kv : _items) { + if (ignoreIndex && *ignoreIndex == i) { + ++i; + continue; + } + if (stricmp(kv.first.c_str(), key.c_str()) == 0) + return false; + ++i; + } + return true; +} \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/MissionSpecs/CustomDataDialogModel.h b/qtfred/src/mission/dialogs/MissionSpecs/CustomDataDialogModel.h new file mode 100644 index 00000000000..b456fe77f5b --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionSpecs/CustomDataDialogModel.h @@ -0,0 +1,40 @@ +#pragma once + +#include "globalincs/pstypes.h" + +#include "../AbstractDialogModel.h" + +namespace fso::fred::dialogs { + +// Mission Specs is responsible for committing to The_mission on its own Apply or discarding on Reject +class CustomDataDialogModel : public AbstractDialogModel { + public: + CustomDataDialogModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; + + void setInitial(const SCP_map& in); + + const SCP_map& items() const noexcept + { + return _items; + } + + bool add(const std::pair& e, SCP_string* errorOut); + bool updateAt(size_t index, const std::pair& e, SCP_string* errorOut); + bool removeAt(size_t index); + bool hasKey(const SCP_string& key) const; + std::optional indexOfKey(const SCP_string& key) const; + + // Validation helpers + static bool validateKeySyntax(const SCP_string& key, SCP_string* err = nullptr); + static bool validateValue(const SCP_string& val, SCP_string* err = nullptr); + + private: + bool keyIsUnique(const SCP_string& key, std::optional ignoreIndex = std::nullopt) const; + + SCP_map _items; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/MissionSpecs/CustomStringsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecs/CustomStringsDialogModel.cpp new file mode 100644 index 00000000000..03c5ae4d683 --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionSpecs/CustomStringsDialogModel.cpp @@ -0,0 +1,152 @@ +#include "CustomStringsDialogModel.h" + +namespace fso::fred::dialogs { + +CustomStringsDialogModel::CustomStringsDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + +} + +bool CustomStringsDialogModel::apply() +{ + // No direct application; this model is used to collect custom strings + // and the actual application is handled by the MissionSpecDialogModel. + return true; +} + +void CustomStringsDialogModel::reject() +{ + // No direct rejection; this model is used to collect custom strings + // and the actual rejection is handled by the MissionSpecDialogModel. +} + +void CustomStringsDialogModel::setInitial(const SCP_vector& in) +{ + _items = in; +} + +bool CustomStringsDialogModel::add(const custom_string& e, SCP_string* errorOut) +{ + // validation + if (!validateKeySyntax(e.name, errorOut)) + return false; + if (!validateValue(e.value, errorOut)) + return false; + if (!validateText(e.text, errorOut)) + return false; + + if (!keyIsUnique(e.name)) { + if (errorOut) + *errorOut = "Key must be unique."; + return false; + } + + _items.push_back(e); + set_modified(); + return true; +} + +bool CustomStringsDialogModel::updateAt(size_t index, const custom_string& e, SCP_string* errorOut) +{ + if (index >= _items.size()) { + if (errorOut) + *errorOut = "Invalid index."; + return false; + } + // validation + if (!validateKeySyntax(e.name, errorOut)) + return false; + if (!validateValue(e.value, errorOut)) + return false; + if (!validateText(e.text, errorOut)) + return false; + + if (!keyIsUnique(e.name, index)) { + if (errorOut) + *errorOut = "Key must be unique."; + return false; + } + + if (_items[index].name == e.name && _items[index].value == e.value && _items[index].text == e.text) { + return true; // no change + } + + _items[index] = e; + set_modified(); + return true; +} + +bool CustomStringsDialogModel::removeAt(size_t index) +{ + if (index >= _items.size()) + return false; + _items.erase(_items.begin() + index); + set_modified(); + return true; +} + +bool CustomStringsDialogModel::hasKey(const SCP_string& key) const +{ + return std::any_of(_items.begin(), _items.end(), [&](const custom_string& it) { + return stricmp(it.name.c_str(), key.c_str()) == 0; + }); +} + +std::optional CustomStringsDialogModel::indexOfKey(const SCP_string& key) const +{ + for (size_t i = 0; i < _items.size(); ++i) { + if (stricmp(_items[i].name.c_str(), key.c_str()) == 0) + return i; + } + return std::nullopt; +} + +bool CustomStringsDialogModel::validateKeySyntax(const SCP_string& key, SCP_string* errorOut) +{ + if (key.empty()) { + if (errorOut) + *errorOut = "Key cannot be empty."; + return false; + } + // No whitespace allowed + if (key.find_first_of(" \t\r\n") != SCP_string::npos) { + if (errorOut) + *errorOut = "Key cannot contain whitespace."; + return false; + } + return true; +} + +bool CustomStringsDialogModel::validateValue(const SCP_string& value, SCP_string* errorOut) +{ + if (value.empty()) { + if (errorOut) + *errorOut = "Value cannot be empty."; + return false; + } + return true; +} + +bool CustomStringsDialogModel::validateText(const SCP_string& text, SCP_string* errorOut) +{ + if (text.empty()) { + if (errorOut) + *errorOut = "Text cannot be empty."; + return false; + } + return true; +} + +bool CustomStringsDialogModel::keyIsUnique(const SCP_string& key, std::optional ignoreIndex) const +{ + for (size_t i = 0; i < _items.size(); ++i) { + if (ignoreIndex && *ignoreIndex == i) + continue; + if (stricmp(_items[i].name.c_str(), key.c_str()) == 0) + return false; + } + return true; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/MissionSpecs/CustomStringsDialogModel.h b/qtfred/src/mission/dialogs/MissionSpecs/CustomStringsDialogModel.h new file mode 100644 index 00000000000..8faf52a02ca --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionSpecs/CustomStringsDialogModel.h @@ -0,0 +1,42 @@ +#pragma once + +#include "../AbstractDialogModel.h" + +#include "globalincs/pstypes.h" + +namespace fso::fred::dialogs { + +// Mission Specs is responsible for committing to The_mission on its own Apply or discarding on Reject +class CustomStringsDialogModel : public AbstractDialogModel { + public: + + CustomStringsDialogModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; + + void setInitial(const SCP_vector& in); + + const SCP_vector& items() const noexcept + { + return _items; + } + + bool add(const custom_string& e, SCP_string* errorOut = nullptr); + bool updateAt(size_t index, const custom_string& e, SCP_string* errorOut = nullptr); + bool removeAt(size_t index); + + bool hasKey(const SCP_string& key) const; + std::optional indexOfKey(const SCP_string& key) const; + + static bool validateKeySyntax(const SCP_string& key, SCP_string* errorOut = nullptr); + static bool validateValue(const SCP_string& value, SCP_string* errorOut = nullptr); + static bool validateText(const SCP_string& text, SCP_string* errorOut = nullptr); + + private: + bool keyIsUnique(const SCP_string& key, std::optional ignoreIndex = std::nullopt) const; + + SCP_vector _items; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.cpp similarity index 55% rename from qtfred/src/mission/dialogs/CustomWingNamesDialogModel.cpp rename to qtfred/src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.cpp index bcfa960cd8e..ffcf8a2ddee 100644 --- a/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.cpp @@ -2,27 +2,22 @@ #include "ship/ship.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { CustomWingNamesDialogModel::CustomWingNamesDialogModel(QObject * parent, EditorViewport * viewport) : AbstractDialogModel(parent, viewport) { - initializeData(); } bool CustomWingNamesDialogModel::apply() { - int i; - - for (auto wing : _m_starting) { + for (auto& wing : _m_starting) { Editor::strip_quotation_marks(wing); } - for (auto wing : _m_squadron) { + for (auto& wing : _m_squadron) { Editor::strip_quotation_marks(wing); } - for (auto wing : _m_tvt) { + for (auto& wing : _m_tvt) { Editor::strip_quotation_marks(wing); } @@ -67,78 +62,71 @@ bool CustomWingNamesDialogModel::apply() { } - // copy starting wings - for (i = 0; i < MAX_STARTING_WINGS; i++) { - strcpy_s(Starting_wing_names[i], _m_starting[i].c_str()); - } - - // copy squadron wings - for (i = 0; i < MAX_SQUADRON_WINGS; i++) { - strcpy_s(Squadron_wing_names[i], _m_squadron[i].c_str()); - } - - // copy tvt wings - for (i = 0; i < MAX_TVT_WINGS; i++) { - strcpy_s(TVT_wing_names[i], _m_tvt[i].c_str()); - } - - _viewport->editor->update_custom_wing_indexes(); - + // No direct application; this model is used to collect custom strings + // and the actual application is handled by the MissionSpecDialogModel. return true; } void CustomWingNamesDialogModel::reject() { + // No direct rejection; this model is used to collect custom strings + // and the actual rejection is handled by the MissionSpecDialogModel. } -void CustomWingNamesDialogModel::initializeData() { - int i; +void CustomWingNamesDialogModel::setInitialStartingWings(const std::array& startingWings) +{ + _m_starting = startingWings; +} - // init starting wings - for (i = 0; i < MAX_STARTING_WINGS; i++) { - _m_starting[i] = Starting_wing_names[i]; - } +void CustomWingNamesDialogModel::setInitialSquadronWings(const std::array& squadronWings) +{ + _m_squadron = squadronWings; +} - // init squadron wings - for (i = 0; i < MAX_SQUADRON_WINGS; i++) { - _m_squadron[i] = Squadron_wing_names[i]; - } +void CustomWingNamesDialogModel::setInitialTvTWings(const std::array& tvtWings) +{ + _m_tvt = tvtWings; +} - // init tvt wings - for (i = 0; i < MAX_TVT_WINGS; i++) { - _m_tvt[i] = TVT_wing_names[i]; - } +const std::array& CustomWingNamesDialogModel::getStartingWings() const { + return _m_starting; } -void CustomWingNamesDialogModel::setStartingWing(SCP_string str, int index) { +const std::array& CustomWingNamesDialogModel::getSquadronWings() const { + return _m_squadron; +} + +const std::array& CustomWingNamesDialogModel::getTvTWings() const { + return _m_tvt; +} + +void CustomWingNamesDialogModel::setStartingWing(const SCP_string& str, int index) +{ modify(_m_starting[index], str); } -void CustomWingNamesDialogModel::setSquadronWing(SCP_string str, int index) { +void CustomWingNamesDialogModel::setSquadronWing(const SCP_string& str, int index) +{ modify(_m_squadron[index], str); } -void CustomWingNamesDialogModel::setTvTWing(SCP_string str, int index) { +void CustomWingNamesDialogModel::setTvTWing(const SCP_string& str, int index) +{ modify(_m_tvt[index], str); } -SCP_string CustomWingNamesDialogModel::getStartingWing(int index) { +const SCP_string& CustomWingNamesDialogModel::getStartingWing(int index) +{ return _m_starting[index]; } -SCP_string CustomWingNamesDialogModel::getSquadronWing(int index) { +const SCP_string& CustomWingNamesDialogModel::getSquadronWing(int index) +{ return _m_squadron[index]; } -SCP_string CustomWingNamesDialogModel::getTvTWing(int index) { +const SCP_string& CustomWingNamesDialogModel::getTvTWing(int index) +{ return _m_tvt[index]; } -bool CustomWingNamesDialogModel::query_modified() { - return Starting_wing_names[0] != _m_starting[0] || Starting_wing_names[1] != _m_starting[1] || Starting_wing_names[2] != _m_starting[2] - || Squadron_wing_names[0] != _m_squadron[0] || Squadron_wing_names[1] != _m_squadron[1] || Squadron_wing_names[2] != _m_squadron[2] || Squadron_wing_names[3] != _m_squadron[3] || Squadron_wing_names[4] != _m_squadron[4] - || TVT_wing_names[0] != _m_tvt[0] || TVT_wing_names[1] != _m_tvt[1]; -} - -} -} -} \ No newline at end of file +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.h b/qtfred/src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.h new file mode 100644 index 00000000000..7256715b599 --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.h @@ -0,0 +1,35 @@ +#pragma once + +#include "../AbstractDialogModel.h" + +namespace fso::fred::dialogs { + +class CustomWingNamesDialogModel : public AbstractDialogModel { +public: + CustomWingNamesDialogModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; + + void setInitialStartingWings(const std::array& startingWings); + void setInitialSquadronWings(const std::array& squadronWings); + void setInitialTvTWings(const std::array& tvtWings); + + const std::array& getStartingWings() const; + const std::array& getSquadronWings() const; + const std::array& getTvTWings() const; + + void setStartingWing(const SCP_string&, int); + void setSquadronWing(const SCP_string&, int); + void setTvTWing(const SCP_string&, int); + const SCP_string& getStartingWing(int); + const SCP_string& getSquadronWing(int); + const SCP_string& getTvTWing(int); +private: + + std::array _m_starting; + std::array _m_squadron; + std::array _m_tvt; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.cpp new file mode 100644 index 00000000000..b98097369c8 --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.cpp @@ -0,0 +1,128 @@ +#include "SoundEnvironmentDialogModel.h" + +namespace fso::fred::dialogs { + +SoundEnvironmentDialogModel::SoundEnvironmentDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ +} + +bool SoundEnvironmentDialogModel::apply() +{ + // No direct application; this model is used to collect custom strings + // and the actual application is handled by the MissionSpecDialogModel. + return true; +} + +void SoundEnvironmentDialogModel::reject() +{ + // No direct rejection; this model is used to collect custom strings + // and the actual rejection is handled by the MissionSpecDialogModel. +} + +void SoundEnvironmentDialogModel::setInitial(const sound_env& env) +{ + _working = env; +} + +sound_env SoundEnvironmentDialogModel::params() const +{ + return _working; +} + +bool SoundEnvironmentDialogModel::validateVolume(float vol, SCP_string* errorOut) +{ + if (vol < 0.0f || vol > 1.0f) { + if (errorOut) + *errorOut = "Volume must be between 0.0 and 1.0."; + return false; + } + return true; +} + +bool SoundEnvironmentDialogModel::validateDamping(float d, SCP_string* errorOut) +{ + if (d < 0.0f || d > 1.0f) { + if (errorOut) + *errorOut = "Damping must be between 0.0 and 1.0."; + return false; + } + return true; +} + +bool SoundEnvironmentDialogModel::validateDecay(float decay, SCP_string* errorOut) +{ + if (decay <= 0.0f) { + if (errorOut) + *errorOut = "Decay must be greater than 0."; + return false; + } + return true; +} + +bool SoundEnvironmentDialogModel::setId(int id, SCP_string* errorOut) +{ + if (id < -1 || id >= static_cast(EFX_presets.size())) { + if (errorOut) + *errorOut = "Invalid environment ID."; + return false; + } + + if (_working.id == id) + return true; + + modify(_working.id, id); + + if (id == -1) { + // No environment selected; clear fields to defaults + modify(_working.volume, 0.0f); + modify(_working.damping, 0.1f); + modify(_working.decay, 0.1f); + return true; + } + + _working.volume = EFX_presets[id].flGain; + _working.damping = EFX_presets[id].flDecayHFRatio; + _working.decay = EFX_presets[id].flDecayTime; + return true; +} + +int SoundEnvironmentDialogModel::getId() const +{ + return _working.id; +} + +bool SoundEnvironmentDialogModel::setVolume(float vol, SCP_string* errorOut) +{ + if (!validateVolume(vol, errorOut)) + return false; + if (_working.volume == vol) + return true; + + modify(_working.volume, vol); + return true; +} + +bool SoundEnvironmentDialogModel::setDamping(float d, SCP_string* errorOut) +{ + if (!validateDamping(d, errorOut)) + return false; + if (_working.damping == d) + return true; + + modify(_working.damping, d); + return true; +} + +bool SoundEnvironmentDialogModel::setDecay(float decay, SCP_string* errorOut) +{ + if (!validateDecay(decay, errorOut)) + return false; + if (_working.decay == decay) + return true; + + modify(_working.decay, decay); + return true; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.h b/qtfred/src/mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.h new file mode 100644 index 00000000000..e44e5b1ade6 --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.h @@ -0,0 +1,34 @@ +#pragma once + +#include "globalincs/pstypes.h" // for SCP_string +#include "sound/sound.h" + +#include "mission/dialogs/AbstractDialogModel.h" + +namespace fso::fred::dialogs { + +class SoundEnvironmentDialogModel final : public AbstractDialogModel { + public: + SoundEnvironmentDialogModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; + + void setInitial(const sound_env& env); + sound_env params() const; + + bool setId(int id, SCP_string* errorOut = nullptr); + int getId() const; + bool setVolume(float vol, SCP_string* errorOut = nullptr); + bool setDamping(float damping, SCP_string* errorOut = nullptr); + bool setDecay(float decay, SCP_string* errorOut = nullptr); + + private: + sound_env _working = {}; + + static bool validateVolume(float vol, SCP_string* errorOut); + static bool validateDamping(float d, SCP_string* errorOut); + static bool validateDecay(float decay, SCP_string* errorOut); +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/CustomWingNamesDialog.cpp b/qtfred/src/ui/dialogs/CustomWingNamesDialog.cpp deleted file mode 100644 index 56deb21eb95..00000000000 --- a/qtfred/src/ui/dialogs/CustomWingNamesDialog.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "CustomWingNamesDialog.h" - -#include "ui_CustomWingNamesDialog.h" -#include - -namespace fso { -namespace fred { -namespace dialogs { - -CustomWingNamesDialog::CustomWingNamesDialog(QWidget* parent, EditorViewport* viewport) : - QDialog(parent), ui(new Ui::CustomWingNamesDialog()), _model(new CustomWingNamesDialogModel(this, viewport)), - _viewport(viewport) { - ui->setupUi(this); - - connect(this, &QDialog::accepted, _model.get(), &CustomWingNamesDialogModel::apply); - connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &CustomWingNamesDialog::rejectHandler); - - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &CustomWingNamesDialog::updateUI); - - // Starting wings - connect(ui->startingWing_1, &QLineEdit::textChanged, this, [this](const QString & param) { startingWingChanged(param, 0); }); - connect(ui->startingWing_2, &QLineEdit::textChanged, this, [this](const QString & param) { startingWingChanged(param, 1); }); - connect(ui->startingWing_3, &QLineEdit::textChanged, this, [this](const QString & param) { startingWingChanged(param, 2); }); - - // Squadron wings - connect(ui->squadronWing_1, &QLineEdit::textChanged, this, [this](const QString & param) { squadronWingChanged(param, 0); }); - connect(ui->squadronWing_2, &QLineEdit::textChanged, this, [this](const QString & param) { squadronWingChanged(param, 1); }); - connect(ui->squadronWing_3, &QLineEdit::textChanged, this, [this](const QString & param) { squadronWingChanged(param, 2); }); - connect(ui->squadronWing_4, &QLineEdit::textChanged, this, [this](const QString & param) { squadronWingChanged(param, 3); }); - connect(ui->squadronWing_5, &QLineEdit::textChanged, this, [this](const QString & param) { squadronWingChanged(param, 4); }); - - // Dogfight wings - connect(ui->dogfightWing_1, &QLineEdit::textChanged, this, [this](const QString & param) { dogfightWingChanged(param, 0); }); - connect(ui->dogfightWing_2, &QLineEdit::textChanged, this, [this](const QString & param) { dogfightWingChanged(param, 1); }); - - updateUI(); - - // Resize the dialog to the minimum size - resize(QDialog::sizeHint()); -} - -CustomWingNamesDialog::~CustomWingNamesDialog() { -} - -void CustomWingNamesDialog::closeEvent(QCloseEvent * e) { - if (!rejectOrCloseHandler(this, _model.get(), _viewport)) { - e->ignore(); - }; -} -void CustomWingNamesDialog::rejectHandler() -{ - this->close(); -} -void CustomWingNamesDialog::updateUI() { - // Update starting wings - ui->startingWing_1->setText(_model->getStartingWing(0).c_str()); - ui->startingWing_2->setText(_model->getStartingWing(1).c_str()); - ui->startingWing_3->setText(_model->getStartingWing(2).c_str()); - - // Update squadron wings - ui->squadronWing_1->setText(_model->getSquadronWing(0).c_str()); - ui->squadronWing_2->setText(_model->getSquadronWing(1).c_str()); - ui->squadronWing_3->setText(_model->getSquadronWing(2).c_str()); - ui->squadronWing_4->setText(_model->getSquadronWing(3).c_str()); - ui->squadronWing_5->setText(_model->getSquadronWing(4).c_str()); - - // Update dogfight wings - ui->dogfightWing_1->setText(_model->getTvTWing(0).c_str()); - ui->dogfightWing_2->setText(_model->getTvTWing(1).c_str()); -} - -void CustomWingNamesDialog::startingWingChanged(const QString & str, int index) { - _model->setStartingWing(str.toStdString(), index); -} - -void CustomWingNamesDialog::squadronWingChanged(const QString & str, int index) { - _model->setSquadronWing(str.toStdString(), index); -} - -void CustomWingNamesDialog::dogfightWingChanged(const QString & str, int index) { - _model->setTvTWing(str.toStdString(), index); -} - -} -} -} diff --git a/qtfred/src/ui/dialogs/CustomWingNamesDialog.h b/qtfred/src/ui/dialogs/CustomWingNamesDialog.h deleted file mode 100644 index 338f9b76cb1..00000000000 --- a/qtfred/src/ui/dialogs/CustomWingNamesDialog.h +++ /dev/null @@ -1,48 +0,0 @@ -#ifndef CUSTOMWINGNAMESDIALOG_H -#define CUSTOMWINGNAMESDIALOG_H - -#include -#include - -#include - -#include "mission/dialogs/CustomWingNamesDialogModel.h" - -namespace fso { -namespace fred { -namespace dialogs { - -namespace Ui { -class CustomWingNamesDialog; -} - -class CustomWingNamesDialog : public QDialog -{ - Q_OBJECT - -public: - explicit CustomWingNamesDialog(QWidget* parent, EditorViewport* viewport); - ~CustomWingNamesDialog() override; - -protected: - void closeEvent(QCloseEvent*) override; - - void rejectHandler(); - -private: - std::unique_ptr ui; - std::unique_ptr _model; - EditorViewport* _viewport; - - void updateUI(); - - void startingWingChanged(const QString &, int); - void squadronWingChanged(const QString &, int); - void dogfightWingChanged(const QString &, int); -}; - -} -} -} - -#endif // CUSTOMWINGNAMESDIALOG_H diff --git a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp index 24739cf4f96..49c6e07d7fb 100644 --- a/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp @@ -126,6 +126,4 @@ void JumpNodeEditorDialog::on_hiddenByDefaultCheckBox_toggled(bool checked) _model->setHidden(checked); } -void temp() {}; - } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp index 1b9a87c8f52..698cf44e52c 100644 --- a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp @@ -2,125 +2,76 @@ #include "ui_MissionSpecDialog.h" +#include +#include +#include +#include +#include #include #include "mission/util.h" #include #include +#include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { MissionSpecDialog::MissionSpecDialog(FredView* parent, EditorViewport* viewport) : QDialog(parent), ui(new Ui::MissionSpecDialog()), _model(new MissionSpecDialogModel(this, viewport)), _viewport(viewport) { ui->setupUi(this); - connect(this, &QDialog::accepted, _model.get(), &MissionSpecDialogModel::apply); - connect(ui->dialogButtonBox, &QDialogButtonBox::rejected, _model.get(), &MissionSpecDialogModel::reject); + connect(_model.get(), &AbstractDialogModel::modelChanged, this, &MissionSpecDialog::updateUi); - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &MissionSpecDialog::updateUI); - - // Mission title and creator - connect(ui->missionTitle, static_cast(&QLineEdit::textChanged), this, &MissionSpecDialog::missionTitleChanged); - connect(ui->missionDesigner, static_cast(&QLineEdit::textChanged), this, &MissionSpecDialog::missionDesignerChanged); - - // Mission type - connect(ui->m_type_SinglePlayer, &QRadioButton::toggled, this, [this](bool param) {missionTypeToggled(param, MISSION_TYPE_SINGLE); }); - connect(ui->m_type_MultiPlayer, &QRadioButton::toggled, this, [this](bool param) {missionTypeToggled(param, MISSION_TYPE_MULTI | MISSION_TYPE_MULTI_COOP); }); - connect(ui->m_type_Training, &QRadioButton::toggled, this, [this](bool param) {missionTypeToggled(param, MISSION_TYPE_TRAINING); }); - connect(ui->m_type_Cooperative, &QRadioButton::toggled, this, [this](bool param) {missionTypeToggled(param, MISSION_TYPE_MULTI | MISSION_TYPE_MULTI_COOP); }); - connect(ui->m_type_TeamVsTeam, &QRadioButton::toggled, this, [this](bool param) {missionTypeToggled(param, MISSION_TYPE_MULTI | MISSION_TYPE_MULTI_TEAMS); }); - connect(ui->m_type_Dogfight, &QRadioButton::toggled, this, [this](bool param) {missionTypeToggled(param, MISSION_TYPE_MULTI | MISSION_TYPE_MULTI_DOGFIGHT); }); - - // Respawn info - connect(ui->maxRespawnCount, static_cast(&QSpinBox::valueChanged), this, &MissionSpecDialog::maxRespawnChanged); - connect(ui->respawnDelayCount, static_cast(&QSpinBox::valueChanged), this, &MissionSpecDialog::respawnDelayChanged); - - // Custom Wing Names - // Placeholder - UI and Model need to be made - - // Squadron Reassign - connect(ui->squadronName, &QLineEdit::textChanged, this, &MissionSpecDialog::squadronNameChanged); - - // Loading Screen - Nothing to connect as buttons are connected directly to slots - - // Support Ships - connect(ui->toggleSupportShip, &QCheckBox::toggled, this, &MissionSpecDialog::disallowSupportChanged); - connect(ui->toggleHullRepair, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Support_repairs_hull); }); - connect(ui->hullRepairMax, static_cast(&QDoubleSpinBox::valueChanged), this, &MissionSpecDialog::hullRepairMaxChanged); - connect(ui->subsysRepairMax, static_cast(&QDoubleSpinBox::valueChanged), this, &MissionSpecDialog::subsysRepairMaxChanged); - - // Ship Trails - connect(ui->toggleTrail, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Toggle_ship_trails); }); - connect(ui->toggleSpeedDisplay, &QCheckBox::toggled, this, &MissionSpecDialog::trailDisplaySpeedToggled); - connect(ui->minDisplaySpeed, static_cast(&QSpinBox::valueChanged), this, &MissionSpecDialog::minTrailDisplaySpeedChanged); - - // Built-in Command Messages - connect(ui->senderCombBox, static_cast(&QComboBox::currentIndexChanged),this, &MissionSpecDialog::cmdSenderChanged); - connect(ui->personaComboBox, static_cast(&QComboBox::currentIndexChanged), this, &MissionSpecDialog::cmdPersonaChanged); - - // Mission Music - - // Sound Environment - - // Mission flags - Lambda functions are used to allow the passing of additional parameters to a single method - connect(ui->toggleAllTeamsAtWar, &QCheckBox::toggled, _model.get(), &MissionSpecDialogModel::setMissionFullWar); - connect(ui->toggleRedAlert, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Red_alert); }); - connect(ui->toggleScramble, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Scramble); }); - connect(ui->togglePromotion, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::No_promotion); }); - connect(ui->toggleBuiltinMsg, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::No_builtin_msgs); }); - connect(ui->toggleBuiltinCmdMsg, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::No_builtin_command); }); - connect(ui->toggleNoTraitor, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::No_traitor); }); - connect(ui->toggleBeamFreeDefault, &QCheckBox::toggled, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Beam_free_all_by_default); }); - connect(ui->toggleNoBriefing, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::No_briefing); }); - connect(ui->toggleDebriefing, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Toggle_debriefing); }); - connect(ui->toggleAutopilotCinematics, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Use_ap_cinematics); }); - connect(ui->toggleHardcodedAutopilot, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Deactivate_ap); }); - connect(ui->toggleAIControlStart, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Player_start_ai); }); - connect(ui->toggleChaseViewStart, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Toggle_start_chase_view); }); - connect(ui->toggle2DMission, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Mission_2d); }); - connect(ui->toggleGoalsInBriefing, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Toggle_showing_goals); }); - connect(ui->toggleMissionEndToMainhall, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::End_to_mainhall); }); - connect(ui->toggleOverrideHashCommand, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Override_hashcommand); }); - connect(ui->togglePreloadSubspace, &QCheckBox::toggled, this, [this](bool param) {flagToggled(param, Mission::Mission_Flags::Preload_subspace); }); - - // AI Profiles - connect(ui->aiProfileCombo, static_cast(&QComboBox::currentIndexChanged), this, &MissionSpecDialog::aiProfileIndexChanged); - - // Mission Description - connect(ui->missionDescEditor,&QPlainTextEdit::textChanged, this, &MissionSpecDialog::missionDescChanged); - - // Designer Notes - connect(ui->designerNoteEditor, &QPlainTextEdit::textChanged, this, &MissionSpecDialog::designerNotesChanged); - - updateUI(); + initializeUi(); + updateUi(); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); } -MissionSpecDialog::~MissionSpecDialog() { +MissionSpecDialog::~MissionSpecDialog() = default; + +void MissionSpecDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void MissionSpecDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close } void MissionSpecDialog::closeEvent(QCloseEvent* e) { - if (!rejectOrCloseHandler(this, _model.get(), _viewport)) { - e->ignore(); - }; + reject(); + e->ignore(); // Don't let the base class close the window } -void MissionSpecDialog::rejectHandler() + +void MissionSpecDialog::initializeUi() { - this->close(); + initFlagList(); + updateUi(); } -void MissionSpecDialog::updateUI() { +void MissionSpecDialog::updateUi() { util::SignalBlockers blockers(this); ui->missionTitle->setText(_model->getMissionTitle().c_str()); ui->missionDesigner->setText(_model->getDesigner().c_str()); - ui->createdLabel->setText(_model->getCreatedTime().c_str()); - ui->modifiedLabel->setText(_model->getModifiedTime().c_str()); + SCP_string created = "Created: " + _model->getCreatedTime(); + SCP_string modified = "Modified: " + _model->getModifiedTime(); + ui->createdLabel->setText(created.c_str()); + ui->modifiedLabel->setText(modified.c_str()); updateMissionType(); @@ -137,9 +88,11 @@ void MissionSpecDialog::updateUI() { ui->highResScreen->setText(_model->getHighResLoadingScren().c_str()); ui->toggleSupportShip->setChecked(_model->getDisallowSupport()); + ui->toggleHullRepair->setChecked(_model->getMissionFlag(Mission::Mission_Flags::Support_repairs_hull)); ui->hullRepairMax->setValue(_model->getHullRepairMax()); ui->subsysRepairMax->setValue(_model->getSubsysRepairMax()); + ui->toggleTrail->setChecked(_model->getMissionFlag(Mission::Mission_Flags::Toggle_ship_trails)); ui->toggleSpeedDisplay->setChecked(_model->getTrailThresholdFlag()); ui->minDisplaySpeed->setEnabled(_model->getTrailThresholdFlag()); ui->minDisplaySpeed->setValue(_model->getTrailDisplaySpeed()); @@ -155,6 +108,30 @@ void MissionSpecDialog::updateUI() { updateTextEditors(); } +void MissionSpecDialog::initFlagList() +{ + updateFlags(); + + // per flag immediate apply to the model + connect(ui->flagList, &fso::fred::FlagListWidget::flagToggled, this, [this](const QString& name, bool checked) { + _model->setMissionFlag(name.toUtf8().constData(), checked); + }); +} + +void MissionSpecDialog::updateFlags() +{ + const auto flags = _model->getMissionFlagsList(); + + QVector> toWidget; + toWidget.reserve(static_cast(flags.size())); + for (const auto& p : flags) { + QString name = QString::fromUtf8(p.first.c_str()); + toWidget.append({name, p.second}); + } + + ui->flagList->setFlags(toWidget); +} + void MissionSpecDialog::updateMissionType() { int m_type = _model->getMissionType(); @@ -188,11 +165,15 @@ void MissionSpecDialog::updateCmdMessage() { auto sender = _model->getCommandSender(); ui->senderCombBox->clear(); ui->senderCombBox->addItem(DEFAULT_COMMAND, QVariant(QString(DEFAULT_COMMAND))); + for (i = 0; i < MAX_SHIPS; i++) { - if (Ships[i].objnum >= 0) - if (Ship_info[Ships[i].ship_info_index].is_huge_ship()) + if (Ships[i].objnum >= 0) { + if (Ship_info[Ships[i].ship_info_index].is_huge_ship()) { ui->senderCombBox->addItem(Ships[i].ship_name, QVariant(QString(Ships[i].ship_name))); + } + } } + ui->senderCombBox->setCurrentIndex(ui->senderCombBox->findText(sender.c_str())); save_idx = _model->getCommandPersona(); @@ -203,6 +184,8 @@ void MissionSpecDialog::updateCmdMessage() { } } ui->personaComboBox->setCurrentIndex(ui->personaComboBox->findData(save_idx)); + + ui->toggleOverrideHashCommand->setChecked(_model->getMissionFlag(Mission::Mission_Flags::Override_hashcommand)); } void MissionSpecDialog::updateMusic() { @@ -225,31 +208,6 @@ void MissionSpecDialog::updateMusic() { ui->musicPackCombo->setCurrentIndex(ui->musicPackCombo->findText(musicPack.c_str())); } -void MissionSpecDialog::updateFlags() { - auto flags = _model->getMissionFlags(); - ui->toggle2DMission->setChecked(flags[Mission::Mission_Flags::Mission_2d]); - ui->toggleAIControlStart->setChecked(flags[Mission::Mission_Flags::Player_start_ai]); - ui->toggleChaseViewStart->setChecked(flags[Mission::Mission_Flags::Toggle_start_chase_view]); - ui->toggleAllTeamsAtWar->setChecked(flags[Mission::Mission_Flags::All_attack]); - ui->toggleAutopilotCinematics->setChecked(flags[Mission::Mission_Flags::Use_ap_cinematics]); - ui->toggleBeamFreeDefault->setChecked(flags[Mission::Mission_Flags::Beam_free_all_by_default]); - ui->toggleBuiltinCmdMsg->setChecked(flags[Mission::Mission_Flags::No_builtin_command]); - ui->toggleBuiltinMsg->setChecked(flags[Mission::Mission_Flags::No_builtin_msgs]); - ui->toggleDebriefing->setChecked(flags[Mission::Mission_Flags::Toggle_debriefing]); - ui->toggleGoalsInBriefing->setChecked(flags[Mission::Mission_Flags::Toggle_showing_goals]); - ui->toggleHardcodedAutopilot->setChecked(flags[Mission::Mission_Flags::Deactivate_ap]); - ui->toggleMissionEndToMainhall->setChecked(flags[Mission::Mission_Flags::End_to_mainhall]); - ui->toggleOverrideHashCommand->setChecked(flags[Mission::Mission_Flags::Override_hashcommand]); - ui->toggleNoBriefing->setChecked(flags[Mission::Mission_Flags::No_briefing]); - ui->toggleNoTraitor->setChecked(flags[Mission::Mission_Flags::No_traitor]); - ui->togglePromotion->setChecked(flags[Mission::Mission_Flags::No_promotion]); - ui->toggleRedAlert->setChecked(flags[Mission::Mission_Flags::Red_alert]); - ui->toggleScramble->setChecked(flags[Mission::Mission_Flags::Scramble]); - ui->toggleHullRepair->setChecked(flags[Mission::Mission_Flags::Support_repairs_hull]); - ui->toggleTrail->setChecked(flags[Mission::Mission_Flags::Toggle_ship_trails]); - ui->togglePreloadSubspace->setChecked(flags[Mission::Mission_Flags::Preload_subspace]); -} - void MissionSpecDialog::updateAIProfiles() { int idx = _model->getAIProfileIndex(); ui->aiProfileCombo->clear(); @@ -271,116 +229,225 @@ void MissionSpecDialog::updateTextEditors() { ui->designerNoteEditor->setTextCursor(textCursor); } -void MissionSpecDialog::missionTitleChanged(const QString & string) { - _model->setMissionTitle(string.toStdString()); +void MissionSpecDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void MissionSpecDialog::on_okAndCancelButtons_rejected() +{ + reject(); } -void MissionSpecDialog::missionDesignerChanged(const QString & string) { - _model->setDesigner(string.toStdString()); +void MissionSpecDialog::on_missionTitle_textChanged(const QString & string) { + _model->setMissionTitle(string.toUtf8().constData()); +} + +void MissionSpecDialog::on_missionDesigner_textChanged(const QString & string) { + _model->setDesigner(string.toUtf8().constData()); +} + +void MissionSpecDialog::on_m_type_SinglePlayer_toggled(bool checked) { + if (checked) { + _model->setMissionType(MISSION_TYPE_SINGLE); + } } -void MissionSpecDialog::missionTypeToggled(bool enabled, int m_type) { - if (enabled) { - _model->setMissionType(m_type); +void MissionSpecDialog::on_m_type_MultiPlayer_toggled(bool checked) { + if (checked) { + _model->setMissionType(MISSION_TYPE_MULTI | MISSION_TYPE_MULTI_COOP); } } -void MissionSpecDialog::maxRespawnChanged(int value) { +void MissionSpecDialog::on_m_type_Training_toggled(bool checked) { + if (checked) { + _model->setMissionType(MISSION_TYPE_TRAINING); + } +} + +void MissionSpecDialog::on_m_type_Cooperative_toggled(bool checked) { + if (checked) { + _model->setMissionType(MISSION_TYPE_MULTI | MISSION_TYPE_MULTI_COOP); + } +} + +void MissionSpecDialog::on_m_type_TeamVsTeam_toggled(bool checked) { + if (checked) { + _model->setMissionType(MISSION_TYPE_MULTI | MISSION_TYPE_MULTI_TEAMS); + } +} + +void MissionSpecDialog::on_m_type_Dogfight_toggled(bool checked) { + if (checked) { + _model->setMissionType(MISSION_TYPE_MULTI | MISSION_TYPE_MULTI_DOGFIGHT); + } +} + +void MissionSpecDialog::on_maxRespawnCount_valueChanged(int value) { _model->setNumRespawns(value); } -void MissionSpecDialog::respawnDelayChanged(int value) { +void MissionSpecDialog::on_respawnDelayCount_valueChanged(int value) { _model->setMaxRespawnDelay(value); } -void MissionSpecDialog::squadronNameChanged(const QString & string) { - _model->setSquadronName(string.toStdString()); +void MissionSpecDialog::on_squadronName_textChanged(const QString & string) { + _model->setSquadronName(string.toUtf8().constData()); } -void MissionSpecDialog::on_customWingNameButton_clicked() { - CustomWingNamesDialog* dialog = new CustomWingNamesDialog(this, _viewport); - dialog->setAttribute(Qt::WA_DeleteOnClose); - dialog->exec(); +void MissionSpecDialog::on_customWingNameButton_clicked() +{ + CustomWingNamesDialog dialog(this, _viewport); + dialog.setInitialStartingWings(_model->getCustomStartingWings()); + dialog.setInitialSquadronWings(_model->getCustomSquadronWings()); + dialog.setInitialTvTWings(_model->getCustomTvTWings()); + + if (dialog.exec() == QDialog::Accepted) { + _model->setCustomStartingWings(dialog.getStartingWings()); + _model->setCustomSquadronWings(dialog.getSquadronWings()); + _model->setCustomTvTWings(dialog.getTvTWings()); + } } void MissionSpecDialog::on_squadronLogoButton_clicked() { - QString filename = QFileDialog::getOpenFileName(this, tr("Open Image"), "", tr("Image Files (*.dds *.pcx);;DDS (*.dds);;PCX(*.pcx);;All Files (*.*)")); - if (!(filename.isNull() || filename.isEmpty())) { - _model->setSquadronLogo(QFileInfo(filename).fileName().toStdString()); + const auto files = _model->getSquadLogoList(); + if (files.empty()) { + QMessageBox::information(this, "Select Squad Image", "No images found."); + return; } + + QStringList qnames; + qnames.reserve(static_cast(files.size())); + for (const auto& s : files) + qnames << QString::fromStdString(s); + + ImagePickerDialog dlg(this); + dlg.setWindowTitle("Select Squad Image"); + dlg.allowUnset(true); + dlg.setImageFilenames(qnames); + + // Optional: preselect current + dlg.setInitialSelection(QString::fromStdString(_model->getSquadronLogo())); + + if (dlg.exec() != QDialog::Accepted) + return; + + const std::string chosen = dlg.selectedFile().toUtf8().constData(); + _model->setSquadronLogo(chosen); } void MissionSpecDialog::on_lowResScreenButton_clicked() { QString filename = QFileDialog::getOpenFileName(this, tr("Open Image"), "", tr("Image Files (*.dds *.pcx *.jpg *.jpeg *.tga *.png);;DDS (*.dds);;PCX (*.pcx);;JPG (*.jpg *.jpeg);;TGA (*.tga);;PNG (*.png) ;;All Files (*.*)")); if (!(filename.isNull() || filename.isEmpty())) { - _model->setLowResLoadingScreen(QFileInfo(filename).fileName().toStdString()); + _model->setLowResLoadingScreen(QFileInfo(filename).fileName().toUtf8().constData()); } } void MissionSpecDialog::on_highResScreenButton_clicked() { QString filename = QFileDialog::getOpenFileName(this, tr("Open Image"), "", tr("Image Files (*.dds *.pcx *.jpg *.jpeg *.tga *.png);;DDS (*.dds);;PCX (*.pcx);;JPG (*.jpg *.jpeg);;TGA (*.tga);;PNG (*.png) ;;All Files (*.*)")); if (!(filename.isNull() || filename.isEmpty())) { - _model->setHighResLoadingScreen(QFileInfo(filename).fileName().toStdString()); + _model->setHighResLoadingScreen(QFileInfo(filename).fileName().toUtf8().constData()); } } -void MissionSpecDialog::disallowSupportChanged(bool enabled) { +void MissionSpecDialog::on_toggleSupportShip_toggled(bool enabled) { _model->setDisallowSupport(enabled); } -void MissionSpecDialog::hullRepairMaxChanged(double value) { +void MissionSpecDialog::on_toggleHullRepair_toggled(bool enabled) { + _model->setMissionFlagDirect(Mission::Mission_Flags::Support_repairs_hull, enabled); +} + +void MissionSpecDialog::on_hullRepairMax_valueChanged(double value) { _model->setHullRepairMax((float)value); } -void MissionSpecDialog::subsysRepairMaxChanged(double value) { +void MissionSpecDialog::on_subsysRepairMax_valueChanged(double value) { _model->setSubsysRepairMax((float)value); } -void MissionSpecDialog::trailDisplaySpeedToggled(bool enabled) { +void MissionSpecDialog::on_toggleTrail_toggled(bool enabled) { + _model->setMissionFlagDirect(Mission::Mission_Flags::Toggle_ship_trails, enabled); +} + +void MissionSpecDialog::on_toggleSpeedDisplay_toggled(bool enabled) { _model->setTrailThresholdFlag(enabled); } -void MissionSpecDialog::minTrailDisplaySpeedChanged(int value) { +void MissionSpecDialog::on_minDisplaySpeed_valueChanged(int value) { _model->setTrailDisplaySpeed(value); } -void MissionSpecDialog::cmdSenderChanged(int index) { - auto sender = ui->senderCombBox->itemData(index).value().toStdString(); +void MissionSpecDialog::on_senderCombBox_currentIndexChanged(int index) { + SCP_string sender = ui->senderCombBox->itemData(index).value().toUtf8().constData(); _model->setCommandSender(sender); } -void MissionSpecDialog::cmdPersonaChanged(int index) { +void MissionSpecDialog::on_personaComboBox_currentIndexChanged(int index) { auto cmdPIndex = ui->personaComboBox->itemData(index).value(); _model->setCommandPersona(cmdPIndex); } -void MissionSpecDialog::eventMusicChanged(int index) { +void MissionSpecDialog::on_toggleOverrideHashCommand_toggled(bool checked) { + _model->setMissionFlagDirect(Mission::Mission_Flags::Override_hashcommand, checked); +} + +void MissionSpecDialog::on_defaultMusicCombo_currentIndexChanged(int index) { auto defMusicIdx = ui->defaultMusicCombo->itemData(index).value(); _model->setEventMusic(defMusicIdx); } -void MissionSpecDialog::subEventMusicChanged(int index) { - auto subMusic = ui->musicPackCombo->itemData(index).value().toStdString(); +void MissionSpecDialog::on_musicPackCombo_currentIndexChanged(int index) { + SCP_string subMusic = ui->musicPackCombo->itemData(index).value().toUtf8().constData(); _model->setSubEventMusic(subMusic); } -void MissionSpecDialog::flagToggled(bool enabled, Mission::Mission_Flags flag) { - _model->setMissionFlag(flag, enabled); +void MissionSpecDialog::on_aiProfileCombo_currentIndexChanged(int index) +{ + auto aipIndex = ui->aiProfileCombo->itemData(index).value(); + _model->setAIProfileIndex(aipIndex); } -void MissionSpecDialog::missionDescChanged() { - _model->setMissionDescText(ui->missionDescEditor->document()->toPlainText().toStdString()); -} +void MissionSpecDialog::on_soundEnvButton_clicked() +{ + SoundEnvironmentDialog dlg(this, _viewport); + dlg.setInitial(_model->getSoundEnvironmentParams()); -void MissionSpecDialog::designerNotesChanged() { - _model->setDesignerNoteText(ui->designerNoteEditor->document()->toPlainText().toStdString()); + if (dlg.exec() == QDialog::Accepted) { + _model->setSoundEnvironmentParams(dlg.items()); + } } -void MissionSpecDialog::aiProfileIndexChanged(int index) { - auto aipIndex = ui->aiProfileCombo->itemData(index).value(); - _model->setAIProfileIndex(aipIndex); +void MissionSpecDialog::on_customDataButton_clicked() +{ + CustomDataDialog dlg(this, _viewport); + dlg.setInitial(_model->getCustomData()); + + if (dlg.exec() == QDialog::Accepted) { + _model->setCustomData(dlg.items()); + } } +void MissionSpecDialog::on_customStringsButton_clicked() +{ + CustomStringsDialog dlg(this, _viewport); + dlg.setInitial(_model->getCustomStrings()); + + if (dlg.exec() == QDialog::Accepted) { + _model->setCustomStrings(dlg.items()); + } } + +void MissionSpecDialog::on_missionDescEditor_textChanged() +{ + SCP_string desc = ui->missionDescEditor->document()->toPlainText().toUtf8().constData(); + _model->setMissionDescText(desc); } + +void MissionSpecDialog::on_designerNoteEditor_textChanged() +{ + SCP_string note = ui->designerNoteEditor->document()->toPlainText().toUtf8().constData(); + _model->setDesignerNoteText(note); } + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionSpecDialog.h b/qtfred/src/ui/dialogs/MissionSpecDialog.h index 7c36fee6453..1c01d2eced0 100644 --- a/qtfred/src/ui/dialogs/MissionSpecDialog.h +++ b/qtfred/src/ui/dialogs/MissionSpecDialog.h @@ -1,17 +1,11 @@ -#ifndef MISSIONSPECDIALOG_H -#define MISSIONSPECDIALOG_H - #include #include #include #include -#include "CustomWingNamesDialog.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class MissionSpecDialog; @@ -25,27 +19,74 @@ class MissionSpecDialog : public QDialog explicit MissionSpecDialog(FredView* parent, EditorViewport* viewport); ~MissionSpecDialog() override; + void accept() override; + void reject() override; + protected: void closeEvent(QCloseEvent*) override; - void rejectHandler(); private slots: + // Dialog controls + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + // Left column + void on_missionTitle_textChanged(const QString& string); + void on_missionDesigner_textChanged(const QString& string); + void on_m_type_SinglePlayer_toggled(bool checked); + void on_m_type_MultiPlayer_toggled(bool checked); + void on_m_type_Training_toggled(bool checked); + void on_m_type_Cooperative_toggled(bool checked); + void on_m_type_TeamVsTeam_toggled(bool checked); + void on_m_type_Dogfight_toggled(bool checked); + void on_maxRespawnCount_valueChanged(int value); + void on_respawnDelayCount_valueChanged(int value); void on_customWingNameButton_clicked(); + void on_squadronName_textChanged(const QString& string); void on_squadronLogoButton_clicked(); void on_lowResScreenButton_clicked(); void on_highResScreenButton_clicked(); -private: + // Middle column + void on_toggleSupportShip_toggled(bool checked); + void on_toggleHullRepair_toggled(bool checked); + void on_hullRepairMax_valueChanged(double value); + void on_subsysRepairMax_valueChanged(double value); + void on_toggleTrail_toggled(bool checked); + void on_toggleSpeedDisplay_toggled(bool checked); + void on_minDisplaySpeed_valueChanged(int value); + void on_senderCombBox_currentIndexChanged(int index); + void on_personaComboBox_currentIndexChanged(int index); + void on_toggleOverrideHashCommand_toggled(bool checked); + void on_defaultMusicCombo_currentIndexChanged(int index); + void on_musicPackCombo_currentIndexChanged(int index); + + // Right column + // flags are dynamically generated and connected + void on_aiProfileCombo_currentIndexChanged(int index); + + // General + void on_soundEnvButton_clicked(); + void on_customDataButton_clicked(); + void on_customStringsButton_clicked(); + void on_missionDescEditor_textChanged(); + void on_designerNoteEditor_textChanged(); + + +private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; - void updateUI(); + void initializeUi(); + void updateUi(); + + void initFlagList(); + void updateFlags(); void updateMissionType(); void updateCmdMessage(); void updateMusic(); - void updateFlags(); void updateAIProfiles(); void updateTextEditors(); @@ -82,8 +123,4 @@ private slots: }; -} -} -} - -#endif // MISSIONSPECDIALOG_H +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.cpp new file mode 100644 index 00000000000..54795666750 --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.cpp @@ -0,0 +1,243 @@ +#include "CustomDataDialog.h" +#include "ui_CustomDataDialog.h" + +#include "mission/util.h" + +#include + +#include +#include +#include + +namespace fso::fred::dialogs { + +namespace { +enum Columns { + ColKey = 0, + ColValue = 1, + ColumnCount = 2 +}; +} // namespace + +CustomDataDialog::CustomDataDialog(QWidget* parent, EditorViewport* viewport) + : QDialog(parent), ui(new Ui::CustomDataDialog()), _model(new CustomDataDialogModel(this, viewport)), + _viewport(viewport) +{ + ui->setupUi(this); + + buildView(); + refreshTable(); + + // Initial selection if any rows exist + if (_tableModel->rowCount() > 0) { + selectRow(0); + } +} + +CustomDataDialog::~CustomDataDialog() = default; + +void CustomDataDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void CustomDataDialog::reject() +{ + // Custom reject or close logic because we need to handle talkback to Mission Specs + if (_model->query_modified()) { + auto button = _viewport->dialogProvider->showButtonDialog(fso::fred::DialogType::Question, + "Changes detected", + "Do you want to keep your changes?", + {fso::fred::DialogButton::Yes, fso::fred::DialogButton::No, fso::fred::DialogButton::Cancel}); + + if (button == fso::fred::DialogButton::Yes) { + accept(); + } + if (button == fso::fred::DialogButton::No) { + _model->reject(); + QDialog::reject(); // actually close + } + } else { + _model->reject(); + QDialog::reject(); + } +} + +void CustomDataDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window +} + +void CustomDataDialog::setInitial(const SCP_map& items) +{ + _model->setInitial(items); + + // Rebuild view from the new working copy + refreshTable(); + if (_tableModel->rowCount() > 0) { + selectRow(0); + } else { + clearEditors(); + } +} + +void CustomDataDialog::buildView() +{ + _tableModel = new QStandardItemModel(this); + _tableModel->setColumnCount(ColumnCount); + _tableModel->setHorizontalHeaderLabels({tr("Key"), tr("Value")}); + + ui->stringsTableView->setModel(_tableModel); + ui->stringsTableView->setSelectionBehavior(QAbstractItemView::SelectRows); + ui->stringsTableView->setSelectionMode(QAbstractItemView::SingleSelection); + ui->stringsTableView->setEditTriggers(QAbstractItemView::NoEditTriggers); + ui->stringsTableView->verticalHeader()->setVisible(false); + ui->stringsTableView->horizontalHeader()->setStretchLastSection(true); + ui->stringsTableView->setSortingEnabled(false); + + // Make sure headers are not interactive + auto* hdr = ui->stringsTableView->horizontalHeader(); + hdr->setSectionsClickable(false); // no click/press behavior + hdr->setSortIndicatorShown(false); // hide sort arrow + hdr->setHighlightSections(false); // don’t change look when selected + hdr->setSectionsMovable(false); // no drag-to-reorder columns + hdr->setFocusPolicy(Qt::NoFocus); + + // When a selection is made then load the editors + connect(ui->stringsTableView->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + [this](const QItemSelection&, const QItemSelection&) { + const auto idx = ui->stringsTableView->currentIndex(); + loadRowIntoEditors(idx.isValid() ? idx.row() : -1); + }); +} + +void CustomDataDialog::refreshTable() +{ + _tableModel->setRowCount(0); + + const auto& rows = _model->items(); + _tableModel->insertRows(0, static_cast(rows.size())); + int r = 0; + for (const auto& kv : rows) { + auto* keyItem = new QStandardItem(QString::fromStdString(kv.first)); + auto* valItem = new QStandardItem(QString::fromStdString(kv.second)); + keyItem->setEditable(false); + valItem->setEditable(false); + _tableModel->setItem(r, ColKey, keyItem); + _tableModel->setItem(r, ColValue, valItem); + ++r; + } +} + +void CustomDataDialog::selectRow(int row) +{ + if (row < 0 || row >= _tableModel->rowCount()) { + ui->stringsTableView->clearSelection(); + loadRowIntoEditors(-1); + return; + } + ui->stringsTableView->selectRow(row); + loadRowIntoEditors(row); +} + +void CustomDataDialog::loadRowIntoEditors(int row) +{ + util::SignalBlockers blockers(this); + + if (row < 0 || row >= _tableModel->rowCount()) { + clearEditors(); + return; + } + + const auto* keyItem = _tableModel->item(row, ColKey); + const auto* valItem = _tableModel->item(row, ColValue); + ui->keyLineEdit->setText(keyItem ? keyItem->text() : QString()); + ui->valueLineEdit->setText(valItem ? valItem->text() : QString()); +} + +std::pair CustomDataDialog::editorsToEntry() const +{ + std::pair e; + e.first = ui->keyLineEdit->text().toUtf8().constData(); + e.second = ui->valueLineEdit->text().toUtf8().constData(); + return e; +} + +void CustomDataDialog::clearEditors() +{ + util::SignalBlockers blockers(this); + + ui->keyLineEdit->clear(); + ui->valueLineEdit->clear(); +} + +void CustomDataDialog::on_addButton_clicked() +{ + auto e = editorsToEntry(); + SCP_string err; + if (!_model->add(e, &err)) { + QMessageBox::warning(this, tr("Invalid Entry"), QString::fromStdString(err)); + return; + } + + refreshTable(); + selectRow(_tableModel->rowCount() - 1); + clearEditors(); +} + +void CustomDataDialog::on_updateButton_clicked() +{ + const auto idx = ui->stringsTableView->currentIndex(); + if (!idx.isValid()) + return; + + auto e = editorsToEntry(); + SCP_string err; + if (!_model->updateAt(static_cast(idx.row()), e, &err)) { + QMessageBox::warning(this, tr("Invalid Entry"), QString::fromStdString(err)); + return; + } + + // The key change may reorder the map. Rebuild and reselect by key. + refreshTable(); + if (auto idxOpt = _model->indexOfKey(e.first)) { + selectRow(static_cast(*idxOpt)); + } +} + +void CustomDataDialog::on_removeButton_clicked() +{ + const auto idx = ui->stringsTableView->currentIndex(); + if (!idx.isValid()) + return; + + const auto key = _tableModel->item(idx.row(), ColKey)->text(); + if (QMessageBox::question(this, tr("Remove Entry"), tr("Remove key \"%1\"?").arg(key)) != QMessageBox::Yes) { + return; + } + + if (_model->removeAt(static_cast(idx.row()))) { + const int next = std::min(idx.row(), _tableModel->rowCount() - 2); // -1 after removal, then clamp + refreshTable(); + selectRow(next); + } +} + +void CustomDataDialog::on_okAndCancelButtons_accepted() +{ + accept(); // Mission Specs will read model->items() and commit during its own Apply +} + +void CustomDataDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.h b/qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.h new file mode 100644 index 00000000000..1df9319f190 --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionSpecs/CustomDataDialog.h @@ -0,0 +1,60 @@ +#pragma once + +#include "mission/dialogs/MissionSpecs/CustomDataDialogModel.h" + +#include + +#include +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class CustomDataDialog; +} + +class CustomDataDialog final : public QDialog { + Q_OBJECT + public: + explicit CustomDataDialog(QWidget* parent, EditorViewport* viewport); + ~CustomDataDialog() override; + + void accept() override; + void reject() override; + + void setInitial(const SCP_map& items); + + const SCP_map& items() const + { + return _model->items(); + } + + protected: + void closeEvent(QCloseEvent* e) override; + + private slots: + // Top-row buttons + void on_addButton_clicked(); + void on_updateButton_clicked(); + void on_removeButton_clicked(); + + // Dialog buttons + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + private: // NOLINT(readability-redundant-access-specifiers) + void buildView(); + void refreshTable(); + void selectRow(int row); + void loadRowIntoEditors(int row); + std::pair editorsToEntry() const; + void clearEditors(); + + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + QStandardItemModel* _tableModel = nullptr; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.cpp new file mode 100644 index 00000000000..91c19f87d43 --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.cpp @@ -0,0 +1,241 @@ +#include "CustomStringsDialog.h" + +#include "ui_CustomStringsDialog.h" + +#include +#include "mission/util.h" + +#include +#include +#include + +namespace fso::fred::dialogs { + +namespace { +enum Columns { + ColKey = 0, + ColValue = 1, + ColumnCount = 2 +}; +} // namespace + +CustomStringsDialog::CustomStringsDialog(QWidget* parent, EditorViewport* viewport) + : QDialog(parent), ui(new Ui::CustomStringsDialog()), _model(new CustomStringsDialogModel(this, viewport)), + _viewport(viewport) +{ + ui->setupUi(this); + + buildView(); + refreshTable(); + + // Initial selection if any rows exist + if (_tableModel->rowCount() > 0) { + selectRow(0); + } +} + +CustomStringsDialog::~CustomStringsDialog() = default; + +void CustomStringsDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void CustomStringsDialog::reject() +{ + // Custom reject or close logic because we need to handle talkback to Mission Specs + if (_model->query_modified()) { + auto button = _viewport->dialogProvider->showButtonDialog(fso::fred::DialogType::Question, + "Changes detected", + "Do you want to keep your changes?", + {fso::fred::DialogButton::Yes, fso::fred::DialogButton::No, fso::fred::DialogButton::Cancel}); + + if (button == fso::fred::DialogButton::Yes) { + accept(); + } + if (button == fso::fred::DialogButton::No) { + _model->reject(); + QDialog::reject(); // actually close + } + } else { + _model->reject(); + QDialog::reject(); + } +} + +void CustomStringsDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window +} + +void CustomStringsDialog::setInitial(const SCP_vector& items) +{ + _model->setInitial(items); + + // Rebuild view from the new working copy + refreshTable(); + if (_tableModel->rowCount() > 0) { + selectRow(0); + } else { + clearEditors(); + } +} + +void CustomStringsDialog::buildView() +{ + _tableModel = new QStandardItemModel(this); + _tableModel->setColumnCount(ColumnCount); + _tableModel->setHorizontalHeaderLabels({tr("Key"), tr("Value")}); + + ui->stringsTableView->setModel(_tableModel); + ui->stringsTableView->setSelectionBehavior(QAbstractItemView::SelectRows); + ui->stringsTableView->setSelectionMode(QAbstractItemView::SingleSelection); + ui->stringsTableView->setEditTriggers(QAbstractItemView::NoEditTriggers); + ui->stringsTableView->verticalHeader()->setVisible(false); + ui->stringsTableView->horizontalHeader()->setStretchLastSection(true); + ui->stringsTableView->setSortingEnabled(false); + + // Make sure headers are not interactive + auto* hdr = ui->stringsTableView->horizontalHeader(); + hdr->setSectionsClickable(false); // no click/press behavior + hdr->setSortIndicatorShown(false); // hide sort arrow + hdr->setHighlightSections(false); // don’t change look when selected + hdr->setSectionsMovable(false); // no drag-to-reorder columns + hdr->setFocusPolicy(Qt::NoFocus); + + // When a selection is made then load the editors + connect(ui->stringsTableView->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + [this](const QItemSelection&, const QItemSelection&) { + const auto idx = ui->stringsTableView->currentIndex(); + loadRowIntoEditors(idx.isValid() ? idx.row() : -1); + }); +} + +void CustomStringsDialog::refreshTable() +{ + _tableModel->setRowCount(0); + + const auto& rows = _model->items(); + _tableModel->insertRows(0, static_cast(rows.size())); + for (int i = 0; i < static_cast(rows.size()); ++i) { + const auto& e = rows[static_cast(i)]; + auto* keyItem = new QStandardItem(QString::fromStdString(e.name)); + auto* valItem = new QStandardItem(QString::fromStdString(e.value)); + keyItem->setEditable(false); + valItem->setEditable(false); + _tableModel->setItem(i, ColKey, keyItem); + _tableModel->setItem(i, ColValue, valItem); + } +} + +void CustomStringsDialog::selectRow(int row) +{ + if (row < 0 || row >= _tableModel->rowCount()) { + ui->stringsTableView->clearSelection(); + loadRowIntoEditors(-1); + return; + } + ui->stringsTableView->selectRow(row); + loadRowIntoEditors(row); +} + +void CustomStringsDialog::loadRowIntoEditors(int row) +{ + util::SignalBlockers blockers(this); + + if (row < 0 || row >= _tableModel->rowCount()) { + clearEditors(); + return; + } + + const auto& e = _model->items()[static_cast(row)]; + ui->keyLineEdit->setText(QString::fromStdString(e.name)); + ui->valueLineEdit->setText(QString::fromStdString(e.value)); + ui->stringTextEdit->setPlainText(QString::fromStdString(e.text)); +} + +custom_string CustomStringsDialog::editorsToEntry() const +{ + custom_string e; + e.name = ui->keyLineEdit->text().toUtf8().constData(); + e.value = ui->valueLineEdit->text().toUtf8().constData(); + e.text = ui->stringTextEdit->toPlainText().toUtf8().constData(); + return e; +} + +void CustomStringsDialog::clearEditors() +{ + util::SignalBlockers blockers(this); + + ui->keyLineEdit->clear(); + ui->valueLineEdit->clear(); + ui->stringTextEdit->clear(); +} + +void CustomStringsDialog::on_addButton_clicked() +{ + auto e = editorsToEntry(); + SCP_string err; + if (!_model->add(e, &err)) { + QMessageBox::warning(this, tr("Invalid Entry"), QString::fromStdString(err)); + return; + } + + refreshTable(); + //selectRow(_tableModel->rowCount() - 1); + clearEditors(); +} + +void CustomStringsDialog::on_updateButton_clicked() +{ + const auto idx = ui->stringsTableView->currentIndex(); + if (!idx.isValid()) + return; + + auto e = editorsToEntry(); + SCP_string err; + if (!_model->updateAt(static_cast(idx.row()), e, &err)) { + QMessageBox::warning(this, tr("Invalid Entry"), QString::fromStdString(err)); + return; + } + + // Reflect updated values in table + _tableModel->item(idx.row(), ColKey)->setText(QString::fromStdString(e.name)); + _tableModel->item(idx.row(), ColValue)->setText(QString::fromStdString(e.value)); +} + +void CustomStringsDialog::on_removeButton_clicked() +{ + const auto idx = ui->stringsTableView->currentIndex(); + if (!idx.isValid()) + return; + + const auto key = _tableModel->item(idx.row(), ColKey)->text(); + if (QMessageBox::question(this, tr("Remove Entry"), tr("Remove key \"%1\"?").arg(key)) != QMessageBox::Yes) { + return; + } + + if (_model->removeAt(static_cast(idx.row()))) { + refreshTable(); + selectRow(std::min(idx.row(), _tableModel->rowCount() - 1)); + } +} + +void CustomStringsDialog::on_okAndCancelButtons_accepted() +{ + accept(); // Mission Specs will read model->items() and commit during its own Apply +} + +void CustomStringsDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.h b/qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.h new file mode 100644 index 00000000000..89bfca516b9 --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionSpecs/CustomStringsDialog.h @@ -0,0 +1,58 @@ +#pragma once + +#include "mission/dialogs/MissionSpecs/CustomStringsDialogModel.h" + +#include + +#include +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class CustomStringsDialog; +} + +class CustomStringsDialog final : public QDialog { + Q_OBJECT + public: + + explicit CustomStringsDialog(QWidget* parent, EditorViewport* viewport); + ~CustomStringsDialog() override; + + void accept() override; + void reject() override; + + void setInitial(const SCP_vector& items); + + const SCP_vector& items() const { return _model->items(); } + + protected: + void closeEvent(QCloseEvent*) override; + + private slots: + // Top-row buttons + void on_addButton_clicked(); + void on_updateButton_clicked(); + void on_removeButton_clicked(); + + // Dialog buttons + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + private: // NOLINT(readability-redundant-access-specifiers) + void buildView(); + void refreshTable(); + void selectRow(int row); + void loadRowIntoEditors(int row); + custom_string editorsToEntry() const; + void clearEditors(); + + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + QStandardItemModel* _tableModel = nullptr; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.cpp new file mode 100644 index 00000000000..99527d691ec --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.cpp @@ -0,0 +1,180 @@ +#include "CustomWingNamesDialog.h" + +#include "ui_CustomWingNamesDialog.h" +#include + +#include + +namespace fso::fred::dialogs { + +CustomWingNamesDialog::CustomWingNamesDialog(QWidget* parent, EditorViewport* viewport) : + QDialog(parent), ui(new Ui::CustomWingNamesDialog()), _model(new CustomWingNamesDialogModel(this, viewport)), + _viewport(viewport) { + ui->setupUi(this); + + connect(_model.get(), &AbstractDialogModel::modelChanged, this, &CustomWingNamesDialog::updateUi); + + updateUi(); + + // Resize the dialog to the minimum size + resize(QDialog::sizeHint()); +} + +CustomWingNamesDialog::~CustomWingNamesDialog() = default; + +void CustomWingNamesDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void CustomWingNamesDialog::reject() +{ + // Custom reject or close logic because we need to handle talkback to Mission Specs + if (_model->query_modified()) { + auto button = _viewport->dialogProvider->showButtonDialog(fso::fred::DialogType::Question, + "Changes detected", + "Do you want to keep your changes?", + {fso::fred::DialogButton::Yes, fso::fred::DialogButton::No, fso::fred::DialogButton::Cancel}); + + if (button == fso::fred::DialogButton::Yes) { + accept(); +} + if (button == fso::fred::DialogButton::No) { + _model->reject(); + QDialog::reject(); // actually close +} + } else { + _model->reject(); + QDialog::reject(); + } +} + +void CustomWingNamesDialog::closeEvent(QCloseEvent * e) { + reject(); + e->ignore(); // Don't let the base class close the window +} + +void CustomWingNamesDialog::setInitialStartingWings(const std::array& startingWings) { + _model->setInitialStartingWings(startingWings); + updateUi(); +} + +void CustomWingNamesDialog::setInitialSquadronWings(const std::array& squadronWings) { + _model->setInitialSquadronWings(squadronWings); + updateUi(); +} + +void CustomWingNamesDialog::setInitialTvTWings(const std::array& tvtWings) { + _model->setInitialTvTWings(tvtWings); + updateUi(); +} + +const std::array& CustomWingNamesDialog::getStartingWings() const { + return _model->getStartingWings(); +} + +const std::array& CustomWingNamesDialog::getSquadronWings() const { + return _model->getSquadronWings(); +} + +const std::array& CustomWingNamesDialog::getTvTWings() const { + return _model->getTvTWings(); +} + +void CustomWingNamesDialog::updateUi() { + util::SignalBlockers blockers(this); + + // Update starting wings + ui->startingWing_1->setText(_model->getStartingWing(0).c_str()); + ui->startingWing_2->setText(_model->getStartingWing(1).c_str()); + ui->startingWing_3->setText(_model->getStartingWing(2).c_str()); + + // Update squadron wings + ui->squadronWing_1->setText(_model->getSquadronWing(0).c_str()); + ui->squadronWing_2->setText(_model->getSquadronWing(1).c_str()); + ui->squadronWing_3->setText(_model->getSquadronWing(2).c_str()); + ui->squadronWing_4->setText(_model->getSquadronWing(3).c_str()); + ui->squadronWing_5->setText(_model->getSquadronWing(4).c_str()); + + // Update dogfight wings + ui->dogfightWing_1->setText(_model->getTvTWing(0).c_str()); + ui->dogfightWing_2->setText(_model->getTvTWing(1).c_str()); +} + +void CustomWingNamesDialog::startingWingChanged(const QString & str, int index) { + _model->setStartingWing(str.toUtf8().constData(), index); +} + +void CustomWingNamesDialog::squadronWingChanged(const QString & str, int index) { + _model->setSquadronWing(str.toUtf8().constData(), index); +} + +void CustomWingNamesDialog::dogfightWingChanged(const QString & str, int index) { + _model->setTvTWing(str.toUtf8().constData(), index); +} + +void CustomWingNamesDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void CustomWingNamesDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void CustomWingNamesDialog::on_startingWing_1_textChanged(const QString & text) +{ + startingWingChanged(text, 0); +} + +void CustomWingNamesDialog::on_startingWing_2_textChanged(const QString & text) +{ + startingWingChanged(text, 1); +} + +void CustomWingNamesDialog::on_startingWing_3_textChanged(const QString & text) +{ + startingWingChanged(text, 2); +} + +void CustomWingNamesDialog::on_squadronWing_1_textChanged(const QString & text) +{ + squadronWingChanged(text, 0); +} + +void CustomWingNamesDialog::on_squadronWing_2_textChanged(const QString & text) +{ + squadronWingChanged(text, 1); +} + +void CustomWingNamesDialog::on_squadronWing_3_textChanged(const QString & text) +{ + squadronWingChanged(text, 2); +} + +void CustomWingNamesDialog::on_squadronWing_4_textChanged(const QString & text) +{ + squadronWingChanged(text, 3); +} + +void CustomWingNamesDialog::on_squadronWing_5_textChanged(const QString & text) +{ + squadronWingChanged(text, 4); +} + +void CustomWingNamesDialog::on_dogfightWing_1_textChanged(const QString & text) +{ + dogfightWingChanged(text, 0); +} + +void CustomWingNamesDialog::on_dogfightWing_2_textChanged(const QString & text) +{ + dogfightWingChanged(text, 1); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.h b/qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.h new file mode 100644 index 00000000000..7265b36b564 --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionSpecs/CustomWingNamesDialog.h @@ -0,0 +1,62 @@ +#include +#include + +#include + +#include "mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.h" + +namespace fso::fred::dialogs { + +namespace Ui { +class CustomWingNamesDialog; +} + +class CustomWingNamesDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CustomWingNamesDialog(QWidget* parent, EditorViewport* viewport); + ~CustomWingNamesDialog() override; + + void accept() override; + void reject() override; + + void setInitialStartingWings(const std::array& startingWings); + void setInitialSquadronWings(const std::array& squadronWings); + void setInitialTvTWings(const std::array& tvtWings); + + const std::array& getStartingWings() const; + const std::array& getSquadronWings() const; + const std::array& getTvTWings() const; + +protected: + void closeEvent(QCloseEvent* e) override; + +private slots: + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + void on_startingWing_1_textChanged(const QString& text); + void on_startingWing_2_textChanged(const QString& text); + void on_startingWing_3_textChanged(const QString& text); + void on_squadronWing_1_textChanged(const QString& text); + void on_squadronWing_2_textChanged(const QString& text); + void on_squadronWing_3_textChanged(const QString& text); + void on_squadronWing_4_textChanged(const QString& text); + void on_squadronWing_5_textChanged(const QString& text); + void on_dogfightWing_1_textChanged(const QString& text); + void on_dogfightWing_2_textChanged(const QString& text); + +private: // NOLINT(readability-redundant-access-specifiers) + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + void updateUi(); + + void startingWingChanged(const QString&, int); + void squadronWingChanged(const QString&, int); + void dogfightWingChanged(const QString&, int); +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.cpp new file mode 100644 index 00000000000..52bf7d6be02 --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.cpp @@ -0,0 +1,225 @@ +#include "SoundEnvironmentDialog.h" + +#include "ui_SoundEnvironmentDialog.h" + +#include "cfile/cfile.h" +#include "sound/audiostr.h" +#include "sound/ds.h" +#include "sound/sound.h" + +#include + +#include +#include +#include + +using namespace fso::fred::dialogs; + +SoundEnvironmentDialog::SoundEnvironmentDialog(QWidget* parent, EditorViewport* viewport) + : QDialog(parent), ui(std::make_unique()), + _model(new SoundEnvironmentDialogModel(this, viewport)), _viewport(viewport) +{ + ui->setupUi(this); + + populatePresets(); + + applyPresetFields(); +} + +SoundEnvironmentDialog::~SoundEnvironmentDialog() = default; + +void SoundEnvironmentDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close + + closeWave(); + disableEnvPreview(); +} + +void SoundEnvironmentDialog::reject() +{ + // Custom reject or close logic because we need to handle talkback to Mission Specs + if (_model->query_modified()) { + auto button = _viewport->dialogProvider->showButtonDialog(fso::fred::DialogType::Question, + "Changes detected", + "Do you want to keep your changes?", + {fso::fred::DialogButton::Yes, fso::fred::DialogButton::No, fso::fred::DialogButton::Cancel}); + + if (button == fso::fred::DialogButton::Yes) { + accept(); + } + if (button == fso::fred::DialogButton::No) { + _model->reject(); + QDialog::reject(); // actually close + + closeWave(); + disableEnvPreview(); + } + } else { + _model->reject(); + QDialog::reject(); + + closeWave(); + disableEnvPreview(); + } +} + +void SoundEnvironmentDialog::closeEvent(QCloseEvent* e) +{ + reject(); + closeWave(); + disableEnvPreview(); + e->ignore(); // Don't let the base class close the window +} + +void SoundEnvironmentDialog::enableOrDisableFields() +{ + const bool enabled = ui->environmentComboBox->currentIndex() > 0; + ui->volumeSpinBox->setEnabled(enabled); + ui->dampingSpinBox->setEnabled(enabled); + ui->decaySpinBox->setEnabled(enabled); + ui->browseButton->setEnabled(enabled); + ui->playButton->setEnabled(enabled && _waveId >= 0); +} + +void SoundEnvironmentDialog::populatePresets() +{ + util::SignalBlockers blockers(this); + + ui->environmentComboBox->clear(); + ui->environmentComboBox->addItem(QString("")); // index 0 = none + for (auto& preset : EFX_presets) { + ui->environmentComboBox->addItem(QString::fromStdString(preset.name)); + } +} + +void SoundEnvironmentDialog::applyPresetFields() +{ + util::SignalBlockers blockers(this); + + int presetIndex = _model->getId(); + + if (presetIndex >= 0) { + const auto& p = EFX_presets[presetIndex]; + ui->volumeSpinBox->setValue(p.flGain); + ui->dampingSpinBox->setValue(p.flDecayHFRatio); + ui->decaySpinBox->setValue(p.flDecayTime); + } else { // + ui->volumeSpinBox->setValue(0.0); + ui->dampingSpinBox->setValue(0.1); + ui->decaySpinBox->setValue(0.1); + } + + enableOrDisableFields(); +} + +void SoundEnvironmentDialog::setInitial(const sound_env& env) +{ + _model->setInitial(env); + + util::SignalBlockers blockers(this); + + ui->environmentComboBox->setCurrentIndex(env.id + 1); + ui->volumeSpinBox->setValue(env.volume); + ui->dampingSpinBox->setValue(env.damping); + ui->decaySpinBox->setValue(env.decay); + + enableOrDisableFields(); +} + +sound_env SoundEnvironmentDialog::items() const +{ + return _model->params(); +} + +void SoundEnvironmentDialog::on_environmentComboBox_currentIndexChanged(int index) +{ + _model->setId(index - 1); + applyPresetFields(); +} + +void SoundEnvironmentDialog::on_volumeSpinBox_valueChanged(double value) +{ + _model->setVolume(static_cast(value)); +} + +void SoundEnvironmentDialog::on_dampingSpinBox_valueChanged(double value) +{ + _model->setDamping(static_cast(value)); +} + +void SoundEnvironmentDialog::on_decaySpinBox_valueChanged(double value) +{ + _model->setDecay(static_cast(value)); +} + +void SoundEnvironmentDialog::on_browseButton_clicked() +{ + closeWave(); + + const int pushed = cfile_push_chdir(CF_TYPE_DATA); + const QString filter = "Voice Files (*.ogg *.wav);;Ogg Vorbis Files (*.ogg);;Wave Files (*.wav)"; + const auto path = QFileDialog::getOpenFileName(this, tr("Choose sound"), QString(), filter); + + if (!path.isEmpty()) { + const QString justName = QFileInfo(path).fileName(); + _waveId = audiostream_open(justName.toUtf8().constData(), ASF_SOUNDFX); + ui->fileSelectionLabel->setText(justName); + } + if (!pushed) + cfile_pop_dir(); + + enableOrDisableFields(); +} + +void SoundEnvironmentDialog::on_playButton_clicked() +{ + if (!sound_env_supported()) { + QMessageBox::warning(this, tr("Error"), tr("Sound environment effects are not available! Unable to preview!")); + return; + } + if (_waveId < 0) { + on_browseButton_clicked(); + if (_waveId < 0) + return; + } + + // Build a temp env from current fields and set it active for preview. + sound_env temp = _model->params(); + + if (temp.id >= 0) { + sound_env_set(&temp); + } else { + sound_env_disable(); + } + + audiostream_play(_waveId, 1.0f, 0); // simple preview +} + +void SoundEnvironmentDialog::on_okAndCancelButtons_accepted() +{ + accept(); // Mission Specs will read model->items() and commit during its own Apply +} + +void SoundEnvironmentDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void SoundEnvironmentDialog::closeWave() +{ + if (_waveId >= 0) { + audiostream_close_file(_waveId, false); + ui->fileSelectionLabel->setText(QString("")); // clear label + _waveId = -1; + } +} + +void SoundEnvironmentDialog::disableEnvPreview() +{ + sound_env_disable(); +} diff --git a/qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.h b/qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.h new file mode 100644 index 00000000000..ffa433ecb18 --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionSpecs/SoundEnvironmentDialog.h @@ -0,0 +1,60 @@ +#pragma once +#include "mission/dialogs/MissionSpecs/SoundEnvironmentDialogModel.h" + +#include + +#include "sound/sound.h" // for sound_env + +#include + +class EditorViewport; + +namespace fso::fred::dialogs { + +namespace Ui { +class SoundEnvironmentDialog; +} + +class SoundEnvironmentDialog final : public QDialog { + Q_OBJECT + public: + SoundEnvironmentDialog(QWidget* parent, EditorViewport* viewport); + ~SoundEnvironmentDialog() override; + + void accept() override; + void reject() override; + + void setInitial(const sound_env& env); // seed from Mission Specs + sound_env items() const; // pull back edited data + + protected: + void closeEvent(QCloseEvent* e) override; + + private slots: + void on_environmentComboBox_currentIndexChanged(int index); + void on_volumeSpinBox_valueChanged(double value); + void on_dampingSpinBox_valueChanged(double value); + void on_decaySpinBox_valueChanged(double value); + + void on_browseButton_clicked(); + void on_playButton_clicked(); + + // Dialog buttons + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + private: // NOLINT(readability-redundant-access-specifiers) + void enableOrDisableFields(); + void populatePresets(); + void applyPresetFields(); + void closeWave(); + static void disableEnvPreview(); + + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + int _waveId = -1; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/CustomDataDialog.ui b/qtfred/ui/CustomDataDialog.ui new file mode 100644 index 00000000000..7e752796fa0 --- /dev/null +++ b/qtfred/ui/CustomDataDialog.ui @@ -0,0 +1,159 @@ + + + fso::fred::dialogs::CustomDataDialog + + + Qt::WindowModal + + + + 0 + 0 + 650 + 335 + + + + Custom Data + + + true + + + true + + + + 6 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Key + + + + + + + + + + Value + + + + + + + + + + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + Add + + + + + + + Remove + + + + + + + Update + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 0 + 0 + + + + + + + + + + + 0 + 0 + + + + Qt::LeftToRight + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + false + + + + + + + + diff --git a/qtfred/ui/CustomStringsDialog.ui b/qtfred/ui/CustomStringsDialog.ui new file mode 100644 index 00000000000..bac9b607a32 --- /dev/null +++ b/qtfred/ui/CustomStringsDialog.ui @@ -0,0 +1,208 @@ + + + fso::fred::dialogs::CustomStringsDialog + + + Qt::WindowModal + + + + 0 + 0 + 647 + 455 + + + + Edit Mission Custom Strings + + + true + + + true + + + + 6 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Key + + + + + + + + + + Value + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 0 + 0 + + + + + + + + + + Text + + + + + + + + + + + + QLayout::SetDefaultConstraint + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Update + + + + + + + + 0 + 0 + + + + Add + + + + + + + Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 0 + 0 + + + + Qt::LeftToRight + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + false + + + + + + + + diff --git a/qtfred/ui/CustomWingNamesDialog.ui b/qtfred/ui/CustomWingNamesDialog.ui index 51f16a5f6ec..52c52a63277 100644 --- a/qtfred/ui/CustomWingNamesDialog.ui +++ b/qtfred/ui/CustomWingNamesDialog.ui @@ -2,12 +2,15 @@ fso::fred::dialogs::CustomWingNamesDialog + + Qt::WindowModal + 0 0 - 580 - 104 + 564 + 101 @@ -106,7 +109,7 @@ - + Qt::Vertical @@ -121,22 +124,5 @@ - - - buttonBox - accepted() - fso::fred::dialogs::CustomWingNamesDialog - accept() - - - 603 - 50 - - - 324 - 50 - - - - + diff --git a/qtfred/ui/MissionSpecDialog.ui b/qtfred/ui/MissionSpecDialog.ui index 1cd7c7554e1..8d4552ed8f7 100644 --- a/qtfred/ui/MissionSpecDialog.ui +++ b/qtfred/ui/MissionSpecDialog.ui @@ -2,6 +2,9 @@ fso::fred::dialogs::MissionSpecDialog + + Qt::WindowModal + 0 @@ -16,6 +19,9 @@ 595 + + Qt::NoFocus + Mission Specs @@ -224,6 +230,9 @@ 0 + + 16777215 + 3 @@ -241,7 +250,7 @@ -1 - 999 + 16777215 -1 @@ -604,6 +613,9 @@ QAbstractSpinBox::NoButtons + + 16777215 + @@ -716,17 +728,10 @@ - - - - Sound Environment - - - - + 6 @@ -746,7 +751,7 @@ - + 0 @@ -782,183 +787,25 @@ 3 - - - All Teams at War - - - m_flagGroup - - - - - - - Red Alert Mission - - - m_flagGroup - - - - - - - Scramble Mission - - - m_flagGroup - - - - - - - Disallow Promotions/Badges - - - m_flagGroup - - - - - - - Disable Built-in Messages - - - m_flagGroup - - - - - - - Disable Built-in Command Messages - - - m_flagGroup - - - - - - - No Traitor - - - m_flagGroup - - - - - - - All Ships Beam-Freed By Default - - - m_flagGroup - - - - - - - No Briefing - - - m_flagGroup - - - - - - - Toggle Debriefing (On/Off) - - - m_flagGroup - - - - - - - Use Autopilot Cinematics - - - m_flagGroup - - - - - - - Deactivate Hardcoded Autopilot - - - m_flagGroup - - - - - - - Player Starts under AI Control (NO MULTI) - - - m_flagGroup - - - - - - - Player Starts in Chase View - - - m_flagGroup - - - - - - - 2D Mission + + + + 0 + 0 + - - m_flagGroup - - - - - - - Toggle Showing Goals In Briefing + + + 0 + 0 + - - m_flagGroup - - - - - - - Mission End to Mainhall + + true - - m_flagGroup - - - - - - - Preload Subspace Tunnel + + true - - m_flagGroup - @@ -994,6 +841,50 @@ 3 + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Sound Environment + + + + + + + Custom Data + + + + + + + Custom Strings + + + + + @@ -1035,32 +926,18 @@ + + + fso::fred::FlagListWidget + QWidget +
ui/widgets/FlagList.h
+ 1 +
+
- - - dialogButtonBox - accepted() - fso::fred::dialogs::MissionSpecDialog - accept() - - - 628 - 19 - - - 357 - 297 - - - - + - - - false - - - + diff --git a/qtfred/ui/SoundEnvironmentDialog.ui b/qtfred/ui/SoundEnvironmentDialog.ui new file mode 100644 index 00000000000..f88e0c9caf6 --- /dev/null +++ b/qtfred/ui/SoundEnvironmentDialog.ui @@ -0,0 +1,189 @@ + + + fso::fred::dialogs::SoundEnvironmentDialog + + + Qt::WindowModal + + + + 0 + 0 + 376 + 200 + + + + Sound Environment + + + true + + + true + + + + 20 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Environment + + + + + + + + + + Volume + + + + + + + 6 + + + 1.000000000000000 + + + 0.001000000000000 + + + + + + + Damping + + + + + + + 0.100000000000000 + + + 2.000000000000000 + + + 0.010000000000000 + + + + + + + Decay Time + + + + + + + 0.100000000000000 + + + 20.000000000000000 + + + + + + + + + Preview + + + + + + <No File Selected> + + + + + + + + + Browse + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + :/images/play.bmp:/images/play.bmp + + + + + + + + + + + + + + Qt::Vertical + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + + + + From 6c101fe52a0890f1733c17a416ab896f59926b44 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sun, 24 Aug 2025 09:10:40 -0500 Subject: [PATCH 411/466] QtFRED Fiction Viewer Dialog (#6965) * refactory qtfred fiction viewer dialog * do this more efficiently --- .../dialogs/FictionViewerDialogModel.cpp | 147 +++++++++++------ .../dialogs/FictionViewerDialogModel.h | 61 +++---- qtfred/src/ui/dialogs/FictionViewerDialog.cpp | 156 +++++++++--------- qtfred/src/ui/dialogs/FictionViewerDialog.h | 40 ++--- qtfred/ui/FictionViewerDialog.ui | 8 +- 5 files changed, 226 insertions(+), 186 deletions(-) diff --git a/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp b/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp index 774d85ef0cb..f02f91c6255 100644 --- a/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp +++ b/qtfred/src/mission/dialogs/FictionViewerDialogModel.cpp @@ -1,10 +1,7 @@ -#include #include #include "mission/dialogs/FictionViewerDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { FictionViewerDialogModel::FictionViewerDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { @@ -13,78 +10,136 @@ FictionViewerDialogModel::FictionViewerDialogModel(QObject* parent, EditorViewpo } bool FictionViewerDialogModel::apply() { - // store the fields in the data structure - fiction_viewer_stage *stagep = &Fiction_viewer_stages.at(0); - if (_storyFile.empty()) { - Fiction_viewer_stages.erase(Fiction_viewer_stages.begin()); - stagep = nullptr; + // if the story file for the current stage is empty, treat as no fiction viewer stage + // currently we only support one stage, so just check the first one + const auto& stage = _fictionViewerStages.at(0); + const bool empty = stage.story_filename[0] == '\0'; + + if (empty) { + _fictionViewerStages.clear(); Mission_music[SCORE_FICTION_VIEWER] = -1; } else { - strcpy_s(stagep->story_filename, _storyFile.c_str()); - strcpy_s(stagep->font_filename, _fontFile.c_str()); - strcpy_s(stagep->voice_filename, _voiceFile.c_str()); - Mission_music[SCORE_FICTION_VIEWER] = _fictionMusic - 1; + // Keep whatever you’ve edited in _fictionViewerStages + Mission_music[SCORE_FICTION_VIEWER] = _fictionMusic; // -1 for none is valid } + + // Commit working copy to mission + Fiction_viewer_stages = _fictionViewerStages; return true; } void FictionViewerDialogModel::reject() { - // nothing to do if the dialog is created each time it's opened + // nothing to do } void FictionViewerDialogModel::initializeData() { + + _fictionViewerStages = Fiction_viewer_stages; + // make sure we have at least one stage - if (Fiction_viewer_stages.empty()) { + if (_fictionViewerStages.empty()) { fiction_viewer_stage stage; memset(&stage, 0, sizeof(fiction_viewer_stage)); stage.formula = Locked_sexp_true; - Fiction_viewer_stages.push_back(stage); + _fictionViewerStages.push_back(stage); } - _musicOptions.emplace_back("None", 0); - for (int i = 0; i < (int)Spooled_music.size(); ++i) { - _musicOptions.emplace_back(Spooled_music[i].name, i + 1); // + 1 because option 0 is None + _musicOptions.emplace_back("None", -1); + for (int i = 0; i < static_cast(Spooled_music.size()); ++i) { + _musicOptions.emplace_back(Spooled_music[i].name, i); } - - // init fields based on first fiction viewer stage - const fiction_viewer_stage *stagep = &Fiction_viewer_stages.at(0); - _storyFile = stagep->story_filename; - _fontFile = stagep->font_filename; - _voiceFile = stagep->voice_filename; - - // initialize file name length limits - _maxStoryFileLength = sizeof(stagep->story_filename) - 1; - _maxFontFileLength = sizeof(stagep->font_filename) - 1; - _maxVoiceFileLength = sizeof(stagep->voice_filename) - 1; // music is managed through the mission - _fictionMusic = Mission_music[SCORE_FICTION_VIEWER] + 1; + _fictionMusic = Mission_music[SCORE_FICTION_VIEWER]; +} - modelChanged(); +const SCP_vector>& FictionViewerDialogModel::getMusicOptions() +{ + return _musicOptions; } -void FictionViewerDialogModel::setFictionMusic(int fictionMusic) { - Assert(fictionMusic >= 0); - Assert(fictionMusic <= (int)Spooled_music.size()); - modify(_fictionMusic, fictionMusic); +SCP_string FictionViewerDialogModel::getStoryFile() const +{ + return _fictionViewerStages[_fictionViewerStageIndex].story_filename; } -bool FictionViewerDialogModel::query_modified() const { - Assert(!Fiction_viewer_stages.empty()); +void FictionViewerDialogModel::setStoryFile(const SCP_string& storyFile) +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; - const fiction_viewer_stage *stagep = &Fiction_viewer_stages.at(0); - - return strcmp(_storyFile.c_str(), stagep->story_filename) != 0 - || strcmp(_fontFile.c_str(), stagep->font_filename) != 0 - || strcmp(_voiceFile.c_str(), stagep->voice_filename) != 0 - || _fictionMusic != (Mission_music[SCORE_FICTION_VIEWER] + 1); + if (strcmp(stage.story_filename, storyFile.c_str()) != 0) { + strcpy_s(stage.story_filename, storyFile.c_str()); + set_modified(); + } +} + +SCP_string FictionViewerDialogModel::getFontFile() const +{ + return _fictionViewerStages[_fictionViewerStageIndex].font_filename; +} + +void FictionViewerDialogModel::setFontFile(const SCP_string& fontFile) +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + if (stricmp(stage.font_filename, fontFile.c_str()) != 0) { + strcpy_s(stage.font_filename, fontFile.c_str()); + set_modified(); + } } -bool FictionViewerDialogModel::hasMultipleStages() const { - return Fiction_viewer_stages.size() > 1; +SCP_string FictionViewerDialogModel::getVoiceFile() const +{ + return _fictionViewerStages[_fictionViewerStageIndex].voice_filename; } +void FictionViewerDialogModel::setVoiceFile(const SCP_string& voiceFile) +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + if (stricmp(stage.voice_filename, voiceFile.c_str()) != 0) { + strcpy_s(stage.voice_filename, voiceFile.c_str()); + set_modified(); + } +} + +int FictionViewerDialogModel::getFictionMusic() const +{ + // TODO research how music is set for multiple fiction viewer stages so we + // can return the correct index when multiple stages is fully supported + return _fictionMusic; +} + +void FictionViewerDialogModel::setFictionMusic(int fictionMusic) { + bool valid = fictionMusic == -1 || SCP_vector_inbounds(Spooled_music, fictionMusic); + Assertion(valid, + "Fiction music index out of bounds: %d (max %d)", + fictionMusic, + static_cast(Spooled_music.size())); + + modify(_fictionMusic, fictionMusic); } + +int FictionViewerDialogModel::getMaxStoryFileLength() const +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + return sizeof(stage.story_filename) - 1; +} + +int FictionViewerDialogModel::getMaxFontFileLength() const +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + return sizeof(stage.font_filename) - 1; } + +int FictionViewerDialogModel::getMaxVoiceFileLength() const +{ + auto& stage = _fictionViewerStages[_fictionViewerStageIndex]; + + return sizeof(stage.voice_filename) - 1; } + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/FictionViewerDialogModel.h b/qtfred/src/mission/dialogs/FictionViewerDialogModel.h index 21c9350a33c..3e35ce0c309 100644 --- a/qtfred/src/mission/dialogs/FictionViewerDialogModel.h +++ b/qtfred/src/mission/dialogs/FictionViewerDialogModel.h @@ -2,61 +2,40 @@ #include "mission/dialogs/AbstractDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +#include + +namespace fso::fred::dialogs { class FictionViewerDialogModel: public AbstractDialogModel { Q_OBJECT public: - struct MusicOptionElement { - SCP_string name; - int id = -1; - - MusicOptionElement(const char* Name, int Id) - : name(Name), id(Id) { - } - }; - FictionViewerDialogModel(QObject* parent, EditorViewport* viewport); - ~FictionViewerDialogModel() override = default; - bool apply() override; + bool apply() override; void reject() override; - const SCP_string& getStoryFile() const { return _storyFile; } - const SCP_string& getFontFile() const { return _fontFile; } - const SCP_string& getVoiceFile() const { return _voiceFile; } - int getFictionMusic() const { return _fictionMusic; } - const SCP_vector& getMusicOptions() const { return _musicOptions; } + const SCP_vector>& getMusicOptions(); - void setStoryFile(const SCP_string& storyFile) { modify(_storyFile, storyFile); } - void setFontFile(const SCP_string& fontFile) { modify(_fontFile, fontFile); } - void setVoiceFile(const SCP_string& voiceFile) { modify(_voiceFile, voiceFile); } - // TODO input validation on passed in fictionMusic? + SCP_string getStoryFile() const; + void setStoryFile(const SCP_string& storyFile); + SCP_string getFontFile() const; + void setFontFile(const SCP_string& fontFile); + SCP_string getVoiceFile() const; + void setVoiceFile(const SCP_string& voiceFile); + int getFictionMusic() const; void setFictionMusic(int fictionMusic); - int getMaxStoryFileLength() const { return _maxStoryFileLength; } - int getMaxFontFileLength() const { return _maxFontFileLength; } - int getMaxVoiceFileLength() const { return _maxVoiceFileLength; } - - bool query_modified() const; - - bool hasMultipleStages() const; + int getMaxStoryFileLength() const; + int getMaxFontFileLength() const; + int getMaxVoiceFileLength() const; private: void initializeData(); - - SCP_string _storyFile; - SCP_string _fontFile; - SCP_string _voiceFile; - int _fictionMusic; - SCP_vector _musicOptions; - - int _maxStoryFileLength, _maxFontFileLength, _maxVoiceFileLength; + SCP_vector _fictionViewerStages; + int _fictionViewerStageIndex = 0; + int _fictionMusic = -1; + SCP_vector> _musicOptions; }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/FictionViewerDialog.cpp b/qtfred/src/ui/dialogs/FictionViewerDialog.cpp index 3bf84192e3c..f69944f18e1 100644 --- a/qtfred/src/ui/dialogs/FictionViewerDialog.cpp +++ b/qtfred/src/ui/dialogs/FictionViewerDialog.cpp @@ -4,120 +4,126 @@ #include "ui/util/SignalBlockers.h" #include "ui_FictionViewerDialog.h" #include "mission/util.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { FictionViewerDialog::FictionViewerDialog(FredView* parent, EditorViewport* viewport) : - QDialog(parent), - _viewport(viewport), - _editor(viewport->editor), - ui(new Ui::FictionViewerDialog()), - _model(new FictionViewerDialogModel(this, viewport)) { + QDialog(parent), _viewport(viewport), ui(new Ui::FictionViewerDialog()), _model(new FictionViewerDialogModel(this, viewport)) +{ ui->setupUi(this); - ui->storyFileEdit->setMaxLength(_model->getMaxStoryFileLength()); - ui->fontFileEdit->setMaxLength(_model->getMaxFontFileLength()); - ui->voiceFileEdit->setMaxLength(_model->getMaxVoiceFileLength()); - - connect(this, &QDialog::accepted, _model.get(), &FictionViewerDialogModel::apply); - connect(ui->dialogButtonBox, &QDialogButtonBox::rejected, this, &FictionViewerDialog::rejectHandler); - - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &FictionViewerDialog::updateUI); - - connect(ui->storyFileEdit, &QLineEdit::textChanged, this, &FictionViewerDialog::storyFileTextChanged); - connect(ui->fontFileEdit, &QLineEdit::textChanged, this, &FictionViewerDialog::fontFileTextChanged); - connect(ui->voiceFileEdit, &QLineEdit::textChanged, this, &FictionViewerDialog::voiceFileTextChanged); - connect(ui->musicComboBox, static_cast(&QComboBox::currentIndexChanged), this, &FictionViewerDialog::musicSelectionChanged); - // Initial set up of the UI - updateUI(); + initializeUi(); + updateUi(); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); - if (_model->hasMultipleStages()) { + // Fiction viewer can have multiple *conditional* stages but only ever displays one during a mission. + // So in order to properly handle multiple stages in the editor we will need to add a formula editor + // to the dialog like goals or cutscenes. It looks like formulas already saved/parsed in the mission file + // so this is just an editor UI limitation maybe? This should be handled in the next pass at the FV dialog + // because the model doesn't yet support reading/writing the formula + /*if (_model->hasMultipleStages()) { viewport->dialogProvider->showButtonDialog(DialogType::Information, "Multiple stages detected", "This mission has multiple fiction viewer stages defined. Currently, qtFRED will only allow you to edit the first stage.", { DialogButton::Ok}); - } -} -FictionViewerDialog::~FictionViewerDialog() { + }*/ } +FictionViewerDialog::~FictionViewerDialog() = default; -void FictionViewerDialog::updateMusicComboBox() { - ui->musicComboBox->clear(); - - for (const auto& el : _model->getMusicOptions()) { - ui->musicComboBox->addItem(QString::fromStdString(el.name), QVariant(el.id)); +void FictionViewerDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); } + // else: validation failed, don’t close +} - if (ui->musicComboBox->count() > 0) { - ui->musicComboBox->setEnabled(true); - int selectedIndex = -1; - for (int i = 0; i < ui->musicComboBox->count(); ++i) { - const int itemId = ui->musicComboBox->itemData(i).value(); - if (itemId == _model->getFictionMusic()) { - selectedIndex = i; - break; - } - } - ui->musicComboBox->setCurrentIndex(selectedIndex); - } else { - ui->musicComboBox->setEnabled(false); +void FictionViewerDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close } + // else: do nothing, don't close } -void FictionViewerDialog::updateUI() { - util::SignalBlockers blockers(this); + +void FictionViewerDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window +} + +void FictionViewerDialog::initializeUi() +{ + ui->storyFileEdit->setMaxLength(_model->getMaxStoryFileLength()); + ui->fontFileEdit->setMaxLength(_model->getMaxFontFileLength()); + ui->voiceFileEdit->setMaxLength(_model->getMaxVoiceFileLength()); updateMusicComboBox(); +} + +void FictionViewerDialog::updateUi() { + util::SignalBlockers blockers(this); ui->storyFileEdit->setText(QString::fromStdString(_model->getStoryFile())); ui->fontFileEdit->setText(QString::fromStdString(_model->getFontFile())); ui->voiceFileEdit->setText(QString::fromStdString(_model->getVoiceFile())); + ui->musicComboBox->setCurrentIndex(ui->musicComboBox->findData(_model->getFictionMusic())); } -void FictionViewerDialog::musicSelectionChanged(int index) { - if (index >= 0) { - int itemId = ui->musicComboBox->itemData(index).value(); - _model->setFictionMusic(itemId); +void FictionViewerDialog::updateMusicComboBox() +{ + util::SignalBlockers blockers(this); + + ui->musicComboBox->clear(); + + const auto& musicOptions = _model->getMusicOptions(); + + if (musicOptions.empty()) { + ui->musicComboBox->setEnabled(false); + return; } -} -void FictionViewerDialog::storyFileTextChanged() { - _model->setStoryFile(ui->storyFileEdit->text().toStdString()); + ui->musicComboBox->setEnabled(true); + for (const auto& option : musicOptions) { + ui->musicComboBox->addItem(QString::fromStdString(option.first), option.second); + } } -void FictionViewerDialog::fontFileTextChanged() { - _model->setFontFile(ui->fontFileEdit->text().toStdString()); +void FictionViewerDialog::on_okAndCancelButtons_accepted() +{ + accept(); } -void FictionViewerDialog::voiceFileTextChanged() { - _model->setVoiceFile(ui->voiceFileEdit->text().toStdString()); +void FictionViewerDialog::on_okAndCancelButtons_rejected() +{ + reject(); } -void FictionViewerDialog::keyPressEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Escape) { - // Instead of calling reject when we close a dialog it should try to close the window which will will allow the - // user to save unsaved changes - event->ignore(); - this->close(); - return; - } - QDialog::keyPressEvent(event); +void FictionViewerDialog::on_storyFileEdit_textChanged(const QString& text) +{ + _model->setStoryFile(text.toUtf8().constData()); } -void FictionViewerDialog::closeEvent(QCloseEvent* e) { - if (!rejectOrCloseHandler(this, _model.get(), _viewport)) { - e->ignore(); - }; -} -void FictionViewerDialog::rejectHandler() +void FictionViewerDialog::on_fontFileEdit_textChanged(const QString& text) { - this->close(); -} + _model->setFontFile(text.toUtf8().constData()); } + +void FictionViewerDialog::on_voiceFileEdit_textChanged(const QString& text) +{ + _model->setVoiceFile(text.toUtf8().constData()); } + +void FictionViewerDialog::on_musicComboBox_currentIndexChanged(int index) +{ + _model->setFictionMusic(ui->musicComboBox->itemData(index).value()); } + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/FictionViewerDialog.h b/qtfred/src/ui/dialogs/FictionViewerDialog.h index 09fcfd5ef09..cc2b95ec4ae 100644 --- a/qtfred/src/ui/dialogs/FictionViewerDialog.h +++ b/qtfred/src/ui/dialogs/FictionViewerDialog.h @@ -5,11 +5,7 @@ #include #include -#include - -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class FictionViewerDialog; @@ -21,30 +17,34 @@ class FictionViewerDialog : public QDialog { FictionViewerDialog(FredView* parent, EditorViewport* viewport); ~FictionViewerDialog() override; - void musicSelectionChanged(int index); - void storyFileTextChanged(); - void fontFileTextChanged(); - void voiceFileTextChanged(); + void accept() override; + void reject() override; protected: - void keyPressEvent(QKeyEvent* event) override; - void closeEvent(QCloseEvent*) override; - void rejectHandler(); - private: + void closeEvent(QCloseEvent* e) override; // funnel all Window X presses through reject() - void updateMusicComboBox(); +private slots: + // dialog controls + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + void on_storyFileEdit_textChanged(const QString& text); + void on_fontFileEdit_textChanged(const QString& text); + void on_voiceFileEdit_textChanged(const QString& text); + void on_musicComboBox_currentIndexChanged(int index); + + private: // NOLINT(readability-redundant-access-specifiers) + + void initializeUi(); + void updateUi(); - void updateUI(); + void updateMusicComboBox(); EditorViewport* _viewport = nullptr; - Editor* _editor = nullptr; - std::unique_ptr ui; std::unique_ptr _model; }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/FictionViewerDialog.ui b/qtfred/ui/FictionViewerDialog.ui index 7d65057be29..8328e182987 100644 --- a/qtfred/ui/FictionViewerDialog.ui +++ b/qtfred/ui/FictionViewerDialog.ui @@ -6,8 +6,8 @@ 0 0 - 229 - 157 + 277 + 172 @@ -74,7 +74,7 @@
- + QDialogButtonBox::Cancel|QDialogButtonBox::Ok @@ -85,7 +85,7 @@ - dialogButtonBox + okAndCancelButtons accepted() fso::fred::dialogs::FictionViewerDialog accept() From 9f30d3001c238840fd1ca37ede6df9864685d0f1 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sun, 24 Aug 2025 09:10:52 -0500 Subject: [PATCH 412/466] QtFRED Command Briefing Dialog (#6968) * refactor the qtfred command briefing dialog * clang --- code/mission/missionparse.h | 9 + .../dialogs/CommandBriefingDialogModel.cpp | 334 +++++------- .../dialogs/CommandBriefingDialogModel.h | 58 +- .../src/ui/dialogs/CommandBriefingDialog.cpp | 494 ++++++++---------- qtfred/src/ui/dialogs/CommandBriefingDialog.h | 51 +- qtfred/ui/CommandBriefingDialog.ui | 127 ++--- 6 files changed, 463 insertions(+), 610 deletions(-) diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index d00deb761a5..1b2333c6d58 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -89,6 +89,15 @@ extern bool check_for_24_3_data(); #define IS_MISSION_MULTI_TEAMS (The_mission.game_type & MISSION_TYPE_MULTI_TEAMS) #define IS_MISSION_MULTI_DOGFIGHT (The_mission.game_type & MISSION_TYPE_MULTI_DOGFIGHT) +// Used in the mission editor +inline const std::vector> Mission_event_teams_tvt = [] { + std::vector> arr; + arr.reserve(MAX_TVT_TEAMS); + for (int i = 0; i < MAX_TVT_TEAMS; ++i) { + arr.emplace_back("Team " + std::to_string(i + 1), i); + } + return arr; +}(); // Goober5000 typedef struct support_ship_info { diff --git a/qtfred/src/mission/dialogs/CommandBriefingDialogModel.cpp b/qtfred/src/mission/dialogs/CommandBriefingDialogModel.cpp index 14d2502bcbb..04260e46a52 100644 --- a/qtfred/src/mission/dialogs/CommandBriefingDialogModel.cpp +++ b/qtfred/src/mission/dialogs/CommandBriefingDialogModel.cpp @@ -3,9 +3,7 @@ #include "sound/audiostr.h" #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { CommandBriefingDialogModel::CommandBriefingDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) @@ -13,63 +11,12 @@ CommandBriefingDialogModel::CommandBriefingDialogModel(QObject* parent, EditorVi initializeData(); } -void CommandBriefingDialogModel::initializeData() -{ - Cur_cmd_brief = Cmd_briefs; // default to first cmd briefing - _wipCommandBrief.num_stages = Cur_cmd_brief->num_stages; - strcpy_s(_wipCommandBrief.background[0],Cur_cmd_brief->background[0]); - strcpy_s(_wipCommandBrief.background[1],Cur_cmd_brief->background[1]); - - if (!strlen(_wipCommandBrief.background[0])) { - strcpy_s(_wipCommandBrief.background[0], ""); - } - - if (!strlen(_wipCommandBrief.background[1])) { - strcpy_s(_wipCommandBrief.background[1], ""); - } - - _currentStage = 0; - - int i; - - for (i = 0; i < _wipCommandBrief.num_stages; i++) { - _wipCommandBrief.stage[i] = Cur_cmd_brief->stage[i]; - strcpy_s(_wipCommandBrief.stage[i].ani_filename, Cur_cmd_brief->stage[i].ani_filename); - _wipCommandBrief.stage[i].text = Cur_cmd_brief->stage[i].text; - _wipCommandBrief.stage[i].wave = Cur_cmd_brief->stage[i].wave; - strcpy_s(_wipCommandBrief.stage[i].wave_filename, Cur_cmd_brief->stage[i].wave_filename); - } - - for (i = _wipCommandBrief.num_stages; i < CMD_BRIEF_STAGES_MAX; i++) { - strcpy_s(_wipCommandBrief.stage[i].ani_filename, ""); - _wipCommandBrief.stage[i].text = ""; - _wipCommandBrief.stage[i].wave = -1; - strcpy_s(_wipCommandBrief.stage[i].wave_filename, "none"); - } - - _briefingTextUpdateRequired = true; - _stageNumberUpdateRequired = true; // always need to start off setting the correct stage. - _soundTestUpdateRequired = true; - _currentlyPlayingSound = -1; - - _currentTeam = 0; // this is forced to zero and kept there until multiple teams command briefing is supported. - modelChanged(); -} - bool CommandBriefingDialogModel::apply() { stopSpeech(); - // Copy the bits that are global to the Command Briefing - Cur_cmd_brief->num_stages = _wipCommandBrief.num_stages; - strcpy_s(Cur_cmd_brief->background[0], _wipCommandBrief.background[0]); - strcpy_s(Cur_cmd_brief->background[1], _wipCommandBrief.background[1]); - - int i = 0; - - for (i = 0; i < CMD_BRIEF_STAGES_MAX; i++) { - Cur_cmd_brief->stage[i] =_wipCommandBrief.stage[i]; - audiostream_close_file(_wipCommandBrief.stage[i].wave, false); + for (int i = 0; i < MAX_TVT_TEAMS; i++) { + Cmd_briefs[i] = _wipCommandBrief[i]; } return true; @@ -77,273 +24,248 @@ bool CommandBriefingDialogModel::apply() void CommandBriefingDialogModel::reject() { - stopSpeech(); - for (int i = _wipCommandBrief.num_stages; i < CMD_BRIEF_STAGES_MAX; i++) { - memset(&_wipCommandBrief.stage[i].ani_filename, 0, CF_MAX_FILENAME_LENGTH); - _wipCommandBrief.stage[i].text.clear(); - audiostream_close_file(_wipCommandBrief.stage[i].wave, false); - _wipCommandBrief.stage[i].wave = -1; - memset(&_wipCommandBrief.stage[i].wave_filename, 0, CF_MAX_FILENAME_LENGTH); - } +} - _wipCommandBrief.num_stages = 0; - memset(&_wipCommandBrief.background[0], 0, CF_MAX_FILENAME_LENGTH); - memset(&_wipCommandBrief.background[1], 0, CF_MAX_FILENAME_LENGTH); +void CommandBriefingDialogModel::initializeData() +{ + initializeTeamList(); + + // Make a working copy + for (int i = 0; i < MAX_TVT_TEAMS; i++) { + _wipCommandBrief[i] = Cmd_briefs[i]; + } + _currentTeam = 0; // default to the first team + _currentStage = 0; // default to the first stage } -void CommandBriefingDialogModel::update_init() {} - void CommandBriefingDialogModel::gotoPreviousStage() { - // make sure if (_currentStage <= 0) { _currentStage = 0; return; } - _briefingTextUpdateRequired = true; - _stageNumberUpdateRequired = true; stopSpeech(); _currentStage--; - modelChanged(); } void CommandBriefingDialogModel::gotoNextStage() { - _currentStage++; - - if (_currentStage >= _wipCommandBrief.num_stages) { - _currentStage = _wipCommandBrief.num_stages - 1; - } - else { - stopSpeech(); - _briefingTextUpdateRequired = true; - _stageNumberUpdateRequired = true; + if (_currentStage >= CMD_BRIEF_STAGES_MAX - 1) { + _currentStage = CMD_BRIEF_STAGES_MAX - 1; + return; } - // should update regardless, who knows, maybe there was an inexplicable invalid index before. - modelChanged(); + if (_currentStage >= _wipCommandBrief[_currentTeam].num_stages - 1) { + _currentStage = _wipCommandBrief[_currentTeam].num_stages - 1; + return; + } + + _currentStage++; + stopSpeech(); } void CommandBriefingDialogModel::addStage() { - _stageNumberUpdateRequired = true; - _briefingTextUpdateRequired = true; - stopSpeech(); - if (_wipCommandBrief.num_stages >= CMD_BRIEF_STAGES_MAX) { - _wipCommandBrief.num_stages = CMD_BRIEF_STAGES_MAX; - _currentStage = _wipCommandBrief.num_stages - 1; - modelChanged(); // signal that the model has changed, in case of inexplicable invalid index. + if (_wipCommandBrief[_currentTeam].num_stages >= CMD_BRIEF_STAGES_MAX) { + _wipCommandBrief[_currentTeam].num_stages = CMD_BRIEF_STAGES_MAX; + _currentStage = _wipCommandBrief[_currentTeam].num_stages - 1; return; } - _wipCommandBrief.num_stages++; - _currentStage = _wipCommandBrief.num_stages - 1; + _wipCommandBrief[_currentTeam].num_stages++; + _currentStage = _wipCommandBrief[_currentTeam].num_stages - 1; + _wipCommandBrief[_currentTeam].stage[_currentStage].text = ""; set_modified(); - modelChanged(); } // copies the current stage as the next stage and then moves the rest of the stages over. void CommandBriefingDialogModel::insertStage() { - _stageNumberUpdateRequired = true; + stopSpeech(); - if (_wipCommandBrief.num_stages >= CMD_BRIEF_STAGES_MAX) { - _wipCommandBrief.num_stages = CMD_BRIEF_STAGES_MAX; + if (_wipCommandBrief[_currentTeam].num_stages >= CMD_BRIEF_STAGES_MAX) { + _wipCommandBrief[_currentTeam].num_stages = CMD_BRIEF_STAGES_MAX; set_modified(); - modelChanged(); // signal that the model has changed, in case of inexplicable invalid index. return; } - _wipCommandBrief.num_stages++; + _wipCommandBrief[_currentTeam].num_stages++; - for (int i = _wipCommandBrief.num_stages - 1; i > _currentStage; i--) { - _wipCommandBrief.stage[i] = _wipCommandBrief.stage[i - 1]; + for (int i = _wipCommandBrief[_currentTeam].num_stages - 1; i > _currentStage; i--) { + _wipCommandBrief[_currentTeam].stage[i] = _wipCommandBrief[_currentTeam].stage[i - 1]; } + + // Future TODO: Add a QtFRED Option to clear the inserted stage instead of copying the current one. + set_modified(); - modelChanged(); } void CommandBriefingDialogModel::deleteStage() { - _briefingTextUpdateRequired = true; - _stageNumberUpdateRequired = true; - stopSpeech(); // Clear everything if we were on the last stage. - if (_wipCommandBrief.num_stages <= 1) { - _wipCommandBrief.num_stages = 0; - _wipCommandBrief.stage[0].text.clear(); - _wipCommandBrief.stage[0].wave = -1; - memset(_wipCommandBrief.stage[0].wave_filename, 0, CF_MAX_FILENAME_LENGTH); - memset(_wipCommandBrief.stage[0].ani_filename, 0, CF_MAX_FILENAME_LENGTH); + if (_wipCommandBrief[_currentTeam].num_stages <= 1) { + _wipCommandBrief[_currentTeam].num_stages = 0; + _wipCommandBrief[_currentTeam].stage[0].text.clear(); + _wipCommandBrief[_currentTeam].stage[0].wave = -1; + memset(_wipCommandBrief[_currentTeam].stage[0].wave_filename, 0, CF_MAX_FILENAME_LENGTH); + memset(_wipCommandBrief[_currentTeam].stage[0].ani_filename, 0, CF_MAX_FILENAME_LENGTH); set_modified(); - modelChanged(); return; } // copy the stages backwards until we get to the stage we're on - for (int i = _currentStage; i + 1 < _wipCommandBrief.num_stages; i++){ - _wipCommandBrief.stage[i] = _wipCommandBrief.stage[i + 1]; + for (int i = _currentStage; i + 1 < _wipCommandBrief[_currentTeam].num_stages; i++) { + _wipCommandBrief[_currentTeam].stage[i] = _wipCommandBrief[_currentTeam].stage[i + 1]; } - _wipCommandBrief.num_stages--; + _wipCommandBrief[_currentTeam].num_stages--; + + // Clear the tail + const int tail = _wipCommandBrief[_currentTeam].num_stages; // index of the old last element + _wipCommandBrief[_currentTeam].stage[tail].text.clear(); + _wipCommandBrief[_currentTeam].stage[tail].wave = -1; + std::memset(_wipCommandBrief[_currentTeam].stage[tail].wave_filename, 0, CF_MAX_FILENAME_LENGTH); + std::memset(_wipCommandBrief[_currentTeam].stage[tail].ani_filename, 0, CF_MAX_FILENAME_LENGTH); // make sure that the current stage is valid. - if (_wipCommandBrief.num_stages <= _currentStage) { - _currentStage = _wipCommandBrief.num_stages - 1; + if (_wipCommandBrief[_currentTeam].num_stages <= _currentStage) { + _currentStage = _wipCommandBrief[_currentTeam].num_stages - 1; } - modelChanged(); + set_modified(); } -void CommandBriefingDialogModel::setWaveID() +void CommandBriefingDialogModel::testSpeech() { - // close the old one - if (_wipCommandBrief.stage[_currentStage].wave >= 0) { - audiostream_close_file(_wipCommandBrief.stage[_currentStage].wave, false); - } + // May cause unloading/reloading but it's just the mission editor + // we don't need to keep all the waves loaded only to have to unload them + // later anyway. This ensures we have one wave loaded and stopSpeech always unloads it + + stopSpeech(); - // we use ASF_EVENTMUSIC here so that it will keep the extension in place - _wipCommandBrief.stage[_currentStage].wave = audiostream_open(_wipCommandBrief.stage[_currentStage].wave_filename, ASF_EVENTMUSIC); - _soundTestUpdateRequired = true; + _waveId = audiostream_open(_wipCommandBrief[_currentTeam].stage[_currentStage].wave_filename, ASF_EVENTMUSIC); + audiostream_play(_waveId, 1.0f, 0); } -void CommandBriefingDialogModel::testSpeech() +void CommandBriefingDialogModel::copyToOtherTeams() { - if (_wipCommandBrief.stage[_currentStage].wave >= 0 && !audiostream_is_playing(_wipCommandBrief.stage[_currentStage].wave)) { - stopSpeech(); - audiostream_play(_wipCommandBrief.stage[_currentStage].wave, 1.0f, 0); - _currentlyPlayingSound = _wipCommandBrief.stage[_currentStage].wave; + stopSpeech(); + + for (int i = 0; i < MAX_TVT_TEAMS; i++) { + if (i != _currentTeam) { + _wipCommandBrief[i] = _wipCommandBrief[_currentTeam]; + } } + set_modified(); } -void CommandBriefingDialogModel::stopSpeech() +const SCP_vector>& CommandBriefingDialogModel::getTeamList() { - if (_currentlyPlayingSound >= -1) { - audiostream_stop(_currentlyPlayingSound,1,0); - _currentlyPlayingSound = -1; - } + return _teamList; } -bool CommandBriefingDialogModel::briefingUpdateRequired() +bool CommandBriefingDialogModel::getMissionIsMultiTeam() { - return _briefingTextUpdateRequired; + return The_mission.game_type & MISSION_TYPE_MULTI_TEAMS; } -bool CommandBriefingDialogModel::stageNumberUpdateRequired() -{ - return _stageNumberUpdateRequired; +void CommandBriefingDialogModel::stopSpeech() +{ + if (_waveId >= -1) { + audiostream_close_file(_waveId, false); + _waveId = -1; + } } -bool CommandBriefingDialogModel::soundTestUpdateRequired() -{ - return _soundTestUpdateRequired; +void CommandBriefingDialogModel::initializeTeamList() +{ + _teamList.clear(); + for (auto& team : Mission_event_teams_tvt) { + _teamList.emplace_back(team.first, team.second); + } } -SCP_string CommandBriefingDialogModel::getBriefingText() -{ - _briefingTextUpdateRequired = false; - return _wipCommandBrief.stage[_currentStage].text; +int CommandBriefingDialogModel::getCurrentTeam() const +{ + return _currentTeam; } -SCP_string CommandBriefingDialogModel::getAnimationFilename() -{ - return _wipCommandBrief.stage[_currentStage].ani_filename; -} +void CommandBriefingDialogModel::setCurrentTeam(int teamIn) +{ + modify(_currentTeam, teamIn); +}; -SCP_string CommandBriefingDialogModel::getSpeechFilename() -{ - return _wipCommandBrief.stage[_currentStage].wave_filename; +int CommandBriefingDialogModel::getCurrentStage() const +{ + return _currentStage; } -ubyte CommandBriefingDialogModel::getCurrentTeam() -{ - return _currentTeam; +int CommandBriefingDialogModel::getTotalStages() +{ + return _wipCommandBrief[_currentTeam].num_stages; } -SCP_string CommandBriefingDialogModel::getLowResolutionFilename() +SCP_string CommandBriefingDialogModel::getBriefingText() { - return _wipCommandBrief.background[0]; + return _wipCommandBrief[_currentTeam].stage[_currentStage].text; } -SCP_string CommandBriefingDialogModel::getHighResolutionFilename() -{ - return _wipCommandBrief.background[1]; +void CommandBriefingDialogModel::setBriefingText(const SCP_string& briefingText) +{ + modify(_wipCommandBrief[_currentTeam].stage[_currentStage].text, briefingText); } -int CommandBriefingDialogModel::getTotalStages() +SCP_string CommandBriefingDialogModel::getAnimationFilename() { - _stageNumberUpdateRequired = false; - return _wipCommandBrief.num_stages; + return _wipCommandBrief[_currentTeam].stage[_currentStage].ani_filename; } -int CommandBriefingDialogModel::getCurrentStage() -{ - return _currentStage; +void CommandBriefingDialogModel::setAnimationFilename(const SCP_string& animationFilename) +{ + strcpy_s(_wipCommandBrief[_currentTeam].stage[_currentStage].ani_filename, animationFilename.c_str()); + set_modified(); } -int CommandBriefingDialogModel::getSpeechInstanceNumber() +SCP_string CommandBriefingDialogModel::getSpeechFilename() { - return _wipCommandBrief.stage[_currentStage].wave; + return _wipCommandBrief[_currentTeam].stage[_currentStage].wave_filename; } -void CommandBriefingDialogModel::setBriefingText(const SCP_string& briefingText) -{ - _wipCommandBrief.stage[_currentStage].text = briefingText; +void CommandBriefingDialogModel::setSpeechFilename(const SCP_string& speechFilename) +{ + strcpy_s(_wipCommandBrief[_currentTeam].stage[_currentStage].wave_filename, speechFilename.c_str()); set_modified(); - modelChanged(); } -void CommandBriefingDialogModel::setAnimationFilename(const SCP_string& animationFilename) +SCP_string CommandBriefingDialogModel::getLowResolutionFilename() { - strcpy_s(_wipCommandBrief.stage[_currentStage].ani_filename, animationFilename.c_str()); - set_modified(); - modelChanged(); + return _wipCommandBrief[_currentTeam].background[0]; } -void CommandBriefingDialogModel::setSpeechFilename(const SCP_string& speechFilename) -{ - _soundTestUpdateRequired = true; - strcpy_s(_wipCommandBrief.stage[_currentStage].wave_filename, speechFilename.c_str()); - setWaveID(); +void CommandBriefingDialogModel::setLowResolutionFilename(const SCP_string& lowResolutionFilename) +{ + strcpy_s(_wipCommandBrief[_currentTeam].background[0], lowResolutionFilename.c_str()); set_modified(); - modelChanged(); } -void CommandBriefingDialogModel::setCurrentTeam(const ubyte& teamIn) -{ - _currentTeam = teamIn; - set_modified(); -}; // not yet fully supported - -void CommandBriefingDialogModel::setLowResolutionFilename(const SCP_string& lowResolutionFilename) +SCP_string CommandBriefingDialogModel::getHighResolutionFilename() { - strcpy_s(_wipCommandBrief.background[0], lowResolutionFilename.c_str()); - set_modified(); - modelChanged(); + return _wipCommandBrief[_currentTeam].background[1]; } void CommandBriefingDialogModel::setHighResolutionFilename(const SCP_string& highResolutionFilename) { - strcpy_s(_wipCommandBrief.background[1], highResolutionFilename.c_str()); + strcpy_s(_wipCommandBrief[_currentTeam].background[1], highResolutionFilename.c_str()); set_modified(); - modelChanged(); -} - -void CommandBriefingDialogModel::requestInitialUpdate() -{ - initializeData(); - modelChanged(); } -} -} -} \ No newline at end of file +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/CommandBriefingDialogModel.h b/qtfred/src/mission/dialogs/CommandBriefingDialogModel.h index 0b9d0275226..11d7ad980cd 100644 --- a/qtfred/src/mission/dialogs/CommandBriefingDialogModel.h +++ b/qtfred/src/mission/dialogs/CommandBriefingDialogModel.h @@ -1,11 +1,11 @@ +#pragma once + #include "AbstractDialogModel.h" #include "globalincs/pstypes.h" #include "missionui/missioncmdbrief.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { class CommandBriefingDialogModel: public AbstractDialogModel { @@ -16,57 +16,43 @@ class CommandBriefingDialogModel: public AbstractDialogModel { bool apply() override; void reject() override; - bool briefingUpdateRequired(); - bool stageNumberUpdateRequired(); - bool soundTestUpdateRequired(); - - SCP_string getBriefingText(); - SCP_string getAnimationFilename(); - SCP_string getSpeechFilename(); - ubyte getCurrentTeam(); - SCP_string getLowResolutionFilename(); - SCP_string getHighResolutionFilename(); + int getCurrentTeam() const; + void setCurrentTeam(int teamIn); + int getCurrentStage() const; int getTotalStages(); - int getCurrentStage(); - int getSpeechInstanceNumber(); + SCP_string getBriefingText(); void setBriefingText(const SCP_string& briefingText); + SCP_string getAnimationFilename(); void setAnimationFilename(const SCP_string& animationFilename); + SCP_string getSpeechFilename(); void setSpeechFilename(const SCP_string& speechFilename); - void setCurrentTeam(const ubyte& teamIn); // not yet fully supported + SCP_string getLowResolutionFilename(); void setLowResolutionFilename(const SCP_string& lowResolutionFilename); + SCP_string getHighResolutionFilename(); void setHighResolutionFilename(const SCP_string& highResolutionFilename); - - // work-around function to keep Command Brief Dialog from crashing unexpected on init - void requestInitialUpdate(); - - void testSpeech(); - void stopSpeech(); - + void gotoPreviousStage(); void gotoNextStage(); void addStage(); void insertStage(); void deleteStage(); - - void update_init(); + void testSpeech(); + void copyToOtherTeams(); + const SCP_vector>& getTeamList(); + static bool getMissionIsMultiTeam(); private: void initializeData(); - void setWaveID(); + void stopSpeech(); + void initializeTeamList(); + cmd_brief _wipCommandBrief[MAX_TVT_TEAMS]; int _currentTeam; int _currentStage; - int _currentlyPlayingSound; - - bool _briefingTextUpdateRequired; - bool _stageNumberUpdateRequired; - bool _soundTestUpdateRequired; - - cmd_brief _wipCommandBrief; + int _waveId; + SCP_vector> _teamList; }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/CommandBriefingDialog.cpp b/qtfred/src/ui/dialogs/CommandBriefingDialog.cpp index ad7c46358d4..04fd9a1134f 100644 --- a/qtfred/src/ui/dialogs/CommandBriefingDialog.cpp +++ b/qtfred/src/ui/dialogs/CommandBriefingDialog.cpp @@ -4,295 +4,251 @@ #include #include #include -#include - -namespace fso { -namespace fred { -namespace dialogs { - - - CommandBriefingDialog::CommandBriefingDialog(FredView* parent, EditorViewport* viewport) - : QDialog(parent), ui(new Ui::CommandBriefingDialog()), _model(new CommandBriefingDialogModel(this, viewport)), - _viewport(viewport) - { - this->setFocus(); - ui->setupUi(this); - - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &CommandBriefingDialog::updateUI); - connect(this, &QDialog::accepted, _model.get(), &CommandBriefingDialogModel::apply); - connect(viewport->editor, &Editor::currentObjectChanged, _model.get(), &CommandBriefingDialogModel::apply); - connect(viewport->editor, &Editor::objectMarkingChanged, _model.get(), &CommandBriefingDialogModel::apply); - connect(ui->okAndCancelButtons, &QDialogButtonBox::rejected, this, &CommandBriefingDialog::rejectHandler); - - connect(ui->actionChangeTeams, - static_cast(&QSpinBox::valueChanged), - this, - &CommandBriefingDialog::on_actionChangeTeams_clicked); - - connect(ui->actionBriefingTextEditor, - &QPlainTextEdit::textChanged, - this, - &CommandBriefingDialog::briefingTextChanged); - - connect(ui->animationFileName, - static_cast(&QLineEdit::textChanged), - this, - &CommandBriefingDialog::animationFilenameChanged); - - connect(ui->speechFileName, - static_cast(&QLineEdit::textChanged), - this, - &CommandBriefingDialog::speechFilenameChanged); - - connect(ui->actionLowResolutionFilenameEdit, - static_cast(&QLineEdit::textChanged), - this, - &CommandBriefingDialog::lowResolutionFilenameChanged); - - connect(ui->actionHighResolutionFilenameEdit, - static_cast(&QLineEdit::textChanged), - this, - &CommandBriefingDialog::highResolutionFilenameChanged); - - resize(QDialog::sizeHint()); - - _model->requestInitialUpdate(); - } - - void CommandBriefingDialog::on_actionPrevStage_clicked() - { - _model->gotoPreviousStage(); - } - - void CommandBriefingDialog::on_actionNextStage_clicked() - { - _model->gotoNextStage(); - } - - void CommandBriefingDialog::on_actionAddStage_clicked() - { - _model->addStage(); - } - - void CommandBriefingDialog::on_actionInsertStage_clicked() - { - _model->insertStage(); - } - - void CommandBriefingDialog::on_actionDeleteStage_clicked() - { - _model->deleteStage(); - } - - void CommandBriefingDialog::on_actionChangeTeams_clicked () - { - // not yet supported - } - - void CommandBriefingDialog::on_actionCopyToOtherTeams_clicked() - { - // not yet supported. - } - - void CommandBriefingDialog::on_actionBrowseAnimation_clicked() - { - QString filename; - - if (CommandBriefingDialog::browseFile(&filename)) { - _model->setAnimationFilename(filename.toStdString()); - } - } - - void CommandBriefingDialog::on_actionBrowseSpeechFile_clicked() - { - QString filename; +#include - if (CommandBriefingDialog::browseFile(&filename)) { - _model->setSpeechFilename(filename.toStdString()); - } - } - - void CommandBriefingDialog::on_actionTestSpeechFileButton_clicked() - { - _model->testSpeech(); - } - - void CommandBriefingDialog::on_actionLowResolutionBrowse_clicked() - { - QString filename; - - if (CommandBriefingDialog::browseFile(&filename)) { - _model->setLowResolutionFilename(filename.toStdString()); - } - } - - void CommandBriefingDialog::on_actionHighResolutionBrowse_clicked() - { - QString filename; +namespace fso::fred::dialogs { - if (CommandBriefingDialog::browseFile(&filename)) { - _model->setHighResolutionFilename(filename.toStdString()); - } - } +CommandBriefingDialog::CommandBriefingDialog(FredView* parent, EditorViewport* viewport) +: QDialog(parent), ui(new Ui::CommandBriefingDialog()), _model(new CommandBriefingDialogModel(this, viewport)), +_viewport(viewport) +{ + this->setFocus(); + ui->setupUi(this); - void CommandBriefingDialog::updateUI() - { + initializeUi(); + updateUi(); - util::SignalBlockers blockers(this); - disableTeams(); // until supported, keep it from blowing things up + resize(QDialog::sizeHint()); - // once supported, set the team here +} - // only do this when necessary because the cursor will get moved around if this is not handled properly. - if (_model->briefingUpdateRequired()) { - ui->actionBriefingTextEditor->setPlainText(_model->getBriefingText().c_str()); - } +CommandBriefingDialog::~CommandBriefingDialog() = default; - // these line edits always seems to work fine without having to check with the model first. - ui->animationFileName->setText(_model->getAnimationFilename().c_str()); - ui->speechFileName->setText(_model->getSpeechFilename().c_str()); - ui->actionLowResolutionFilenameEdit->setText(_model->getLowResolutionFilename().c_str()); - ui->actionHighResolutionFilenameEdit->setText(_model->getHighResolutionFilename().c_str()); - - // needs to go at the end. - enableDisableControls(); +void CommandBriefingDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); } + // else: validation failed, don’t close +} - void CommandBriefingDialog::enableDisableControls() - { - - if (_model->stageNumberUpdateRequired()) { - int max_stage = _model->getTotalStages(); - - if (max_stage == 0) { - ui->actionPrevStage->setEnabled(false); - ui->actionNextStage->setEnabled(false); - ui->actionChangeTeams->setEnabled(false); - ui->actionInsertStage->setEnabled(false); - ui->actionDeleteStage->setEnabled(false); - ui->actionBrowseAnimation->setEnabled(false); - ui->animationFileName->setEnabled(false); - ui->actionBrowseSpeechFile->setEnabled(false); - ui->speechFileName->setEnabled(false); - ui->actionTestSpeechFileButton->setEnabled(false); - - ui->currentStageLabel->setText("No Stages"); - return; - } - else { - int current_stage = _model->getCurrentStage() + 1; - - if (current_stage == 1) { - ui->actionPrevStage->setEnabled(false); - } - else { - ui->actionPrevStage->setEnabled(true); - } - - if (current_stage == max_stage) { - ui->actionNextStage->setEnabled(false); - } - else { - ui->actionNextStage->setEnabled(true); - } - - if (max_stage >= CMD_BRIEF_STAGES_MAX) { - ui->actionAddStage->setEnabled(false); - ui->actionInsertStage->setEnabled(false); - } - else { - ui->actionAddStage->setEnabled(true); - ui->actionInsertStage->setEnabled(true); - } - - ui->actionDeleteStage->setEnabled(true); - ui->actionBrowseAnimation->setEnabled(true); - ui->animationFileName->setEnabled(true); - ui->actionBrowseSpeechFile->setEnabled(true); - ui->speechFileName->setEnabled(true); - - - - SCP_string to_ui_string = "Stage "; - to_ui_string += std::to_string(current_stage); - to_ui_string += " of "; - to_ui_string += std::to_string(max_stage); - - ui->currentStageLabel->setText(to_ui_string.c_str()); - } - } - - if (_model->soundTestUpdateRequired()) { - if (_model->getSpeechInstanceNumber() >= 0) { - ui->actionTestSpeechFileButton->setEnabled(true); - } - else { - ui->actionTestSpeechFileButton->setEnabled(false); - } - } +void CommandBriefingDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close } + // else: do nothing, don't close +} - void CommandBriefingDialog::briefingTextChanged() - { - _model->setBriefingText(ui->actionBriefingTextEditor->document()->toPlainText().toStdString()); - } +void CommandBriefingDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window +} - void CommandBriefingDialog::animationFilenameChanged(const QString& string) - { - _model->setAnimationFilename(string.toStdString()); - } +void CommandBriefingDialog::initializeUi() +{ + auto list = _model->getTeamList(); - void CommandBriefingDialog::speechFilenameChanged(const QString& string) - { - _model->setSpeechFilename(string.toStdString()); - } + ui->actionChangeTeams->clear(); - void CommandBriefingDialog::lowResolutionFilenameChanged(const QString& string) - { - _model->setLowResolutionFilename(string.toStdString()); + for (const auto& team : list) { + ui->actionChangeTeams->addItem(QString::fromStdString(team.first), team.second); } +} - void CommandBriefingDialog::highResolutionFilenameChanged(const QString& string) - { - _model->setHighResolutionFilename(string.toStdString()); - } +void CommandBriefingDialog::updateUi() +{ - // literally just here to keep things from blowing up until we have team Command Brief Support. - void CommandBriefingDialog::disableTeams() - { - ui->actionChangeTeams->setEnabled(false); - ui->actionCopyToOtherTeams->setEnabled(false); - } + util::SignalBlockers blockers(this); - // string in returns the file name, and the function returns true for success or false for fail. - bool CommandBriefingDialog::browseFile(QString* stringIn) - { - QFileInfo fileInfo(QFileDialog::getOpenFileName()); - *stringIn = fileInfo.fileName(); + ui->actionChangeTeams->setCurrentIndex(ui->actionChangeTeams->findData(_model->getCurrentTeam())); - if (stringIn->length() >= CF_MAX_FILENAME_LENGTH) { - ReleaseWarning(LOCATION, "No filename in FSO can be %d characters or longer.", CF_MAX_FILENAME_LENGTH); - return false; - } else if (stringIn->isEmpty()) { - return false; - } + ui->actionBriefingTextEditor->setPlainText(_model->getBriefingText().c_str()); + ui->animationFileName->setText(_model->getAnimationFilename().c_str()); + ui->speechFileName->setText(_model->getSpeechFilename().c_str()); + ui->actionLowResolutionFilenameEdit->setText(_model->getLowResolutionFilename().c_str()); + ui->actionHighResolutionFilenameEdit->setText(_model->getHighResolutionFilename().c_str()); - return true; + SCP_string stages = "No Stages"; + int total = _model->getTotalStages(); + int current = _model->getCurrentStage() + 1; // internal is 0 based, ui is 1 based + if (total > 0) { + stages = "Stage "; + stages += std::to_string(current); + stages += " of "; + stages += std::to_string(total); } + ui->currentStageLabel->setText(stages.c_str()); + enableDisableControls(); +} - CommandBriefingDialog::~CommandBriefingDialog() {}; //NOLINT - - void CommandBriefingDialog::closeEvent(QCloseEvent* e) - { - if (!rejectOrCloseHandler(this, _model.get(), _viewport)) { - e->ignore(); - }; - } - void CommandBriefingDialog::rejectHandler() - { - this->close(); - } - } // dialogs -} // fred -} // fso \ No newline at end of file +void CommandBriefingDialog::enableDisableControls() +{ + int total_stages = _model->getTotalStages(); + int current = _model->getCurrentStage(); + + ui->actionPrevStage->setEnabled(total_stages > 0 && current > 0); + ui->actionNextStage->setEnabled(total_stages > 0 && current < total_stages - 1); + ui->actionAddStage->setEnabled(total_stages < CMD_BRIEF_STAGES_MAX); + ui->actionInsertStage->setEnabled(total_stages < CMD_BRIEF_STAGES_MAX); + ui->actionDeleteStage->setEnabled(total_stages > 0); + + ui->actionChangeTeams->setEnabled(_model->getMissionIsMultiTeam()); + ui->actionCopyToOtherTeams->setEnabled(_model->getMissionIsMultiTeam()); + + ui->animationFileName->setEnabled(total_stages > 0); + ui->actionBrowseAnimation->setEnabled(total_stages > 0); + ui->speechFileName->setEnabled(total_stages > 0); + ui->actionBrowseSpeechFile->setEnabled(total_stages > 0); + ui->actionTestSpeechFileButton->setEnabled(total_stages > 0 && !_model->getSpeechFilename().empty()); + + ui->actionLowResolutionFilenameEdit->setEnabled(total_stages > 0); + ui->actionLowResolutionBrowse->setEnabled(total_stages > 0); + ui->actionHighResolutionFilenameEdit->setEnabled(total_stages > 0); + ui->actionHighResolutionBrowse->setEnabled(total_stages > 0); +} + +void CommandBriefingDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void CommandBriefingDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void CommandBriefingDialog::on_actionPrevStage_clicked() +{ + _model->gotoPreviousStage(); + updateUi(); +} + +void CommandBriefingDialog::on_actionNextStage_clicked() +{ + _model->gotoNextStage(); + updateUi(); +} + +void CommandBriefingDialog::on_actionAddStage_clicked() +{ + _model->addStage(); + updateUi(); +} + +void CommandBriefingDialog::on_actionInsertStage_clicked() +{ + _model->insertStage(); + updateUi(); +} + +void CommandBriefingDialog::on_actionDeleteStage_clicked() +{ + _model->deleteStage(); + updateUi(); +} + +void CommandBriefingDialog::on_actionCopyToOtherTeams_clicked() +{ + _model->copyToOtherTeams(); +} + +void CommandBriefingDialog::on_actionBrowseAnimation_clicked() +{ + QString filename; + + if (CommandBriefingDialog::browseFile(&filename)) { + _model->setAnimationFilename(filename.toUtf8().constData()); + } + updateUi(); +} + +void CommandBriefingDialog::on_actionBrowseSpeechFile_clicked() +{ + QString filename; + + if (CommandBriefingDialog::browseFile(&filename)) { + _model->setSpeechFilename(filename.toUtf8().constData()); + } + updateUi(); +} + +void CommandBriefingDialog::on_actionTestSpeechFileButton_clicked() +{ + _model->testSpeech(); +} + +void CommandBriefingDialog::on_actionLowResolutionBrowse_clicked() +{ + QString filename; + + if (CommandBriefingDialog::browseFile(&filename)) { + _model->setLowResolutionFilename(filename.toUtf8().constData()); + } + updateUi(); +} + +void CommandBriefingDialog::on_actionHighResolutionBrowse_clicked() +{ + QString filename; + + if (CommandBriefingDialog::browseFile(&filename)) { + _model->setHighResolutionFilename(filename.toUtf8().constData()); + } + updateUi(); +} + +void CommandBriefingDialog::on_actionChangeTeams_currentIndexChanged(int index) +{ + _model->setCurrentTeam(ui->actionChangeTeams->itemData(index).toInt()); + updateUi(); +} + +void CommandBriefingDialog::on_actionBriefingTextEditor_textChanged() +{ + _model->setBriefingText(ui->actionBriefingTextEditor->document()->toPlainText().toUtf8().constData()); +} + +void CommandBriefingDialog::on_animationFilename_textChanged(const QString& string) +{ + _model->setAnimationFilename(string.toUtf8().constData()); +} + +void CommandBriefingDialog::on_speechFilename_textChanged(const QString& string) +{ + _model->setSpeechFilename(string.toUtf8().constData()); +} + +void CommandBriefingDialog::on_actionLowResolutionFilenameEdit_textChanged(const QString& string) +{ + _model->setLowResolutionFilename(string.toStdString()); +} + +void CommandBriefingDialog::on_actionHighResolutionFilenameEdit_textChanged(const QString& string) +{ + _model->setHighResolutionFilename(string.toStdString()); +} + +// string in returns the file name, and the function returns true for success or false for fail. +bool CommandBriefingDialog::browseFile(QString* stringIn) +{ + QFileInfo fileInfo(QFileDialog::getOpenFileName()); + *stringIn = fileInfo.fileName(); + + if (stringIn->length() >= CF_MAX_FILENAME_LENGTH) { + ReleaseWarning(LOCATION, "No filename in FSO can be %d characters or longer.", CF_MAX_FILENAME_LENGTH); + return false; + } else if (stringIn->isEmpty()) { + return false; + } + + return true; +} + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/CommandBriefingDialog.h b/qtfred/src/ui/dialogs/CommandBriefingDialog.h index 4b1c27ba01c..ce3480c0c87 100644 --- a/qtfred/src/ui/dialogs/CommandBriefingDialog.h +++ b/qtfred/src/ui/dialogs/CommandBriefingDialog.h @@ -1,5 +1,4 @@ -#ifndef COMMANDBRIEFEDITORDIALOG_H -#define COMMANDBRIEFEDITORDIALOG_H +#pragma once #include #include @@ -9,35 +8,35 @@ #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class CommandBriefingDialog; } class CommandBriefingDialog : public QDialog { - Q_OBJECT public: - explicit CommandBriefingDialog(FredView* parent, EditorViewport* viewport); - ~CommandBriefingDialog() override; // NOLINT + ~CommandBriefingDialog() override; + + void accept() override; + void reject() override; -protected: - void closeEvent(QCloseEvent*) override; + protected: + void closeEvent(QCloseEvent* e) override; // funnel all Window X presses through reject() - void rejectHandler(); +private slots: + // dialog controls + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); -private slots: // where the buttons go void on_actionPrevStage_clicked(); void on_actionNextStage_clicked(); void on_actionAddStage_clicked(); void on_actionInsertStage_clicked(); void on_actionDeleteStage_clicked(); - void on_actionChangeTeams_clicked(); void on_actionCopyToOtherTeams_clicked(); void on_actionBrowseAnimation_clicked(); void on_actionBrowseSpeechFile_clicked(); @@ -45,27 +44,23 @@ private slots: // where the buttons go void on_actionLowResolutionBrowse_clicked(); void on_actionHighResolutionBrowse_clicked(); -private: + void on_actionChangeTeams_currentIndexChanged(int index); + void on_actionBriefingTextEditor_textChanged(); + void on_animationFilename_textChanged(const QString& string); + void on_speechFilename_textChanged(const QString& string); + void on_actionLowResolutionFilenameEdit_textChanged(const QString& string); + void on_actionHighResolutionFilenameEdit_textChanged(const QString& string); + +private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; - void updateUI(); - void disableTeams(); + void initializeUi(); + void updateUi(); void enableDisableControls(); - // when fields get updated - void briefingTextChanged(); - void animationFilenameChanged(const QString&); - void speechFilenameChanged(const QString&); - void lowResolutionFilenameChanged(const QString&); - void highResolutionFilenameChanged(const QString&); - static bool browseFile(QString* stringIn); }; -} // namespace dialogs -} // namespace fred -} // namespace fso - -#endif +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/CommandBriefingDialog.ui b/qtfred/ui/CommandBriefingDialog.ui index a3d2171d896..5a03819846b 100644 --- a/qtfred/ui/CommandBriefingDialog.ui +++ b/qtfred/ui/CommandBriefingDialog.ui @@ -6,8 +6,8 @@ 0 0 - 547 - 533 + 894 + 537 @@ -32,6 +32,13 @@ + + + + Team + + + @@ -39,8 +46,15 @@ - - + + + + Delete + + + + + Qt::Horizontal @@ -52,43 +66,37 @@ - - + + - Team + Insert - - - - 1 + + + + + 10 + - - - - - Delete + No Stages - - + + - Prev + Copy to Other Teams - - - - Add - - + + - - + + Qt::Horizontal @@ -100,29 +108,17 @@ - - - - Insert - - - - - + + - Copy to Other Teams + Prev - - - - - 10 - - + + - No Stages + Add @@ -155,7 +151,7 @@ Qt::ScrollBarAlwaysOn - <Text here> + @@ -176,7 +172,7 @@ - none + 31 @@ -186,7 +182,7 @@ - <default> + 31 @@ -210,7 +206,7 @@ - <default> + 31 @@ -263,7 +259,7 @@ - <default> + 31 @@ -273,7 +269,12 @@ - Test Speech + + + + + :/images/play.png + @@ -321,7 +322,6 @@ actionNextStage actionInsertStage actionDeleteStage - actionChangeTeams actionCopyToOtherTeams actionBriefingTextEditor animationFileName @@ -334,23 +334,8 @@ actionHighResolutionFilenameEdit actionHighResolutionBrowse - - - - okAndCancelButtons - accepted() - fso::fred::dialogs::CommandBriefingDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - + + + + From 08a73c1ed7c523d6baf30dee04ae33fafaf3b64e Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 06:43:10 -0500 Subject: [PATCH 413/466] QtFRED Voice Acting Manager Dialog (#6962) * revise Voice Acting Manager dialog * fixes * clang * make window modal --- code/missioneditor/common.cpp | 23 + code/missioneditor/common.h | 26 + code/source_groups.cmake | 6 + fred2/management.cpp | 2 +- fred2/management.h | 12 +- fred2/voiceactingmanager.cpp | 35 +- qtfred/source_groups.cmake | 2 + .../dialogs/VoiceActingManagerModel.cpp | 961 ++++++++++++++++++ .../mission/dialogs/VoiceActingManagerModel.h | 150 +++ qtfred/src/ui/dialogs/VoiceActingManager.cpp | 329 +++++- qtfred/src/ui/dialogs/VoiceActingManager.h | 75 +- qtfred/ui/VoiceActingManager.ui | 346 ++++--- 12 files changed, 1790 insertions(+), 177 deletions(-) create mode 100644 code/missioneditor/common.cpp create mode 100644 code/missioneditor/common.h create mode 100644 qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp create mode 100644 qtfred/src/mission/dialogs/VoiceActingManagerModel.h diff --git a/code/missioneditor/common.cpp b/code/missioneditor/common.cpp new file mode 100644 index 00000000000..3a8a6c3835f --- /dev/null +++ b/code/missioneditor/common.cpp @@ -0,0 +1,23 @@ +// methods and members common to any mission editor FSO may have +#include "common.h" + +// 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."; \ No newline at end of file diff --git a/code/missioneditor/common.h b/code/missioneditor/common.h new file mode 100644 index 00000000000..72039a4fe31 --- /dev/null +++ b/code/missioneditor/common.h @@ -0,0 +1,26 @@ +#pragma once +#include "globalincs/globals.h" +#include "mission/missionmessage.h" + +// Voice acting manager +#define INVALID_MESSAGE ((MMessage*)SIZE_MAX) // was originally SIZE_T_MAX but that wasn't available outside fred. May need more research. + +enum class PersonaSyncIndex : int { + Wingman = 0, // + NonWingman = 1, // + PersonasStart = 2 // indices >= 2 map to specific persona +}; + +extern char Voice_abbrev_briefing[NAME_LENGTH]; +extern char Voice_abbrev_campaign[NAME_LENGTH]; +extern char Voice_abbrev_command_briefing[NAME_LENGTH]; +extern char Voice_abbrev_debriefing[NAME_LENGTH]; +extern char Voice_abbrev_message[NAME_LENGTH]; +extern char Voice_abbrev_mission[NAME_LENGTH]; +extern bool Voice_no_replace_filenames; +extern char Voice_script_entry_format[NOTES_LENGTH]; +extern int Voice_export_selection; +extern bool Voice_group_messages; + +extern SCP_string Voice_script_default_string; +extern SCP_string Voice_script_instructions_string; diff --git a/code/source_groups.cmake b/code/source_groups.cmake index 552d889370f..a6e882d8525 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -836,6 +836,12 @@ add_file_folder("Mission" mission/mission_flags.h ) +# MissionEditor file +add_file_folder("MissionEditor" + missioneditor/common.cpp + missioneditor/common.h +) + # MissionUI files add_file_folder("MissionUI" missionui/chatbox.cpp diff --git a/fred2/management.cpp b/fred2/management.cpp index cfb3ac233b0..534a7dc22a4 100644 --- a/fred2/management.cpp +++ b/fred2/management.cpp @@ -436,7 +436,7 @@ bool fred_init(std::unique_ptr&& graphicsOps) strcpy_s(Voice_abbrev_message, ""); strcpy_s(Voice_abbrev_mission, ""); Voice_no_replace_filenames = false; - strcpy_s(Voice_script_entry_format, "Sender: $sender\r\nPersona: $persona\r\nFile: $filename\r\nMessage: $message"); + strcpy_s(Voice_script_entry_format, Voice_script_default_string.c_str()); Voice_export_selection = 0; Show_waypoints = TRUE; diff --git a/fred2/management.h b/fred2/management.h index 47ba40ebb7d..00867f99507 100644 --- a/fred2/management.h +++ b/fred2/management.h @@ -14,6 +14,7 @@ #include "globalincs/pstypes.h" #include "jumpnode/jumpnode.h" #include "ship/ship.h" +#include "missioneditor/common.h" #include #define SHIP_FILTER_PLAYERS (1 << 0) // set: add players to list as well @@ -43,17 +44,6 @@ extern char* Docking_bay_list[]; extern char Fred_exe_dir[512]; extern char Fred_base_dir[512]; -// Goober5000 - for voice acting manager -extern char Voice_abbrev_briefing[NAME_LENGTH]; -extern char Voice_abbrev_campaign[NAME_LENGTH]; -extern char Voice_abbrev_command_briefing[NAME_LENGTH]; -extern char Voice_abbrev_debriefing[NAME_LENGTH]; -extern char Voice_abbrev_message[NAME_LENGTH]; -extern char Voice_abbrev_mission[NAME_LENGTH]; -extern bool Voice_no_replace_filenames; -extern char Voice_script_entry_format[NOTES_LENGTH]; -extern int Voice_export_selection; - // Goober5000 extern SCP_vector Show_iff; diff --git a/fred2/voiceactingmanager.cpp b/fred2/voiceactingmanager.cpp index 98250957eec..b2c144e963e 100644 --- a/fred2/voiceactingmanager.cpp +++ b/fred2/voiceactingmanager.cpp @@ -7,6 +7,7 @@ #include "freddoc.h" #include "VoiceActingManager.h" #include "globalincs/vmallocator.h" +#include "missioneditor/common.h" #include "missionui/missioncmdbrief.h" #include "mission/missionbriefcommon.h" #include "mission/missionmessage.h" @@ -22,24 +23,6 @@ static char THIS_FILE[] = __FILE__; #endif -#define INVALID_MESSAGE ((MMessage*)SIZE_T_MAX) - -// 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; -bool Voice_group_messages; - -constexpr int WINGMAN_PERSONAS = 0; -constexpr int NON_WINGMAN_PERSONAS = 1; -constexpr int SPECIFIC_PERSONAS_START_AT = 2; - ///////////////////////////////////////////////////////////////////////////// // VoiceActingManager dialog @@ -141,15 +124,7 @@ BOOL VoiceActingManager::OnInitDialog() box->SetCurSel(0); // this text is too long for the .rc file, so set it here - GetDlgItem(IDC_ENTRY_FORMAT_DESC)->SetWindowText( - "$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." - ); + GetDlgItem(IDC_ENTRY_FORMAT_DESC)->SetWindowText(Voice_script_instructions_string.c_str()); // load saved data for file names m_abbrev_briefing = _T(Voice_abbrev_briefing); @@ -993,17 +968,17 @@ bool VoiceActingManager::check_persona(int persona) { Assertion(SCP_vector_inbounds(Personas, persona), "The persona index provided to check_persona() is not in range!"); - if (m_which_persona_to_sync == WINGMAN_PERSONAS) + if (m_which_persona_to_sync == static_cast(PersonaSyncIndex::Wingman)) { return (Personas[persona].flags & PERSONA_FLAG_WINGMAN) != 0; } - else if (m_which_persona_to_sync == NON_WINGMAN_PERSONAS) + else if (m_which_persona_to_sync == static_cast(PersonaSyncIndex::NonWingman)) { return (Personas[persona].flags & PERSONA_FLAG_WINGMAN) == 0; } else { - int real_persona_to_sync = m_which_persona_to_sync - SPECIFIC_PERSONAS_START_AT; + int real_persona_to_sync = m_which_persona_to_sync - static_cast(PersonaSyncIndex::PersonasStart); Assertion(SCP_vector_inbounds(Personas, real_persona_to_sync), "The m_which_persona_to_sync dropdown index is not in range!"); return real_persona_to_sync == persona; } diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 1851f629110..60d643aa332 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -70,6 +70,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/ShieldSystemDialogModel.h src/mission/dialogs/VariableDialogModel.cpp src/mission/dialogs/VariableDialogModel.h + src/mission/dialogs/VoiceActingManagerModel.h + src/mission/dialogs/VoiceActingManagerModel.cpp src/mission/dialogs/WaypointEditorDialogModel.cpp src/mission/dialogs/WaypointEditorDialogModel.h src/mission/dialogs/WingEditorDialogModel.cpp diff --git a/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp b/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp new file mode 100644 index 00000000000..13de8b046c1 --- /dev/null +++ b/qtfred/src/mission/dialogs/VoiceActingManagerModel.cpp @@ -0,0 +1,961 @@ +#include "VoiceActingManagerModel.h" + +#include "globalincs/linklist.h" +#include "cfile/cfile.h" +#include "hud/hudtarget.h" +#include "iff_defs/iff_defs.h" +#include "mission/missiongoals.h" +#include "missioneditor/common.h" +#include "missionui/missioncmdbrief.h" +#include "mission/missionbriefcommon.h" +#include "parse/sexp.h" + +namespace fso::fred::dialogs { + +VoiceActingManagerModel::VoiceActingManagerModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + + initializeData(); +} + +bool VoiceActingManagerModel::apply() +{ + // Persist dialog settings back into globals + strcpy_s(Voice_abbrev_briefing, _abbrevBriefing.c_str()); + strcpy_s(Voice_abbrev_campaign, _abbrevCampaign.c_str()); + strcpy_s(Voice_abbrev_command_briefing, _abbrevCommandBriefing.c_str()); + strcpy_s(Voice_abbrev_debriefing, _abbrevDebriefing.c_str()); + strcpy_s(Voice_abbrev_message, _abbrevMessage.c_str()); + strcpy_s(Voice_abbrev_mission, _abbrevMission.c_str()); + + Voice_no_replace_filenames = _noReplace; + + strcpy_s(Voice_script_entry_format, _scriptEntryFormat.c_str()); + + switch (_exportSelection) { + case ExportSelection::CommandBriefings: + Voice_export_selection = 1; + break; + case ExportSelection::Briefings: + Voice_export_selection = 2; + break; + case ExportSelection::Debriefings: + Voice_export_selection = 3; + break; + case ExportSelection::Messages: + Voice_export_selection = 4; + break; + case ExportSelection::Everything: + default: + Voice_export_selection = 0; + break; + } + Voice_group_messages = _groupMessages; + + // Nothing in apply() modifies mission data directly. + return true; +} + +void VoiceActingManagerModel::reject() +{ + // do nothing +} + +void VoiceActingManagerModel::initializeData() +{ + _abbrevBriefing = Voice_abbrev_briefing; + _abbrevCampaign = Voice_abbrev_campaign; + _abbrevCommandBriefing = Voice_abbrev_command_briefing; + _abbrevDebriefing = Voice_abbrev_debriefing; + _abbrevMessage = Voice_abbrev_message; + _abbrevMission = Voice_abbrev_mission; + + _noReplace = Voice_no_replace_filenames; + _scriptEntryFormat = Voice_script_entry_format; + + if (_scriptEntryFormat.empty()) { + _scriptEntryFormat = Voice_script_default_string; + } + + switch (Voice_export_selection) { + case 1: + _exportSelection = ExportSelection::CommandBriefings; + break; + case 2: + _exportSelection = ExportSelection::Briefings; + break; + case 3: + _exportSelection = ExportSelection::Debriefings; + break; + case 4: + _exportSelection = ExportSelection::Messages; + break; + default: + _exportSelection = ExportSelection::Everything; + break; + } + _groupMessages = Voice_group_messages; + + _suffix = Suffix::WAV; + _includeSenderInFilename = false; + _whichPersonaToSync = 0; +} + +SCP_vector VoiceActingManagerModel::personaChoices() +{ + SCP_vector out; + out.emplace_back(""); + out.emplace_back(""); + for (const auto& persona : Personas) { + out.emplace_back(persona.name); + } + return out; +} + +SCP_vector VoiceActingManagerModel::fileChoices() +{ + SCP_vector out; + + for (int i = 0; i < static_cast(Suffix::numSuffixes); i++) { + switch (static_cast(i)) { + case Suffix::OGG: + out.emplace_back(".ogg"); + break; + case Suffix::WAV: + out.emplace_back(".wav"); + break; + default: + Assertion(false, "Invalid file type selected!"); + break; + } + } + + return out; +} + +SCP_string VoiceActingManagerModel::buildExampleFilename() const +{ + return generateFilename(_previewSelection, 1, 2, INVALID_MESSAGE); +} + +SCP_string VoiceActingManagerModel::pickExampleSection() const +{ + if (!_abbrevCommandBriefing.empty()) + return _abbrevCommandBriefing; + if (!_abbrevBriefing.empty()) + return _abbrevBriefing; + if (!_abbrevDebriefing.empty()) + return _abbrevDebriefing; + if (!_abbrevMessage.empty()) + return _abbrevMessage; + return ""; +} + +SCP_string VoiceActingManagerModel::getSuffixString() const +{ + switch (_suffix) { + case Suffix::OGG: + return ".ogg"; + case Suffix::WAV: + return ".wav"; + default: + Assertion(false, "Invalid file type selected!"); + return ".wav"; + } +} + +int VoiceActingManagerModel::calcDigits(int size) +{ + if (size >= 10000) + return 5; + if (size >= 1000) + return 4; + if (size >= 100) + return 3; + return 2; +} + +SCP_string VoiceActingManagerModel::generateFilename(ExportSelection sel, int number, int digits, const MMessage* message) const +{ + SCP_string prefix = _abbrevCampaign + _abbrevMission; + switch (sel) { + case ExportSelection::CommandBriefings: + prefix += _abbrevCommandBriefing; + break; + case ExportSelection::Briefings: + prefix += _abbrevBriefing; + break; + case ExportSelection::Debriefings: + prefix += _abbrevDebriefing; + break; + case ExportSelection::Messages: + prefix += _abbrevMessage; + break; + default: + Assertion(false, "Invalid export selection for filename generation!"); + prefix = ""; // Fallback, shouldn't happen + } + + SCP_string num = std::to_string(number); + while (static_cast(num.size()) < digits) + num.insert(0, "0"); + + SCP_string out = prefix + num; + + // optional sender suffix + if (message != nullptr && _includeSenderInFilename) { + const auto suffix = getSuffixString(); + const auto currently = out + suffix; + + const size_t allow_to_copy = NAME_LENGTH - size_t(currently.size()); + char sender[NAME_LENGTH]{}; + + if (message == INVALID_MESSAGE) { + strcpy_s(sender, "Alpha 1"); + } else { + getValidSender(sender, sizeof(sender), message); + } + + // truncate to avoid overflow + if (allow_to_copy < strlen(sender)) { + sender[allow_to_copy] = '\0'; + } + + // sanitize -> lowercase and replace non-alnum with '_' + for (size_t j = 0; sender[j] != '\0'; ++j) { + sender[j] = SCP_tolower(sender[j]); + if (!isalnum(static_cast(sender[j]))) + sender[j] = '_'; + } + // compress consecutive underscores + for (size_t j = 1; sender[j] != '\0';) { + if (sender[j - 1] == '_' && sender[j] == '_') { + // shift left + size_t k = j + 1; + while (sender[k] != '\0') { + sender[k - 1] = sender[k]; + ++k; + } + sender[k - 1] = '\0'; + } else { + ++j; + } + } + out += sender; + } + + out += getSuffixString(); + Assertion(out.size() < NAME_LENGTH, "Generated filename exceeds NAME_LENGTH"); + return out; +} + +int VoiceActingManagerModel::generateFilenames() +{ + int num_modified = 0; + + // Command Briefings + { + const int digits = calcDigits(Cmd_briefs[0].num_stages); + for (int i = 0; i < Cmd_briefs[0].num_stages; ++i) { + auto* filename = Cmd_briefs[0].stage[i].wave_filename; + if (!_noReplace || !strlen(filename) || message_filename_is_generic(filename)) { + SCP_string s = generateFilename(ExportSelection::CommandBriefings, i + 1, digits, INVALID_MESSAGE); + if (strcmp(filename, s.c_str()) != 0) { + strcpy(filename, s.c_str()); + ++num_modified; + } + } + } + } + + // Briefings + { + const int digits = calcDigits(Briefings[0].num_stages); + for (int i = 0; i < Briefings[0].num_stages; ++i) { + auto* filename = Briefings[0].stages[i].voice; + if (!_noReplace || !strlen(filename) || message_filename_is_generic(filename)) { + SCP_string s = generateFilename(ExportSelection::Briefings, i + 1, digits, INVALID_MESSAGE); + if (strcmp(filename, s.c_str()) != 0) { + strcpy(filename, s.c_str()); + ++num_modified; + } + } + } + } + + // Debriefings + { + const int digits = calcDigits(Debriefings[0].num_stages); + for (int i = 0; i < Debriefings[0].num_stages; ++i) { + auto* filename = Debriefings[0].stages[i].voice; + if (!_noReplace || !strlen(filename) || message_filename_is_generic(filename)) { + SCP_string s = generateFilename(ExportSelection::Debriefings, i + 1, digits, INVALID_MESSAGE); + if (strcmp(filename, s.c_str()) != 0) { + strcpy(filename, s.c_str()); + ++num_modified; + } + } + } + } + + // Messages + { + const int digits = calcDigits(Num_messages - Num_builtin_messages); + for (int i = 0; i < Num_messages - Num_builtin_messages; ++i) { + auto* message = &Messages[i + Num_builtin_messages]; + const char* filename = message->wave_info.name; + if (!_noReplace || !strlen(filename) || message_filename_is_generic(filename)) { + SCP_string s = generateFilename(ExportSelection::Messages, i + 1, digits, message); + if (message->wave_info.name == nullptr || strcmp(message->wave_info.name, s.c_str()) != 0) { + if (message->wave_info.name) + free(message->wave_info.name); + message->wave_info.name = strdup(s.c_str()); + ++num_modified; + } + } + } + } + + if (num_modified > 0) + set_modified(); + return num_modified; +} + +bool VoiceActingManagerModel::fout(void* fp, const char* format, ...) +{ + SCP_string str; + va_list args; + va_start(args, format); + vsprintf(str, format, args); + va_end(args); + cfputs(str.c_str(), static_cast(fp)); + return true; +} + +static inline void replace_all(std::string& s, const std::string& from, const std::string& to) +{ + if (from.empty()) + return; + std::size_t pos = 0; + while ((pos = s.find(from, pos)) != std::string::npos) { + s.replace(pos, from.size(), to); + pos += to.size(); + } +} + +bool VoiceActingManagerModel::generateScript(const SCP_string& absoluteFilePath) +{ + auto* fp = cfopen(absoluteFilePath.c_str(), "wt"); + if (!fp) + return false; + + // Mission metadata + fout(fp, "%s\n", Mission_filename); + fout(fp, "%s\n\n", The_mission.name); + + auto writeMessageEntry = [&](const char* filename, + const SCP_string& text, + const char* persona, + const char* sender, + const char* name, + const char* note) { + SCP_string entry = _scriptEntryFormat; + replace_all(entry, "\r\n", "\n"); // normalize endings + + // map nulls + const char* filename_safe = filename ? filename : ""; + const char* persona_safe = persona ? persona : ""; + const char* sender_safe = sender ? sender : ""; + const char* name_safe = name ? name : ""; + const char* note_safe = note ? note : ""; + + // replace + replace_all(entry, "$filename", filename_safe); + replace_all(entry, "$message", text); + replace_all(entry, "$persona", persona_safe); + replace_all(entry, "$sender", sender_safe); + replace_all(entry, "$name", name_safe); + replace_all(entry, "$note", note_safe); + + fout(fp, "%s\n\n\n", entry.c_str()); + }; + + // Command Briefings + if (_exportSelection == ExportSelection::Everything || _exportSelection == ExportSelection::CommandBriefings) { + fout(fp, "\n\nCommand Briefings\n-----------------\n\n"); + for (int i = 0; i < Cmd_briefs[0].num_stages; ++i) { + auto* stage = &Cmd_briefs[0].stage[i]; + writeMessageEntry(stage->wave_filename, + stage->text, + "", + "", + "", + ""); + } + } + + // Briefings + if (_exportSelection == ExportSelection::Everything || _exportSelection == ExportSelection::Briefings) { + fout(fp, "\n\nBriefings\n---------\n\n"); + for (int i = 0; i < Briefings[0].num_stages; ++i) { + auto* stage = &Briefings[0].stages[i]; + writeMessageEntry(stage->voice, + stage->text, + "", + "", + "", + ""); + } + } + + // Debriefings + if (_exportSelection == ExportSelection::Everything || _exportSelection == ExportSelection::Debriefings) { + fout(fp, "\n\nDebriefings\n-----------\n\n"); + for (int i = 0; i < Debriefings[0].num_stages; ++i) { + auto* stage = &Debriefings[0].stages[i]; + writeMessageEntry(stage->voice, + stage->text, + "", + "", + "", + ""); + } + } + + // Messages + if (_exportSelection == ExportSelection::Everything || _exportSelection == ExportSelection::Messages) { + fout(fp, "\n\nMessages\n--------\n\n"); + + if (_groupMessages || _exportSelection == ExportSelection::Everything) { + SCP_vector messageIndexes; + messageIndexes.reserve(Num_messages - Num_builtin_messages); + for (int i = 0; i < Num_messages - Num_builtin_messages; ++i) + messageIndexes.emplace_back(i + Num_builtin_messages); + + groupMessageIndexes(messageIndexes); + for (int idx : messageIndexes) { + const auto* msg = &Messages[idx]; + + char sender[NAME_LENGTH + 1]{}; + getValidSender(sender, sizeof(sender), msg); + + const char* persona = (msg->persona_index >= 0) ? Personas[msg->persona_index].name : ""; + const char* senderOut = (sender[0] == '#') ? &sender[1] : sender; + writeMessageEntry(msg->wave_info.name, msg->message, persona, senderOut, msg->name, msg->note.c_str()); + } + } else { + for (int i = 0; i < Num_messages - Num_builtin_messages; ++i) { + const auto* msg = &Messages[i + Num_builtin_messages]; + + char sender[NAME_LENGTH + 1]{}; + getValidSender(sender, sizeof(sender), msg); + + const char* persona = (msg->persona_index >= 0) ? Personas[msg->persona_index].name : ""; + const char* senderOut = (sender[0] == '#') ? &sender[1] : sender; + writeMessageEntry(msg->wave_info.name, msg->message, persona, senderOut, msg->name, msg->note.c_str()); + } + } + } + + cfclose(fp); + return true; +} + +static inline void assign_if_different(int& dest, int src, int& modified) +{ + if (dest != src) { + dest = src; + ++modified; + } +} +static inline void strdup_if_different(char*& dest, const char* src, int& modified) +{ + if (dest == nullptr || strcmp(dest, src) != 0) { + if (dest) + free(dest); + dest = strdup(src); + ++modified; + } +} + +int VoiceActingManagerModel::copyMessagePersonasToShips() +{ + int modified = 0; + SCP_unordered_set alreadyAssigned; + SCP_string inconsistent; + + char senderBuf[NAME_LENGTH]{}; + int senderShip = -1; + bool isCommand = false; + + for (int i = 0; i < Num_messages - Num_builtin_messages; ++i) { + auto* msg = &Messages[i + Num_builtin_messages]; + + getValidSender(senderBuf, NAME_LENGTH, msg, &senderShip, &isCommand); + auto* shipp = (senderShip < 0) ? nullptr : &Ships[senderShip]; + + int personaToCopy = msg->persona_index; + if (personaToCopy >= 0 && checkPersonaFilter(personaToCopy) && shipp) { + if (alreadyAssigned.count(senderShip) && shipp->persona_index != personaToCopy) { + inconsistent += "\n\u2022 "; + inconsistent += shipp->ship_name; + } + alreadyAssigned.insert(senderShip); + assign_if_different(shipp->persona_index, personaToCopy, modified); + } + } + + if (modified > 0) + set_modified(); + + return modified; +} + +int VoiceActingManagerModel::copyShipPersonasToMessages() +{ + int modified = 0; + SCP_unordered_set alreadyAssigned; + SCP_string inconsistent; + + char senderBuf[NAME_LENGTH]{}; + int senderShip = -1; + bool isCommand = false; + + for (int i = 0; i < Num_messages - Num_builtin_messages; ++i) { + auto* msg = &Messages[i + Num_builtin_messages]; + + getValidSender(senderBuf, NAME_LENGTH, msg, &senderShip, &isCommand); + const auto* shipp = (senderShip < 0) ? nullptr : &Ships[senderShip]; + + int personaToCopy = -1; + if (isCommand) + personaToCopy = The_mission.command_persona; + else if (shipp) + personaToCopy = shipp->persona_index; + + if (personaToCopy >= 0 && checkPersonaFilter(personaToCopy)) { + if (alreadyAssigned.count(i) && msg->persona_index != personaToCopy) { + inconsistent += "\n\u2022 "; + inconsistent += msg->name; + } + alreadyAssigned.insert(i); + assign_if_different(msg->persona_index, personaToCopy, modified); + } + } + + if (modified > 0) + set_modified(); + return modified; +} + +int VoiceActingManagerModel::clearPersonasFromNonSenders() +{ + SCP_unordered_set allSenders; + + char senderBuf[NAME_LENGTH]{}; + int senderShip = -1; + + // Gather all ships that actually send a message + for (int i = 0; i < Num_messages - Num_builtin_messages; ++i) { + auto* msg = &Messages[i + Num_builtin_messages]; + getValidSender(senderBuf, NAME_LENGTH, msg, &senderShip); + if (senderShip >= 0) + allSenders.insert(senderShip); + } + + int modified = 0; + for (auto objp : list_range(&obj_used_list)) { + if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { + auto& ship = Ships[objp->instance]; + if (allSenders.count(objp->instance) == 0) { + if (ship.persona_index >= 0 && checkPersonaFilter(ship.persona_index)) { + assign_if_different(ship.persona_index, -1, modified); + } + } + } + } + + if (modified > 0) + set_modified(); + return modified; +} + +int VoiceActingManagerModel::setHeadAnisUsingMessagesTbl() +{ + int modified = 0; + + for (int i = 0; i < Num_messages - Num_builtin_messages; ++i) { + auto* msg = &Messages[i + Num_builtin_messages]; + if (msg->persona_index < 0) + continue; + if (!checkPersonaFilter(msg->persona_index)) + continue; + + // find builtin message that shares this persona + bool found = false; + for (int j = 0; j < Num_builtin_messages; ++j) { + const auto* builtin = &Messages[j]; + if (builtin->persona_index == msg->persona_index) { + strdup_if_different(msg->avi_info.name, builtin->avi_info.name, modified); + found = true; + break; + } + } + if (!found) { + Warning(LOCATION, "Persona index %d not found in builtin messages (messages.tbl)!", msg->persona_index); + } + } + + if (modified > 0) + set_modified(); + return modified; +} + +AnyWingmanCheckResult VoiceActingManagerModel::checkAnyWingmanPersonas() +{ + AnyWingmanCheckResult result; + char senderBuf[NAME_LENGTH]{}; + + for (int i = 0; i < Num_messages - Num_builtin_messages; ++i) { + const auto* msg = &Messages[i + Num_builtin_messages]; + + getValidSender(senderBuf, NAME_LENGTH, msg, nullptr, nullptr); + if (stricmp(senderBuf, "") != 0) + continue; + + result.anyWingmanFound = true; + + // message must have a wingman persona and at least one ship with that persona + if (msg->persona_index < 0) { + ++result.issueCount; + result.report += SCP_string("\n\"") + msg->name + "\" - does not have a persona"; + continue; + } + if ((Personas[msg->persona_index].flags & PERSONA_FLAG_WINGMAN) == 0) { + ++result.issueCount; + result.report += SCP_string("\n\"") + msg->name + "\" - does not have a wingman persona"; + continue; + } + + bool foundPotentialSender = false; + for (auto objp : list_range(&obj_used_list)) { + if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { + if (Ships[objp->instance].persona_index == msg->persona_index) { + foundPotentialSender = true; + break; + } + } + } + if (!foundPotentialSender) { + ++result.issueCount; + + const char* msg_name = msg->name; + const char* persona_name = Personas[msg->persona_index].name; + + result.report += std::string("\n\"") + msg_name + "\" - no ship with persona \"" + persona_name + "\" was found"; + } + } + return result; +} + +const char* VoiceActingManagerModel::getMessageSender(const MMessage* message) +{ + for (int i = 0; i < Num_sexp_nodes; ++i) { + if (Sexp_nodes[i].type == SEXP_NOT_USED) + continue; + + const int op = get_operator_const(i); + int n = CDR(i); + + if (op == OP_SEND_MESSAGE) { + if (!strcmp(message->name, Sexp_nodes[CDDR(n)].text)) + return Sexp_nodes[n].text; + } else if (op == OP_SEND_MESSAGE_LIST || op == OP_SEND_MESSAGE_CHAIN) { + if (op == OP_SEND_MESSAGE_CHAIN) + n = CDR(n); + while (n != -1) { + if (!strcmp(message->name, Sexp_nodes[CDDR(n)].text)) + return Sexp_nodes[n].text; + n = CDDDDR(n); + } + } else if (op == OP_SEND_RANDOM_MESSAGE) { + char* sender = Sexp_nodes[n].text; + n = CDDR(n); + while (n != -1) { + if (!strcmp(message->name, Sexp_nodes[n].text)) + return sender; + n = CDR(n); + } + } else if (op == OP_TRAINING_MSG) { + if (!strcmp(message->name, Sexp_nodes[n].text)) + return "Training Message"; + } + } + return ""; +} + +void VoiceActingManagerModel::getValidSender(char* sender, + size_t sender_size, + const MMessage* message, + int* sender_shipnum, + bool* is_command) +{ + Assert(sender != nullptr); + Assert(message != nullptr); + + memset(sender, 0, sender_size); + strncpy(sender, getMessageSender(message), sender_size - 1); + + if (!strcmp("#Command", sender)) { + if (is_command) + *is_command = true; + + if (The_mission.flags[Mission::Mission_Flags::Override_hashcommand]) { + memset(sender, 0, sender_size); + strncpy(sender, The_mission.command_sender, sender_size - 1); + } + } else { + if (is_command) + *is_command = false; + } + + // strip leading '#' + if (sender[0] == '#') { + size_t i = 1; + for (; sender[i] != '\0'; ++i) + sender[i - 1] = sender[i]; + sender[i - 1] = '\0'; + } + + const int shipnum = ship_name_lookup(sender, 1); + if (sender_shipnum) + *sender_shipnum = shipnum; + + if (shipnum >= 0) { + ship* shipp = &Ships[shipnum]; + + if (*Fred_callsigns[shipnum]) { + hud_stuff_ship_callsign(sender, shipp); + } else if (((Iff_info[shipp->team].flags & IFFF_WING_NAME_HIDDEN) && (shipp->wingnum != -1)) || + (shipp->flags[Ship::Ship_Flags::Hide_ship_name])) { + hud_stuff_ship_class(sender, shipp); + } else { + memset(sender, 0, sender_size); + strncpy(sender, shipp->get_display_name(), sender_size - 1); + } + } +} + +void VoiceActingManagerModel::groupMessageIndexes(SCP_vector& messageIndexes) +{ + const auto initialSize = messageIndexes.size(); + SCP_vector source = messageIndexes; + messageIndexes.clear(); + + for (const auto& ev : Mission_events) + groupMessageIndexesInTree(ev.formula, source, messageIndexes); + + // append remaining + for (int idx : source) + messageIndexes.push_back(idx); + +#ifndef NDEBUG + if (initialSize != messageIndexes.size()) { + // parity check + Warning(LOCATION, "groupMessageIndexes changed list size (%d -> %d)", static_cast(initialSize), static_cast(messageIndexes.size())); + } +#endif +} + +void VoiceActingManagerModel::groupMessageIndexesInTree(int node, SCP_vector& source, SCP_vector& dest) +{ + if (node < 0) + return; + if (Sexp_nodes[node].type == SEXP_NOT_USED) + return; + + const int op = get_operator_const(node); + int n = CDR(node); + + if (op == OP_SEND_MESSAGE_LIST || op == OP_SEND_MESSAGE_CHAIN) { + if (op == OP_SEND_MESSAGE_CHAIN) + n = CDR(n); + while (n != -1) { + char* message_name = Sexp_nodes[CDDR(n)].text; + for (int i = 0; i < static_cast(source.size()); ++i) { + if (!strcmp(message_name, Messages[source[i]].name)) { + dest.push_back(source[i]); + source.erase(source.begin() + i); + break; + } + } + n = CDDDDR(n); + } + } else if (op == OP_SEND_RANDOM_MESSAGE) { + n = CDDR(n); + while (n != -1) { + char* message_name = Sexp_nodes[n].text; + for (int i = 0; i < static_cast(source.size()); ++i) { + if (!strcmp(message_name, Messages[source[i]].name)) { + dest.push_back(source[i]); + source.erase(source.begin() + i); + break; + } + } + n = CDR(n); + } + } + + groupMessageIndexesInTree(CAR(node), source, dest); + groupMessageIndexesInTree(CDR(node), source, dest); +} + +bool VoiceActingManagerModel::checkPersonaFilter(int persona) const +{ + Assertion(SCP_vector_inbounds(Personas, persona), "Persona index out of range in checkPersonaFilter()"); + if (_whichPersonaToSync == static_cast(PersonaSyncIndex::Wingman)) { + return (Personas[persona].flags & PERSONA_FLAG_WINGMAN) != 0; + } else if (_whichPersonaToSync == static_cast(PersonaSyncIndex::NonWingman)) { + return (Personas[persona].flags & PERSONA_FLAG_WINGMAN) == 0; + } else { + const int specific = _whichPersonaToSync - static_cast(PersonaSyncIndex::PersonasStart); + Assertion(SCP_vector_inbounds(Personas, specific), "Dropdown persona index out of range"); + return specific == persona; + } +} + +SCP_string VoiceActingManagerModel::abbrevBriefing() const +{ + return _abbrevBriefing; +} +SCP_string VoiceActingManagerModel::abbrevCampaign() const +{ + return _abbrevCampaign; +} +SCP_string VoiceActingManagerModel::abbrevCommandBriefing() const +{ + return _abbrevCommandBriefing; +} +SCP_string VoiceActingManagerModel::abbrevDebriefing() const +{ + return _abbrevDebriefing; +} +SCP_string VoiceActingManagerModel::abbrevMessage() const +{ + return _abbrevMessage; +} +SCP_string VoiceActingManagerModel::abbrevMission() const +{ + return _abbrevMission; +} + +void VoiceActingManagerModel::setAbbrevBriefing(const SCP_string& v) +{ + modify(_abbrevBriefing, v); +} +void VoiceActingManagerModel::setAbbrevCampaign(const SCP_string& v) +{ + modify(_abbrevCampaign, v); +} +void VoiceActingManagerModel::setAbbrevCommandBriefing(const SCP_string& v) +{ + modify(_abbrevCommandBriefing, v); +} +void VoiceActingManagerModel::setAbbrevDebriefing(const SCP_string& v) +{ + modify(_abbrevDebriefing, v); +} +void VoiceActingManagerModel::setAbbrevMessage(const SCP_string& v) +{ + modify(_abbrevMessage, v); +} +void VoiceActingManagerModel::setAbbrevMission(const SCP_string& v) +{ + modify(_abbrevMission, v); +} + +void VoiceActingManagerModel::setAbbrevSelection(ExportSelection sel) +{ + switch (sel) { + case ExportSelection::CommandBriefings: + _previewSelection = ExportSelection::CommandBriefings; + break; + case ExportSelection::Briefings: + _previewSelection = ExportSelection::Briefings; + break; + case ExportSelection::Debriefings: + _previewSelection = ExportSelection::Debriefings; + break; + case ExportSelection::Messages: + _previewSelection = ExportSelection::Messages; + break; + default: // Other options not allowed so no change! + break; + } +} + +bool VoiceActingManagerModel::includeSenderInFilename() const +{ + return _includeSenderInFilename; +} +void VoiceActingManagerModel::setIncludeSenderInFilename(bool v) +{ + modify(_includeSenderInFilename, v); +} + +bool VoiceActingManagerModel::noReplace() const +{ + return _noReplace; +} +void VoiceActingManagerModel::setNoReplace(bool v) +{ + modify(_noReplace, v); +} + +Suffix VoiceActingManagerModel::suffix() const +{ + return _suffix; +} +void VoiceActingManagerModel::setSuffix(Suffix s) +{ + modify(_suffix, s); +} + +SCP_string VoiceActingManagerModel::scriptEntryFormat() const +{ + return _scriptEntryFormat; +} +void VoiceActingManagerModel::setScriptEntryFormat(const SCP_string& v) +{ + modify(_scriptEntryFormat, v); +} + +ExportSelection VoiceActingManagerModel::exportSelection() const +{ + return _exportSelection; +} +void VoiceActingManagerModel::setExportSelection(ExportSelection sel) +{ + modify(_exportSelection, sel); +} + +bool VoiceActingManagerModel::groupMessages() const +{ + return _groupMessages; +} +void VoiceActingManagerModel::setGroupMessages(bool v) +{ + modify(_groupMessages, v); +} + +int VoiceActingManagerModel::whichPersonaToSync() const +{ + return _whichPersonaToSync; +} +void VoiceActingManagerModel::setWhichPersonaToSync(int idx) +{ + modify(_whichPersonaToSync, idx); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/VoiceActingManagerModel.h b/qtfred/src/mission/dialogs/VoiceActingManagerModel.h new file mode 100644 index 00000000000..0429998e125 --- /dev/null +++ b/qtfred/src/mission/dialogs/VoiceActingManagerModel.h @@ -0,0 +1,150 @@ +#pragma once +#include "AbstractDialogModel.h" + +#include "mission/missionmessage.h" + +namespace fso::fred::dialogs { + + enum class Suffix { + WAV, + OGG, + + numSuffixes + }; + + enum class ExportSelection { + Everything, + CommandBriefings, + Briefings, + Debriefings, + Messages + }; + + struct AnyWingmanCheckResult { + bool anyWingmanFound = false; + int issueCount = 0; + SCP_string report; // empty = all good + }; + +class VoiceActingManagerModel : public AbstractDialogModel { + Q_OBJECT + public: + explicit VoiceActingManagerModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; + + // Abbreviations + SCP_string abbrevBriefing() const; + SCP_string abbrevCampaign() const; + SCP_string abbrevCommandBriefing() const; + SCP_string abbrevDebriefing() const; + SCP_string abbrevMessage() const; + SCP_string abbrevMission() const; + + void setAbbrevBriefing(const SCP_string& v); + void setAbbrevCampaign(const SCP_string& v); + void setAbbrevCommandBriefing(const SCP_string& v); + void setAbbrevDebriefing(const SCP_string& v); + void setAbbrevMessage(const SCP_string& v); + void setAbbrevMission(const SCP_string& v); + + void setAbbrevSelection(ExportSelection sel); + + // Filename settings + bool includeSenderInFilename() const; + void setIncludeSenderInFilename(bool v); + + bool noReplace() const; + void setNoReplace(bool v); + + Suffix suffix() const; + void setSuffix(Suffix s); + + // Script export + SCP_string scriptEntryFormat() const; + void setScriptEntryFormat(const SCP_string& v); + + ExportSelection exportSelection() const; + void setExportSelection(ExportSelection sel); + + bool groupMessages() const; + void setGroupMessages(bool v); + + // Persona sync dropdown index: + // 0 = , 1 = , 2+ = specific persona index + int whichPersonaToSync() const; + void setWhichPersonaToSync(int idx); + + // Populates "", "", then all persona names + static SCP_vector personaChoices(); + static SCP_vector fileChoices(); + + // Builds example filename using current settings + // prefers command->brief->debrief->message ordering + SCP_string buildExampleFilename() const; + + // Returns number of filenames modified across command/brief/debrief/messages + int generateFilenames(); + + // Writes a script file. Path must be absolute. + bool generateScript(const SCP_string& absoluteFilePath); + + // Copy personas in one direction, restricted by whichPersonaToSync selection + // Returns number of modified items + int copyMessagePersonasToShips(); + int copyShipPersonasToMessages(); + + // Clear personas from ships that never send a message and returns count cleared + int clearPersonasFromNonSenders(); + + // Set message head ANIs by matching builtin messages with same persona and returns count modified + int setHeadAnisUsingMessagesTbl(); + + // Validate messages + static AnyWingmanCheckResult checkAnyWingmanPersonas(); + + signals: + + + private slots: + + + private: // NOLINT(readability-redundant-access-specifiers) + SCP_string _abbrevBriefing; + SCP_string _abbrevCampaign; + SCP_string _abbrevCommandBriefing; + SCP_string _abbrevDebriefing; + SCP_string _abbrevMessage; + SCP_string _abbrevMission; + + bool _includeSenderInFilename = false; + bool _noReplace = false; + Suffix _suffix = Suffix::WAV; + ExportSelection _previewSelection = ExportSelection::CommandBriefings; // A little hacky to re-use this enum.. but it's convenient + + SCP_string _scriptEntryFormat; + ExportSelection _exportSelection = ExportSelection::Everything; + bool _groupMessages = false; + + int _whichPersonaToSync = 0; + + void initializeData(); + + SCP_string getSuffixString() const; // ".wav" or ".ogg" + static int calcDigits(int size); // 2..5 + SCP_string pickExampleSection() const; // chooses which abbrev to demo + SCP_string generateFilename(ExportSelection sel, int number, int digits, const MMessage* messageOrNull) const; + + // Classic helpers adapted + static const char* getMessageSender(const MMessage* message); + static void getValidSender(char* out, size_t outSize, const MMessage* message, int* outSenderShipnum = nullptr, bool* outIsCommand = nullptr); + static void groupMessageIndexes(SCP_vector& messageIndexes); + static void groupMessageIndexesInTree(int node, SCP_vector& sourceList, SCP_vector& destList); + bool checkPersonaFilter(int persona) const; + + static bool fout(void* cfilePtr, const char* fmt, ...); // cfilePtr is CFILE* + +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/VoiceActingManager.cpp b/qtfred/src/ui/dialogs/VoiceActingManager.cpp index 6287288e4e4..30c5c6b8ee8 100644 --- a/qtfred/src/ui/dialogs/VoiceActingManager.cpp +++ b/qtfred/src/ui/dialogs/VoiceActingManager.cpp @@ -2,23 +2,338 @@ #include "ui_VoiceActingManager.h" +#include "missioneditor/common.h" -namespace fso { -namespace fred { -namespace dialogs { +#include +#include -VoiceActingManager::VoiceActingManager(FredView* parent, EditorViewport* viewport) : - QDialog(parent), ui(new Ui::VoiceActingManager()), - _viewport(viewport) { + +namespace fso::fred::dialogs { + +VoiceActingManager::VoiceActingManager(FredView* parent, EditorViewport* viewport) + : QDialog(parent), _viewport(viewport), ui(new Ui::VoiceActingManager()), + _model(new VoiceActingManagerModel(this, viewport)) +{ ui->setupUi(this); + + // Install this dialog as the event filter on the abbrev fields + ui->abbrevBriefingLineEdit->installEventFilter(this); + ui->abbrevCampaignLineEdit->installEventFilter(this); + ui->abbrevCommandBriefingLineEdit->installEventFilter(this); + ui->abbrevDebriefingLineEdit->installEventFilter(this); + ui->abbrevMessageLineEdit->installEventFilter(this); + ui->abbrevMissionLineEdit->installEventFilter(this); + + populatePersonaCombo(); + populateSuffixCombo(); + initializeUi(); + updateUi(); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); } -VoiceActingManager::~VoiceActingManager() { +VoiceActingManager::~VoiceActingManager() = default; + +void VoiceActingManager::closeEvent(QCloseEvent* e) +{ + _model->apply(); + e->accept(); //close +} + +bool VoiceActingManager::eventFilter(QObject* obj, QEvent* ev) +{ + if (ev->type() == QEvent::FocusIn) { + // Only react for our four main abbrev fields + if (obj == ui->abbrevBriefingLineEdit || obj == ui->abbrevCommandBriefingLineEdit || + obj == ui->abbrevDebriefingLineEdit || obj == ui->abbrevMessageLineEdit) { + + if (obj == ui->abbrevCommandBriefingLineEdit) { + _model->setAbbrevSelection(ExportSelection::CommandBriefings); + } + + if (obj == ui->abbrevBriefingLineEdit) { + _model->setAbbrevSelection(ExportSelection::Briefings); + } + + if (obj == ui->abbrevDebriefingLineEdit) { + _model->setAbbrevSelection(ExportSelection::Debriefings); + } + + if (obj == ui->abbrevMessageLineEdit) { + _model->setAbbrevSelection(ExportSelection::Messages); + } + refreshExampleFilename(); + } + } + // Let normal processing continue + return QDialog::eventFilter(obj, ev); +} + +void VoiceActingManager::initializeUi() +{ + // Abbreviations + ui->abbrevBriefingLineEdit->setText(QString::fromStdString(_model->abbrevBriefing())); + ui->abbrevCampaignLineEdit->setText(QString::fromStdString(_model->abbrevCampaign())); + ui->abbrevCommandBriefingLineEdit->setText(QString::fromStdString(_model->abbrevCommandBriefing())); + ui->abbrevDebriefingLineEdit->setText(QString::fromStdString(_model->abbrevDebriefing())); + ui->abbrevMessageLineEdit->setText(QString::fromStdString(_model->abbrevMessage())); + ui->abbrevMissionLineEdit->setText(QString::fromStdString(_model->abbrevMission())); + + // Filename settings + ui->includeSenderCheckBox->setChecked(_model->includeSenderInFilename()); + ui->replaceCheckBox->setChecked(_model->noReplace()); + ui->suffixComboBox->setCurrentIndex(suffixToIndex(_model->suffix())); + + // Script export + ui->scriptEntryFormatPlainTextEdit->setPlainText(QString::fromStdString(_model->scriptEntryFormat())); + ui->exportAllRadio->setChecked(_model->exportSelection() == ExportSelection::Everything); + ui->exportCmdBriefingRadio->setChecked(_model->exportSelection() == ExportSelection::CommandBriefings); + ui->exportBriefingRadio->setChecked(_model->exportSelection() == ExportSelection::Briefings); + ui->exportDebriefingRadio->setChecked(_model->exportSelection() == ExportSelection::Debriefings); + ui->exportMessageRadio->setChecked(_model->exportSelection() == ExportSelection::Messages); + ui->groupMessagesCheckBox->setChecked(_model->groupMessages()); + + ui->scriptLegendLabel->setText(QString::fromStdString(Voice_script_instructions_string)); + + // Persona sync + ui->personaSyncComboBox->setCurrentIndex(_model->whichPersonaToSync()); +} + +void VoiceActingManager::updateUi() +{ + refreshExampleFilename(); +} + +void VoiceActingManager::refreshExampleFilename() +{ + const auto ex = _model->buildExampleFilename(); + ui->exampleFilenameLabel->setText(QString::fromStdString(ex)); +} + +void VoiceActingManager::populatePersonaCombo() +{ + for (const auto& p : _model->personaChoices()) { + ui->personaSyncComboBox->addItem(QString::fromStdString(p)); + } +} + +void VoiceActingManager::populateSuffixCombo() +{ + for (const auto& s : _model->fileChoices()) { + ui->suffixComboBox->addItem(QString::fromStdString(s)); + } +} + +void VoiceActingManager::syncGroupMessagesEnabled() +{ + const auto sel = _model->exportSelection(); + const bool enable = (sel == ExportSelection::Everything || sel == ExportSelection::Messages); + ui->groupMessagesCheckBox->setEnabled(enable); +} + +int VoiceActingManager::exportSelectionToIndex(ExportSelection sel) +{ + switch (sel) { + case ExportSelection::Everything: + return 0; + case ExportSelection::CommandBriefings: + return 1; + case ExportSelection::Briefings: + return 2; + case ExportSelection::Debriefings: + return 3; + case ExportSelection::Messages: + return 4; + default: + Assertion(false, "Invalid export selection!"); + return 0; + } +} + +int VoiceActingManager::suffixToIndex(Suffix s) +{ + switch (s) { + case Suffix::WAV: + return 0; + case Suffix::OGG: + return 1; + default: + Assertion(false, "Invalid file type selected!"); + return 0; + } +} + +Suffix VoiceActingManager::indexToSuffix(int idx) +{ + switch (idx) { + case 0: + return Suffix::WAV; + case 1: + return Suffix::OGG; + default: + Assertion(false, "Invalid file type selected!"); + return Suffix::WAV; + } } +void VoiceActingManager::on_abbrevBriefingLineEdit_textEdited(const QString& text) +{ + _model->setAbbrevBriefing(text.toUtf8().constData()); + refreshExampleFilename(); } +void VoiceActingManager::on_abbrevCampaignLineEdit_textEdited(const QString& text) +{ + _model->setAbbrevCampaign(text.toUtf8().constData()); + refreshExampleFilename(); } +void VoiceActingManager::on_abbrevCommandBriefingLineEdit_textEdited(const QString& text) +{ + _model->setAbbrevCommandBriefing(text.toUtf8().constData()); + refreshExampleFilename(); } +void VoiceActingManager::on_abbrevDebriefingLineEdit_textEdited(const QString& text) +{ + _model->setAbbrevDebriefing(text.toUtf8().constData()); + refreshExampleFilename(); +} +void VoiceActingManager::on_abbrevMessageLineEdit_textEdited(const QString& text) +{ + _model->setAbbrevMessage(text.toUtf8().constData()); + refreshExampleFilename(); +} +void VoiceActingManager::on_abbrevMissionLineEdit_textEdited(const QString& text) +{ + _model->setAbbrevMission(text.toUtf8().constData()); + refreshExampleFilename(); +} + +void VoiceActingManager::on_includeSenderCheckBox_toggled(bool checked) +{ + _model->setIncludeSenderInFilename(checked); + refreshExampleFilename(); +} +void VoiceActingManager::on_noReplaceCheckBox_toggled(bool checked) +{ + _model->setNoReplace(checked); +} +void VoiceActingManager::on_suffixComboBox_currentIndexChanged(int index) +{ + _model->setSuffix(indexToSuffix(index)); + refreshExampleFilename(); +} + +void VoiceActingManager::on_scriptEntryFormatPlainTextEdit_textChanged() +{ + _model->setScriptEntryFormat(ui->scriptEntryFormatPlainTextEdit->toPlainText().toUtf8().constData()); +} + +void VoiceActingManager::on_exportAllRadio_toggled(bool checked) +{ + if (checked) { + _model->setExportSelection(ExportSelection::Everything); + syncGroupMessagesEnabled(); + } +} + +void VoiceActingManager::on_exportCmdBriefingRadio_toggled(bool checked) +{ + if (checked) { + _model->setExportSelection(ExportSelection::CommandBriefings); + syncGroupMessagesEnabled(); + } +} + +void VoiceActingManager::on_exportBriefingRadio_toggled(bool checked) +{ + if (checked) { + _model->setExportSelection(ExportSelection::Briefings); + syncGroupMessagesEnabled(); + } +} + +void VoiceActingManager::on_exportDebriefingRadio_toggled(bool checked) +{ + if (checked) { + _model->setExportSelection(ExportSelection::Debriefings); + syncGroupMessagesEnabled(); + } +} + +void VoiceActingManager::on_exportMessageRadio_toggled(bool checked) +{ + if (checked) { + _model->setExportSelection(ExportSelection::Messages); + syncGroupMessagesEnabled(); + } +} + +void VoiceActingManager::on_groupMessagesCheckBox_toggled(bool checked) +{ + _model->setGroupMessages(checked); +} + +void VoiceActingManager::on_personaSyncComboBox_currentIndexChanged(int index) +{ + _model->setWhichPersonaToSync(index); +} + +void VoiceActingManager::on_generateFilenamesButton_clicked() +{ + const int count = _model->generateFilenames(); + QMessageBox::information(this, tr("Generate Filenames"), tr("%1 filename(s) updated.").arg(count)); + refreshExampleFilename(); +} +void VoiceActingManager::on_generateScriptButton_clicked() +{ + const QString path = QFileDialog::getSaveFileName(this, + tr("Export Voice Script"), + QString(), + tr("Text files (*.txt);;All files (*)")); + if (path.isEmpty()) + return; + + const bool ok = _model->generateScript(path.toUtf8().constData()); + if (ok) { + QMessageBox::information(this, tr("Export"), tr("Script exported:\n%1").arg(path)); + } else { + QMessageBox::warning(this, tr("Export Failed"), tr("Could not open:\n%1").arg(path)); + } +} +void VoiceActingManager::on_copyMsgToShipsButton_clicked() +{ + const int n = _model->copyMessagePersonasToShips(); + QMessageBox::information(this, tr("Copy"), tr("Personas copied to %1 ship(s).").arg(n)); +} +void VoiceActingManager::on_copyShipsToMsgsButton_clicked() +{ + const int n = _model->copyShipPersonasToMessages(); + QMessageBox::information(this, tr("Copy"), tr("Personas copied to %1 message(s).").arg(n)); +} +void VoiceActingManager::on_clearNonSendersButton_clicked() +{ + const int n = _model->clearPersonasFromNonSenders(); + QMessageBox::information(this, tr("Clear"), tr("Cleared %1 ship(s).").arg(n)); +} +void VoiceActingManager::on_setHeadAnisButton_clicked() +{ + const int n = _model->setHeadAnisUsingMessagesTbl(); + QMessageBox::information(this, tr("Set Head ANIs"), tr("Updated %1 message(s).").arg(n)); +} +void VoiceActingManager::on_checkAnyWingmanButton_clicked() +{ + const auto res = _model->checkAnyWingmanPersonas(); + if (!res.anyWingmanFound) { + QMessageBox::information(this, tr("Check "), tr("No \"\" messages found.")); + return; + } + if (res.issueCount == 0) { + QMessageBox::information(this, tr("Check "), tr("All \"\" messages look good.")); + } else { + QMessageBox::warning(this, + tr("Check "), + tr("Issues found (%1):\n%2").arg(res.issueCount).arg(QString::fromStdString(res.report))); + } +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/VoiceActingManager.h b/qtfred/src/ui/dialogs/VoiceActingManager.h index 864dd3e1ff0..f237732686c 100644 --- a/qtfred/src/ui/dialogs/VoiceActingManager.h +++ b/qtfred/src/ui/dialogs/VoiceActingManager.h @@ -1,32 +1,79 @@ #pragma once +#include #include #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class VoiceActingManager; } -class VoiceActingManager : public QDialog -{ +class VoiceActingManager : public QDialog { Q_OBJECT - public: explicit VoiceActingManager(FredView* parent, EditorViewport* viewport); - // TODO shouldn't all QDialog subclasses have a virtual destructor? ~VoiceActingManager() override; -private: - std::unique_ptr ui; - //std::unique_ptr _model; - EditorViewport* _viewport; +protected: + void closeEvent(QCloseEvent* event) override; + bool eventFilter(QObject* obj, QEvent* ev) override; + +private slots: + // Abbrev line edits + void on_abbrevBriefingLineEdit_textEdited(const QString& text); + void on_abbrevCampaignLineEdit_textEdited(const QString& text); + void on_abbrevCommandBriefingLineEdit_textEdited(const QString& text); + void on_abbrevDebriefingLineEdit_textEdited(const QString& text); + void on_abbrevMessageLineEdit_textEdited(const QString& text); + void on_abbrevMissionLineEdit_textEdited(const QString& text); + + // Filename settings + void on_includeSenderCheckBox_toggled(bool checked); + void on_noReplaceCheckBox_toggled(bool checked); + void on_suffixComboBox_currentIndexChanged(int index); + + // Script export + void on_scriptEntryFormatPlainTextEdit_textChanged(); + void on_exportAllRadio_toggled(bool checked); + void on_exportCmdBriefingRadio_toggled(bool checked); + void on_exportBriefingRadio_toggled(bool checked); + void on_exportDebriefingRadio_toggled(bool checked); + void on_exportMessageRadio_toggled(bool checked); + void on_groupMessagesCheckBox_toggled(bool checked); + + // Persona sync + void on_personaSyncComboBox_currentIndexChanged(int index); + + // Actions + void on_generateFilenamesButton_clicked(); + void on_generateScriptButton_clicked(); + void on_copyMsgToShipsButton_clicked(); + void on_copyShipsToMsgsButton_clicked(); + void on_clearNonSendersButton_clicked(); + void on_setHeadAnisButton_clicked(); + void on_checkAnyWingmanButton_clicked(); + +private: // NOLINT(readability-redundant-access-specifiers) + EditorViewport* _viewport; + std::unique_ptr ui; + std::unique_ptr _model; + + void initializeUi(); + void updateUi(); + + void refreshExampleFilename(); + void populatePersonaCombo(); + void populateSuffixCombo(); + void syncGroupMessagesEnabled(); + + // enum mappers + static int exportSelectionToIndex(ExportSelection sel); + + static int suffixToIndex(Suffix s); + static Suffix indexToSuffix(int idx); }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/VoiceActingManager.ui b/qtfred/ui/VoiceActingManager.ui index 86e1a82380f..b03020ea41f 100644 --- a/qtfred/ui/VoiceActingManager.ui +++ b/qtfred/ui/VoiceActingManager.ui @@ -2,20 +2,29 @@ fso::fred::dialogs::VoiceActingManager + + Qt::WindowModal + 0 0 - 737 - 402 + 836 + 457 Voice Acting Manager - + + + + 3 + 0 + + File Name Options @@ -34,7 +43,7 @@ - + @@ -44,7 +53,7 @@ - + @@ -54,7 +63,7 @@ - + @@ -64,7 +73,7 @@ - + @@ -74,7 +83,7 @@ - + @@ -84,11 +93,24 @@ - + + + + + Qt::Vertical + + + + 20 + 40 + + + + @@ -103,7 +125,7 @@ - + @@ -118,7 +140,11 @@ - + + + true + + @@ -130,14 +156,14 @@ - + Replace existing file names - + Generate File Names @@ -148,6 +174,12 @@ + + + 1 + 0 + + Script Options @@ -157,135 +189,221 @@ Script Entry Format - - - - - Sender: $sender -Persona: $persona -File: $filename -Message: $message - - - + - - - $filename - name of the message file + + + + + + 0 + 0 + + + + + + + + + + + $name - name of the message +$filename - name of the message file $message - text of the message $persona - persona of the sender $sender - name of the sender +$note - message notes Note that $persona and $sender will only appear for the Message section. - - + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + - - - Export - - - - - - Everything - - - exportOptionsButtonGroup - - - - - - - Command briefings only - - - exportOptionsButtonGroup - - - - - - - Briefings only - - - exportOptionsButtonGroup - - - - - - - Debriefings only - - - exportOptionsButtonGroup - - - - - - - Messages only - - - exportOptionsButtonGroup - - - - - + + + + + + + Sync Personas + + + + + + + + + Copy message personas to ships + + + + + + + Copy ship personas to messages + + + + + + + Clear personas from ships that +don't send messages + + + + + + + Set head ANIs using personas +in messages.tbl + + + + + + + + + + Check if messages sent by "<any +wingman>" have at least one ship +with that persona + + + + + + + + + Export + + - - - Qt::Horizontal + + + Everything - - - 17 - 20 - + + exportOptionsButtonGroup + + + + + + + Command briefings only - + + exportOptionsButtonGroup + + - + - Group send-message-list messages before others + Briefings only + + exportOptionsButtonGroup + - - - Qt::Horizontal + + + Debriefings only - + + exportOptionsButtonGroup + + + + + + + Messages only + + + exportOptionsButtonGroup + + + + + + + + + Qt::Horizontal + + + + 17 + 20 + + + + + + + + Group send-message-list messages before others + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 0 + 0 + + + - 40 - 20 + 0 + 0 - + + Generate Script + + - - - - - - - - Generate Script - - + + + From 76440ddfd10c4b390e0744347856045f3cd2a45b Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 06:43:41 -0500 Subject: [PATCH 414/466] QtFRED Reinforcements Dialog (#6990) * cleanup reinforcements dialog * unused --- .../ReinforcementsEditorDialogModel.cpp | 69 +- .../dialogs/ReinforcementsEditorDialogModel.h | 66 +- .../ui/dialogs/ReinforcementsEditorDialog.cpp | 360 ++++--- .../ui/dialogs/ReinforcementsEditorDialog.h | 45 +- qtfred/ui/ReinforcementsDialog.ui | 985 ++++++++++-------- 5 files changed, 820 insertions(+), 705 deletions(-) diff --git a/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.cpp index 32b01e37b50..c51626b1bf9 100644 --- a/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.cpp @@ -2,9 +2,7 @@ #include "ship/ship.h" #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { ReinforcementsDialogModel::ReinforcementsDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) @@ -38,6 +36,9 @@ void ReinforcementsDialogModel::initializeData() continue; } + // wings can have a use count. + _useCountEnabled.emplace_back(currentWing.name); + bool found = false; for (auto& reinforcement : _reinforcementList) { @@ -48,7 +49,7 @@ void ReinforcementsDialogModel::initializeData() } if (!found) { - _shipWingPool.push_back(currentWing.name); + _shipWingPool.emplace_back(currentWing.name); } } @@ -75,9 +76,6 @@ void ReinforcementsDialogModel::initializeData() _selectedReinforcements.clear(); _selectedReinforcementIndices.clear(); - _numberLineEditUpdateRequired = true; - _listUpdateRequired = true; - modelChanged(); } bool ReinforcementsDialogModel::apply() @@ -99,8 +97,6 @@ bool ReinforcementsDialogModel::apply() } _shipWingPool.clear(); - _numberLineEditUpdateRequired = false; - _listUpdateRequired = false; _selectedReinforcementIndices.clear(); return true; @@ -111,18 +107,6 @@ void ReinforcementsDialogModel::reject() _shipWingPool.clear(); _reinforcementList.clear(); _selectedReinforcementIndices.clear(); - _numberLineEditUpdateRequired = false; - _listUpdateRequired = false; -} - -bool ReinforcementsDialogModel::numberLineEditUpdateRequired() -{ - return _numberLineEditUpdateRequired; -} - -bool ReinforcementsDialogModel::listUpdateRequired() -{ - return _listUpdateRequired; } void ReinforcementsDialogModel::addToReinforcements(const SCP_vector& namesIn) @@ -144,9 +128,6 @@ void ReinforcementsDialogModel::addToReinforcements(const SCP_vector _reinforcementList.pop_back(); } - _listUpdateRequired = true; - _numberLineEditUpdateRequired = true; - modelChanged(); set_modified(); } @@ -167,10 +148,6 @@ void ReinforcementsDialogModel::removeFromReinforcements(const SCP_vector ReinforcementsDialogModel::getShipPoolList() // remember to call this and getShipPoolList together SCP_vector ReinforcementsDialogModel::getReinforcementList() { - _listUpdateRequired = false; - SCP_vector list; for (auto& currentReinforcement : _reinforcementList) @@ -217,6 +192,11 @@ int ReinforcementsDialogModel::getUseCount() return current; } +bool ReinforcementsDialogModel::getUseCountEnabled(const SCP_string& name) +{ + return std::find(_useCountEnabled.begin(), _useCountEnabled.end(), name) != _useCountEnabled.end(); +} + int ReinforcementsDialogModel::getBeforeArrivalDelay() { if (_selectedReinforcementIndices.empty()) { @@ -250,17 +230,27 @@ void ReinforcementsDialogModel::selectReinforcement(const SCP_vector Assertion(namesIn.size() == _selectedReinforcementIndices.size(), "%d vs %d", static_cast(namesIn.size()), static_cast(_selectedReinforcementIndices.size())); - _numberLineEditUpdateRequired = true; - modelChanged(); set_modified(); } void ReinforcementsDialogModel::setUseCount(int count) { - for (auto& reinforcement : _selectedReinforcementIndices) { - std::get<1>(_reinforcementList[reinforcement]) = count; + if (_selectedReinforcementIndices.empty()) + return; + + for (int idx : _selectedReinforcementIndices) { + if (idx < 0 || idx >= static_cast(_reinforcementList.size())) + continue; + + auto& tup = _reinforcementList[idx]; + const SCP_string& name = std::get<0>(tup); + + if (!getUseCountEnabled(name)) + continue; + + std::get<1>(tup) = count; } - modelChanged(); + set_modified(); } @@ -269,7 +259,6 @@ void ReinforcementsDialogModel::setBeforeArrivalDelay(int delay) for (auto& reinforcement : _selectedReinforcementIndices) { std::get<2>(_reinforcementList[reinforcement]) = delay; } - modelChanged(); set_modified(); } @@ -289,8 +278,6 @@ void ReinforcementsDialogModel::moveReinforcementsUp() if (updatedRequired) { updateSelectedIndices(); - _listUpdateRequired = true; - modelChanged(); set_modified(); } } @@ -309,8 +296,6 @@ void ReinforcementsDialogModel::moveReinforcementsDown() if (updatedRequired) { updateSelectedIndices(); - _listUpdateRequired = true; - modelChanged(); set_modified(); } } @@ -334,6 +319,4 @@ void ReinforcementsDialogModel::updateSelectedIndices() std::sort(_selectedReinforcementIndices.begin(), _selectedReinforcementIndices.end()); } -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.h b/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.h index 3cc3655981c..8d1d4842941 100644 --- a/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/ReinforcementsEditorDialogModel.h @@ -3,51 +3,43 @@ #include "AbstractDialogModel.h" #include "globalincs/pstypes.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { - class ReinforcementsDialogModel : public AbstractDialogModel { - public: +class ReinforcementsDialogModel : public AbstractDialogModel { + public: + ReinforcementsDialogModel(QObject* parent, EditorViewport* viewport); - ReinforcementsDialogModel(QObject* parent, EditorViewport* viewport); + bool apply() override; + void reject() override; - bool apply() override; - void reject() override; + void initializeData(); - void initializeData(); + SCP_vector getShipPoolList(); + SCP_vector getReinforcementList(); + int getUseCount(); + bool getUseCountEnabled(const SCP_string& name); + int getBeforeArrivalDelay(); - bool numberLineEditUpdateRequired(); - bool listUpdateRequired(); + void setUseCount(int count); + void setBeforeArrivalDelay(int delay); - SCP_vector getShipPoolList(); - SCP_vector getReinforcementList(); - int getUseCount(); - int getBeforeArrivalDelay(); + void addToReinforcements(const SCP_vector& namesIn); + void removeFromReinforcements(const SCP_vector& namesIn); - void setUseCount(int count); - void setBeforeArrivalDelay(int delay); + void selectReinforcement(const SCP_vector& namesIn); - void addToReinforcements(const SCP_vector& namesIn); - void removeFromReinforcements(const SCP_vector& namesIn); + void moveReinforcementsUp(); + void moveReinforcementsDown(); - void selectReinforcement(const SCP_vector& namesIn); - - void moveReinforcementsUp(); - void moveReinforcementsDown(); - - void updateSelectedIndices(); + void updateSelectedIndices(); - private: - bool _numberLineEditUpdateRequired; - bool _listUpdateRequired; + private: + SCP_vector _shipWingPool; // the list of ships and wings that are not yet reinforcements + SCP_vector _useCountEnabled; // the list of reinforcements that have a use count enabled + SCP_vector> _reinforcementList; // use to store the name of the ship, the use count, and the delay before arriving + SCP_vector _selectedReinforcements; + SCP_vector _selectedReinforcementIndices; // keeps track of what ships are currently selected in order to + // adjust them more easily, in reverse order. +}; - SCP_vector _shipWingPool; // the list of ships and wings that are not yet reinforcements - SCP_vector> _reinforcementList; // use to store the name of the ship, the use count, and the delay before arriving - SCP_vector _selectedReinforcements; - SCP_vector _selectedReinforcementIndices; // keeps track of what ships are currently selected in order to adjust them more easily, in reverse order. - }; - -} -} -} \ No newline at end of file +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp b/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp index a2cb01c96ad..a9e4c3936a7 100644 --- a/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp @@ -4,212 +4,262 @@ #include #include #include -#include #include -namespace fso { -namespace fred { -namespace dialogs { - +namespace fso::fred::dialogs { - ReinforcementsDialog::ReinforcementsDialog(FredView* parent, EditorViewport* viewport) - : QDialog(parent), ui(new Ui::ReinforcementsDialog()), _model(new ReinforcementsDialogModel(this, viewport)), - _viewport(viewport) - { - this->setFocus(); - ui->setupUi(this); +ReinforcementsDialog::ReinforcementsDialog(FredView* parent, EditorViewport* viewport) + : QDialog(parent), ui(new Ui::ReinforcementsDialog()), _model(new ReinforcementsDialogModel(this, viewport)), + _viewport(viewport) +{ + this->setFocus(); + ui->setupUi(this); - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &ReinforcementsDialog::updateUI); - connect(this, &QDialog::accepted, _model.get(), &ReinforcementsDialogModel::apply); - connect(ui->okAndCancelButtonBox, - &QDialogButtonBox::rejected, - this, &ReinforcementsDialog::rejectHandler); + updateUi(); +} +ReinforcementsDialog::~ReinforcementsDialog() = default; - connect(ui->delayLineEdit, - static_cast(&QLineEdit::textChanged), - this, - &ReinforcementsDialog::onDelayChanged); +void ReinforcementsDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} - connect(ui->useLineEdit, - static_cast(&QLineEdit::textChanged), - this, - &ReinforcementsDialog::onUseCountChanged); +void ReinforcementsDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close +} - // force to a number value. Spinbox would be better here, except there are times where we'll want it to be empty, not zero or some other value. - // cannot be done in the designer so do it while setting up other things involving the LineEdit. - ui->delayLineEdit->setValidator(new QIntValidator(0, INT_MAX, this)); - ui->useLineEdit->setValidator(new QIntValidator(0, INT_MAX, this)); +void ReinforcementsDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window +} - connect(ui->chosenShipsList, - static_cast(&QListWidget::itemClicked), - this, - &ReinforcementsDialog::onReinforcementItemChanged); - connect(ui->chosenShipsList, - static_cast(&QListWidget::dropEvent), - this, - &ReinforcementsDialog::onReinforcementItemChanged); +static inline void setSpinMixed(QSpinBox* sb) +{ + sb->setSpecialValueText(QStringLiteral("-")); // special text for mixed values + sb->setMinimum(std::numeric_limits::min()); // sentinel below real min + sb->setValue(sb->minimum()); // triggers special text display +} - connect(ui->actionAddShip, - &QPushButton::clicked, - this, - &ReinforcementsDialog::on_actionAddShip_clicked); +static inline void setSpinNormal(QSpinBox* sb, int min, int max, int value) +{ + sb->setSpecialValueText(QString()); // disable special text + sb->setRange(min, max); + sb->setValue(value); +} - connect(ui->actionRemoveShip, - &QPushButton::clicked, - this, - &ReinforcementsDialog::on_actionRemoveShip_clicked); +void ReinforcementsDialog::updateUi() +{ + util::SignalBlockers blockers(this); + + enableDisableControls(); - updateUI(); + // Save current selections + QSet chosen; + for (auto* it : ui->chosenShipsList->selectedItems()) { + chosen.insert(it->text()); } - void ReinforcementsDialog::updateUI(){ - - if (_model->listUpdateRequired()) { - ui->chosenShipsList->clear(); - ui->possibleShipsList->clear(); - - auto newShipPoolList = _model->getShipPoolList(); - auto newReinforcementList = _model->getReinforcementList(); - - for (auto& candidate : newShipPoolList) { - ui->possibleShipsList->addItem(QString(candidate.c_str())); - } - - for (auto& reinforcement : newReinforcementList) { - ui->chosenShipsList->addItem(QString(reinforcement.c_str())); - } - } - - if (_model->numberLineEditUpdateRequired()) { - int delay = _model->getBeforeArrivalDelay(); - - if (delay == -1) { - ui->delayLineEdit->clear(); - ui->delayLineEdit->setDisabled(false); - } - else if (delay == -2) { - ui->delayLineEdit->clear(); - ui->delayLineEdit->setDisabled(true); - } else { - ui->delayLineEdit->setDisabled(false); - ui->delayLineEdit->setText(QString::number(delay)); - } - - int use = _model->getUseCount(); - - if (use == -1) { - ui->useLineEdit->setDisabled(false); - ui->useLineEdit->clear(); - } - else if (use == -2) { - ui->useLineEdit->clear(); - ui->useLineEdit->setDisabled(true); - } else { - ui->useLineEdit->setDisabled(false); - ui->useLineEdit->setText(QString::number(use)); - } - } + QSet possible; + for (auto* it : ui->chosenShipsList->selectedItems()) { + possible.insert(it->text()); } + ui->chosenShipsList->clear(); + ui->possibleShipsList->clear(); - void ReinforcementsDialog::onReinforcementItemChanged() - { - SCP_vector listOut; - for (auto& currentItem : ui->chosenShipsList->selectedItems()){ - listOut.emplace_back(currentItem->text().toStdString()); + auto newShipPoolList = _model->getShipPoolList(); + auto newReinforcementList = _model->getReinforcementList(); + + for (auto& candidate : newShipPoolList) { + ui->possibleShipsList->addItem(QString(candidate.c_str())); + } + + // Restore previous selections + for (const auto& name : possible) { + const auto matches = ui->possibleShipsList->findItems(name, Qt::MatchExactly); + for (auto* it : matches) { + it->setSelected(true); } - - if (ui->chosenShipsList->selectedItems().count() > 0) { - ui->delayLineEdit->setDisabled(false); - ui->useLineEdit->setDisabled(false); + } + + for (auto& reinforcement : newReinforcementList) { + ui->chosenShipsList->addItem(QString(reinforcement.c_str())); + } + + // Restore previous selections + for (const auto& name : chosen) { + const auto matches = ui->chosenShipsList->findItems(name, Qt::MatchExactly); + for (auto* it : matches) { + it->setSelected(true); } + } - const SCP_vector listOutFinal = listOut; + int use = _model->getUseCount(); - _model->selectReinforcement(listOutFinal); + if (use < 0) { + setSpinMixed(ui->useSpinBox); + } else { + setSpinNormal(ui->useSpinBox, 0, 16777215, use); } + + int delay = _model->getBeforeArrivalDelay(); - void ReinforcementsDialog::onDelayChanged() - { - // need to check that it's not empty so as not to pass nonsense values back to the model. - if (ui->chosenShipsList->selectedItems().count() >= 0 && !ui->delayLineEdit->text().isEmpty()) { - _model->setBeforeArrivalDelay(ui->delayLineEdit->text().toInt()); - } + if (delay < 0) { + setSpinMixed(ui->delaySpinBox); + } else { + setSpinNormal(ui->delaySpinBox, 0, 16777215, delay); } +} + +void ReinforcementsDialog::enableDisableControls() +{ + int count = ui->chosenShipsList->selectedItems().count(); - void ReinforcementsDialog::onUseCountChanged() - { - // need to check that it's not empty so as not to pass nonsense values back to the model. - if (ui->chosenShipsList->selectedItems().count() > 0 && !ui->useLineEdit->text().isEmpty()) { - _model->setUseCount(ui->useLineEdit->text().toInt()); + const auto selected = ui->chosenShipsList->selectedItems(); + + const bool anySupportsUse = std::any_of(selected.cbegin(), selected.cend(), [&](const QListWidgetItem* it) { + return _model->getUseCountEnabled(it->text().toUtf8().constData()); + }); + + ui->useSpinBox->setEnabled(anySupportsUse && count > 0 && _model->getUseCount() != -2); + ui->delaySpinBox->setEnabled(count > 0 && _model->getUseCount() != -2); +} + +void ReinforcementsDialog::on_actionRemoveShip_clicked() +{ + SCP_vector selectedItems; + + for (int i = 0; i < ui->chosenShipsList->count(); i++) { + auto current = ui->chosenShipsList->item(i); + if (current->isSelected()) { + selectedItems.emplace_back(current->text().toUtf8().constData()); } } + const SCP_vector selectedItemsOut = selectedItems; - void ReinforcementsDialog::on_actionRemoveShip_clicked() - { - SCP_vector selectedItems; + _model->removeFromReinforcements(selectedItemsOut); - for (int i = 0; i < ui->chosenShipsList->count(); i++) { - auto current = ui->chosenShipsList->item(i); - if (current->isSelected()) { - selectedItems.emplace_back(current->text().toStdString()); - } - } + updateUi(); +} - const SCP_vector selectedItemsOut = selectedItems; +void ReinforcementsDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void ReinforcementsDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} - _model->removeFromReinforcements(selectedItemsOut); +void ReinforcementsDialog::on_actionAddShip_clicked() +{ + SCP_vector selectedItems; + for (int i = 0; i < ui->possibleShipsList->count(); i++) { + auto current = ui->possibleShipsList->item(i); + if (current->isSelected()) { + selectedItems.emplace_back(current->text().toUtf8().constData()); + } } - void ReinforcementsDialog::on_actionAddShip_clicked() - { - SCP_vector selectedItems; + const SCP_vector selectedItemsOut = selectedItems; - for (int i = 0; i < ui->possibleShipsList->count(); i++) { - auto current = ui->possibleShipsList->item(i); - if (current->isSelected()) { - selectedItems.emplace_back(current->text().toStdString()); - } - } + _model->addToReinforcements(selectedItemsOut); - const SCP_vector selectedItemsOut = selectedItems; + updateUi(); +} - _model->addToReinforcements(selectedItemsOut); +void ReinforcementsDialog::on_moveSelectionUp_clicked() +{ + _model->moveReinforcementsUp(); + updateUi(); +} +void ReinforcementsDialog::on_moveSelectionDown_clicked() +{ + _model->moveReinforcementsDown(); + updateUi(); +} + +void ReinforcementsDialog::on_useSpinBox_valueChanged(int val) +{ + // need to check that it's not empty so as not to pass nonsense values back to the model. + if (ui->chosenShipsList->selectedItems().count() > 0) { + if (val < 0) { + val = 0; + + util::SignalBlockers blockers(this); + ui->useSpinBox->setValue(val); + } + _model->setUseCount(val); } +} + +void ReinforcementsDialog::on_delaySpinBox_valueChanged(int val) +{ + // need to check that it's not empty so as not to pass nonsense values back to the model. + if (ui->chosenShipsList->selectedItems().count() > 0) { + if (val < 0) { + val = 0; - void ReinforcementsDialog::on_moveSelectionUp_clicked() - { - _model->moveReinforcementsUp(); + util::SignalBlockers blockers(this); + ui->delaySpinBox->setValue(val); + } + _model->setBeforeArrivalDelay(val); } +} - void ReinforcementsDialog::on_moveSelectionDown_clicked() - { - _model->moveReinforcementsDown(); +void ReinforcementsDialog::on_chosenShipsList_itemClicked(QListWidgetItem* /*item*/) +{ + SCP_vector listOut; + for (auto& currentItem : ui->chosenShipsList->selectedItems()) { + listOut.emplace_back(currentItem->text().toUtf8().constData()); } - void ReinforcementsDialog::enableDisableControls(){} + const SCP_vector listOutFinal = listOut; - void ReinforcementsDialog::on_chosenShipsList_clicked(){} + _model->selectReinforcement(listOutFinal); - ReinforcementsDialog::~ReinforcementsDialog() {} // NOLINT + updateUi(); +} - void ReinforcementsDialog::closeEvent(QCloseEvent* e){ - if (!rejectOrCloseHandler(this, _model.get(), _viewport)) { - e->ignore(); - }; +void ReinforcementsDialog::on_chosenMultiselectCheckbox_toggled(bool checked) +{ + if (checked) { + ui->chosenShipsList->setSelectionMode(QAbstractItemView::MultiSelection); + } else { + ui->chosenShipsList->setSelectionMode(QAbstractItemView::SingleSelection); + ui->chosenShipsList->clearSelection(); + updateUi(); } +} - void ReinforcementsDialog::rejectHandler() - { - this->close(); +void ReinforcementsDialog::on_poolMultiselectCheckbox_toggled(bool checked) +{ + if (checked) { + ui->possibleShipsList->setSelectionMode(QAbstractItemView::MultiSelection); + } else { + ui->possibleShipsList->setSelectionMode(QAbstractItemView::SingleSelection); + ui->possibleShipsList->clearSelection(); + updateUi(); } - -} } -} \ No newline at end of file + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.h b/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.h index 18a245d4a75..8960c9f12ce 100644 --- a/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.h +++ b/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.h @@ -1,21 +1,17 @@ -#ifndef REINFORCEMENTEDITORDIALOG_H -#define REINFORCEMENTEDITORDIALOG_H +#pragma once #include #include #include #include -// not sure if I need these yet. -//#include +#include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { - namespace Ui { - class ReinforcementsDialog; - } +namespace Ui { + class ReinforcementsDialog; +} class ReinforcementsDialog : public QDialog { @@ -26,32 +22,35 @@ class ReinforcementsDialog : public QDialog { explicit ReinforcementsDialog(FredView* parent, EditorViewport* viewport); ~ReinforcementsDialog() override; // NOLINT + void accept() override; + void reject() override; + protected: - void closeEvent(QCloseEvent*) override; - void rejectHandler(); + void closeEvent(QCloseEvent* e) override; // funnel all Window X presses through reject() private slots: - void on_chosenShipsList_clicked(); + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + void on_actionAddShip_clicked(); void on_actionRemoveShip_clicked(); void on_moveSelectionUp_clicked(); void on_moveSelectionDown_clicked(); + void on_useSpinBox_valueChanged(int val); + void on_delaySpinBox_valueChanged(int val); + void on_chosenShipsList_itemClicked(QListWidgetItem* /*item*/); + + void on_chosenMultiselectCheckbox_toggled(bool checked); + void on_poolMultiselectCheckbox_toggled(bool checked); -private: +private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; - void updateUI(); + void updateUi(); void enableDisableControls(); - void onReinforcementItemChanged(); - void onUseCountChanged(); - void onDelayChanged(); - }; -} -} -} -#endif +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/ReinforcementsDialog.ui b/qtfred/ui/ReinforcementsDialog.ui index f01a24205e9..7ec382db7e2 100644 --- a/qtfred/ui/ReinforcementsDialog.ui +++ b/qtfred/ui/ReinforcementsDialog.ui @@ -6,457 +6,548 @@ 0 0 - 554 - 245 + 564 + 248 - Dialog + Reinforcements - - - - 5 - 5 - 545 - 237 - - - - - - - - 175 - 0 - - - - - 175 - 16777215 - - - - QAbstractItemView::NoEditTriggers - - - false - - - false - - - false - - - false - - - QAbstractItemView::NoDragDrop - - - Qt::MoveAction - - - QAbstractItemView::MultiSelection - - - true - - - - - - - - 70 - 16777215 - - - - Pool: - - - - - - - true - - - - 175 - 0 - - - - - 175 - 16777215 - - - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - 0 - 0 - 0 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - - QAbstractItemView::NoEditTriggers - - - false - - - false - - - false - - - QAbstractItemView::NoDragDrop - - - Qt::MoveAction - - - QAbstractItemView::MultiSelection - - - true - - - - - - - - - Uses: - - - - - - - false - - - - - - - Delay After Arrival: - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Fixed - - - - 10 - 40 - - - - - - - - - 11234123 - 16777215 - - - - Qt::Vertical - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - - 80 - 16777215 - - - - Reinforcements: - - - - - - - - - Add >> - - - - - - - - - - 30 - 16777215 - - - - ↓ - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 40 - 20 - - - - - - - - - 30 - 16777215 - - - - ↑ - - - false - - - true - - - - - - - - - << Remove - - - - - - - + + + + + + + 6 + + + + + + 80 + 16777215 + + + + Pool: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Multiselect + + + true + + + + + + + + + true + + + + 175 + 200 + + + + + 16777215 + 16777215 + + + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + + + 0 + 0 + 0 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + 0 + 0 + 0 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 0 + 0 + 0 + + + + + + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + false + + + QAbstractItemView::NoDragDrop + + + Qt::MoveAction + + + QAbstractItemView::MultiSelection + + + true + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Add >> + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + 30 + 16777215 + + + + ↓ + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + + 30 + 16777215 + + + + ↑ + + + false + + + true + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + << Remove + + + + + + + + + + + + + + 80 + 16777215 + + + + Reinforcements: + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Multiselect + + + true + + + + + + + + + + 175 + 200 + + + + + 16777215 + 16777215 + + + + QAbstractItemView::NoEditTriggers + + + false + + + false + + + false + + + false + + + QAbstractItemView::NoDragDrop + + + Qt::MoveAction + + + QAbstractItemView::MultiSelection + + + true + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Uses: + + + + + + + 16777215 + + + + + + + Delay After Arrival: + + + + + + + 16777215 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 11234123 + 16777215 + + + + Qt::Vertical + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + - - - okAndCancelButtonBox - accepted() - fso::fred::dialogs::ReinforcementsDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - + From 16926a43959fb929cc60ae53be0467b63deb807f Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 06:43:53 -0500 Subject: [PATCH 415/466] QtFRED Shield Sys Dialog (#6991) * refactor global shield sys dialog * I win this time, Clang * Or not... --- qtfred/src/mission/Editor.cpp | 49 +++-- qtfred/src/mission/Editor.h | 16 +- .../dialogs/ShieldSystemDialogModel.cpp | 63 +++++- .../mission/dialogs/ShieldSystemDialogModel.h | 40 ++-- qtfred/src/ui/dialogs/ShieldSystemDialog.cpp | 137 ++++++------ qtfred/src/ui/dialogs/ShieldSystemDialog.h | 34 +-- qtfred/ui/ShieldSystemDialog.ui | 203 +++++++++--------- 7 files changed, 295 insertions(+), 247 deletions(-) diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index f08fd76a1e3..4832d2c9a81 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -101,7 +101,7 @@ extern void allocate_parse_text(size_t size); namespace fso { namespace fred { -Editor::Editor() : currentObject{ -1 }, Shield_sys_teams(Iff_info.size(), 0), Shield_sys_types(MAX_SHIP_CLASSES, 0) { +Editor::Editor() : currentObject{ -1 }, Shield_sys_teams(Iff_info.size(), GlobalShieldStatus::HasShields), Shield_sys_types(MAX_SHIP_CLASSES, GlobalShieldStatus::HasShields) { connect(fredApp, &FredApplication::onIdle, this, &Editor::update); // When the mission changes we need to update all renderers @@ -492,10 +492,10 @@ void Editor::clearMission(bool fast_reload) { nebula_init(Nebula_index, Nebula_pitch, Nebula_bank, Nebula_heading); Shield_sys_teams.clear(); - Shield_sys_teams.resize(Iff_info.size(), 0); + Shield_sys_teams.resize(Iff_info.size(), GlobalShieldStatus::HasShields); for (int i = 0; i < MAX_SHIP_CLASSES; i++) { - Shield_sys_types[i] = 0; + Shield_sys_types[i] = GlobalShieldStatus::HasShields; } setupCurrentObjectIndices(-1); @@ -696,7 +696,7 @@ int Editor::create_player(vec3d* pos, matrix* orient, int type) { } int Editor::create_ship(matrix* orient, vec3d* pos, int ship_type) { - int obj, z1, z2; + int obj; float temp_max_hull_strength; ship_info* sip; @@ -721,9 +721,9 @@ int Editor::create_ship(matrix* orient, vec3d* pos, int ship_type) { // default shield setting shipp->special_shield = -1; - z1 = Shield_sys_teams[shipp->team]; - z2 = Shield_sys_types[ship_type]; - if (((z1 == 1) && z2) || (z2 == 1)) { + auto z1 = Shield_sys_teams[shipp->team]; + auto z2 = Shield_sys_types[ship_type]; + if (((z1 == GlobalShieldStatus::NoShields) && z2 != GlobalShieldStatus::HasShields) || (z2 == GlobalShieldStatus::NoShields)) { Objects[obj].flags.set(Object::Object_Flags::No_shields); } @@ -3243,59 +3243,58 @@ int Editor::global_error_check_mixed_player_wing(int w) { return 0; } -bool Editor::compareShieldSysData(const std::vector& teams, const std::vector& types) const { - Assert(Shield_sys_teams.size() == teams.size()); - Assert(Shield_sys_types.size() == types.size()); +bool Editor::compareShieldSysData(const SCP_vector& teams, const SCP_vector& types) const { + Assertion(Shield_sys_teams.size() == teams.size(), "Mismatched shield data from global shield dialog!"); + Assertion(Shield_sys_types.size() == types.size(), "Mismatched shield data from global shield dialog!"); return (Shield_sys_teams == teams) && (Shield_sys_types == types); } -void Editor::exportShieldSysData(std::vector& teams, std::vector& types) const { +void Editor::exportShieldSysData(SCP_vector& teams, SCP_vector& types) const { teams = Shield_sys_teams; types = Shield_sys_types; } -void Editor::importShieldSysData(const std::vector& teams, const std::vector& types) { - Assert(Shield_sys_teams.size() == teams.size()); - Assert(Shield_sys_types.size() == types.size()); +void Editor::importShieldSysData(const SCP_vector& teams, const SCP_vector& types) { + Assertion(Shield_sys_teams.size() == teams.size(), "Mismatched shield data from global shield dialog!"); + Assertion(Shield_sys_types.size() == types.size(), "Mismatched shield data from global shield dialog!"); Shield_sys_teams = teams; Shield_sys_types = types; for (int i = 0; i < MAX_SHIPS; i++) { if (Ships[i].objnum >= 0) { - int z = Shield_sys_teams[Ships[i].team]; - if (!Shield_sys_types[Ships[i].ship_info_index]) - z = 0; - else if (Shield_sys_types[Ships[i].ship_info_index] == 1) - z = 1; + auto z = Shield_sys_teams[Ships[i].team]; + if (Shield_sys_types[Ships[i].ship_info_index] == GlobalShieldStatus::HasShields) + z = GlobalShieldStatus::HasShields; + else if (Shield_sys_types[Ships[i].ship_info_index] == GlobalShieldStatus::NoShields) + z = GlobalShieldStatus::NoShields; - if (!z) + if (z == GlobalShieldStatus::HasShields) Objects[Ships[i].objnum].flags.remove(Object::Object_Flags::No_shields); - else if (z == 1) + else if (z == GlobalShieldStatus::NoShields) Objects[Ships[i].objnum].flags.set(Object::Object_Flags::No_shields); } } } // adapted from shield_sys_dlg OnInitDialog() -// 0 = has shields, 1 = no shields, 2 = conflict/inconsistent void Editor::normalizeShieldSysData() { std::vector teams(Iff_info.size(), 0); std::vector types(MAX_SHIP_CLASSES, 0); for (int i = 0; i < MAX_SHIPS; i++) { if (Ships[i].objnum >= 0) { - int z = (Objects[Ships[i].objnum].flags[Object::Object_Flags::No_shields]) ? 1 : 0; + auto z = (Objects[Ships[i].objnum].flags[Object::Object_Flags::No_shields]) ? GlobalShieldStatus::NoShields : GlobalShieldStatus::HasShields; if (!teams[Ships[i].team]) Shield_sys_teams[Ships[i].team] = z; else if (Shield_sys_teams[Ships[i].team] != z) - Shield_sys_teams[Ships[i].team] = 2; + Shield_sys_teams[Ships[i].team] = GlobalShieldStatus::MixedShields; if (!types[Ships[i].ship_info_index]) Shield_sys_types[Ships[i].ship_info_index] = z; else if (Shield_sys_types[Ships[i].ship_info_index] != z) - Shield_sys_types[Ships[i].ship_info_index] = 2; + Shield_sys_types[Ships[i].ship_info_index] = GlobalShieldStatus::MixedShields; teams[Ships[i].team]++; types[Ships[i].ship_info_index]++; diff --git a/qtfred/src/mission/Editor.h b/qtfred/src/mission/Editor.h index 730c6c4ae7b..8b0cea4504c 100644 --- a/qtfred/src/mission/Editor.h +++ b/qtfred/src/mission/Editor.h @@ -38,6 +38,12 @@ struct WingNameCheck { std::string message; // human-readable for dialogs }; +enum class GlobalShieldStatus { + HasShields, + NoShields, + MixedShields +}; + /*! Game editor. * Handles everything needed to edit the game, * without any knowledge of the actual GUI framework stack. @@ -203,9 +209,9 @@ class Editor : public QObject { SCP_vector get_docking_list(int model_index); - bool compareShieldSysData(const std::vector& teams, const std::vector& types) const; - void exportShieldSysData(std::vector& teams, std::vector& types) const; - void importShieldSysData(const std::vector& teams, const std::vector& types); + bool compareShieldSysData(const SCP_vector& teams, const SCP_vector& types) const; + void exportShieldSysData(SCP_vector& teams, SCP_vector& types) const; + void importShieldSysData(const SCP_vector& teams, const SCP_vector& types); void normalizeShieldSysData(); static void strip_quotation_marks(SCP_string& str); @@ -231,8 +237,8 @@ class Editor : public QObject { int numMarked = 0; - std::vector Shield_sys_teams; - std::vector Shield_sys_types; + SCP_vector Shield_sys_teams; + SCP_vector Shield_sys_types; int delete_flag; diff --git a/qtfred/src/mission/dialogs/ShieldSystemDialogModel.cpp b/qtfred/src/mission/dialogs/ShieldSystemDialogModel.cpp index 9f8441d1b7e..07bcaebd142 100644 --- a/qtfred/src/mission/dialogs/ShieldSystemDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShieldSystemDialogModel.cpp @@ -3,12 +3,10 @@ #include #include "mission/dialogs/ShieldSystemDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { ShieldSystemDialogModel::ShieldSystemDialogModel(QObject* parent, EditorViewport* viewport) : - AbstractDialogModel(parent, viewport), _teams(Iff_info.size(), 0), _types(MAX_SHIP_CLASSES, 0) { + AbstractDialogModel(parent, viewport), _teams(Iff_info.size(), GlobalShieldStatus::HasShields), _types(MAX_SHIP_CLASSES, GlobalShieldStatus::HasShields) { initializeData(); } @@ -28,8 +26,6 @@ void ShieldSystemDialogModel::initializeData() { for (const auto& iff : Iff_info) { _teamOptions.emplace_back(iff.iff_name); } - - modelChanged(); } bool ShieldSystemDialogModel::apply() { @@ -46,6 +42,61 @@ bool ShieldSystemDialogModel::query_modified() const { return !_editor->compareShieldSysData(_teams, _types); } +int ShieldSystemDialogModel::getCurrentTeam() const +{ + return _currTeam; +} +int ShieldSystemDialogModel::getCurrentShipType() const +{ + return _currType; +} +void ShieldSystemDialogModel::setCurrentTeam(int team) +{ + Assertion(SCP_vector_inbounds(Iff_info, team), "Team index %d out of bounds (size: %d)", team, static_cast(Iff_info.size())); + modify(_currTeam, team); +} +void ShieldSystemDialogModel::setCurrentShipType(int type) +{ + Assertion(type >= 0 && type < MAX_SHIP_CLASSES, "Ship class index %d is invalid!", type); // NOLINT(readability-simplify-boolean-expr) + modify(_currType, type); +} + +GlobalShieldStatus ShieldSystemDialogModel::getCurrentTeamShieldSys() const +{ + return _teams[_currTeam]; } +GlobalShieldStatus ShieldSystemDialogModel::getCurrentTypeShieldSys() const +{ + return _types[_currType]; } +void ShieldSystemDialogModel::setCurrentTeamShieldSys(bool value) +{ + // UI can only turn shields on or off, so just map to the appropriate enum value + + if (value) { + modify(_teams[_currTeam], GlobalShieldStatus::HasShields); + } else { + modify(_teams[_currTeam], GlobalShieldStatus::NoShields); + } } +void ShieldSystemDialogModel::setCurrentTypeShieldSys(bool value) +{ + // UI can only turn shields on or off, so just map to the appropriate enum value + + if (value) { + modify(_types[_currType], GlobalShieldStatus::HasShields); + } else { + modify(_types[_currType], GlobalShieldStatus::NoShields); + } +} + +const SCP_vector& ShieldSystemDialogModel::getShipTypeOptions() const +{ + return _shipTypeOptions; +} +const SCP_vector& ShieldSystemDialogModel::getTeamOptions() const +{ + return _teamOptions; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ShieldSystemDialogModel.h b/qtfred/src/mission/dialogs/ShieldSystemDialogModel.h index 8eb37b68748..e7604a0d9a9 100644 --- a/qtfred/src/mission/dialogs/ShieldSystemDialogModel.h +++ b/qtfred/src/mission/dialogs/ShieldSystemDialogModel.h @@ -3,9 +3,7 @@ #include "iff_defs/iff_defs.h" #include "mission/dialogs/AbstractDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { class ShieldSystemDialogModel: public AbstractDialogModel { Q_OBJECT @@ -17,32 +15,30 @@ class ShieldSystemDialogModel: public AbstractDialogModel { bool apply() override; void reject() override; - int getCurrentTeam() const { return _currTeam; } - int getCurrentShipType() const { return _currType; } - void setCurrentTeam(int team) { Assert(team >= 0 && team < (int)Iff_info.size()); modify(_currTeam, team); } - void setCurrentShipType(int type) { Assert(type >= 0 && type < MAX_SHIP_CLASSES); modify(_currType, type); } + int getCurrentTeam() const; + int getCurrentShipType() const; + void setCurrentTeam(int team); + void setCurrentShipType(int type); - int getCurrentTeamShieldSys() const { return _teams[_currTeam]; } - int getCurrentTypeShieldSys() const { return _types[_currType]; } - void setCurrentTeamShieldSys(const int value) { Assert(value == 0 || value == 1); modify(_teams[_currTeam], value); } - void setCurrentTypeShieldSys(const int value) { Assert(value == 0 || value == 1); modify(_types[_currType], value); } + GlobalShieldStatus getCurrentTeamShieldSys() const; + GlobalShieldStatus getCurrentTypeShieldSys() const; + void setCurrentTeamShieldSys(bool value); + void setCurrentTypeShieldSys(bool value); - const std::vector& getShipTypeOptions() const { return _shipTypeOptions; } - const std::vector& getTeamOptions() const { return _teamOptions; } + const SCP_vector& getShipTypeOptions() const; + const SCP_vector& getTeamOptions() const; bool query_modified() const; private: void initializeData(); - std::vector _shipTypeOptions; - std::vector _teamOptions; - std::vector _teams; - std::vector _types; - int _currTeam; - int _currType; + SCP_vector _shipTypeOptions; + SCP_vector _teamOptions; + SCP_vector _teams; + SCP_vector _types; + int _currTeam; + int _currType; }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShieldSystemDialog.cpp b/qtfred/src/ui/dialogs/ShieldSystemDialog.cpp index 160f704380a..27975d0fdfc 100644 --- a/qtfred/src/ui/dialogs/ShieldSystemDialog.cpp +++ b/qtfred/src/ui/dialogs/ShieldSystemDialog.cpp @@ -5,9 +5,7 @@ #include "ui_ShieldSystemDialog.h" #include "mission/util.h" -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { ShieldSystemDialog::ShieldSystemDialog(FredView* parent, EditorViewport* viewport) : QDialog(parent), @@ -15,40 +13,45 @@ ShieldSystemDialog::ShieldSystemDialog(FredView* parent, EditorViewport* viewpor ui(new Ui::ShieldSystemDialog()), _model(new ShieldSystemDialogModel(this, viewport)) { ui->setupUi(this); - - connect(this, &QDialog::accepted, _model.get(), &ShieldSystemDialogModel::apply); - connect(ui->dialogButtonBox, &QDialogButtonBox::rejected, this, &ShieldSystemDialog::rejectHandler); - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &ShieldSystemDialog::updateUI); - - connect(ui->shipTeamCombo, static_cast(&QComboBox::currentIndexChanged), this, &ShieldSystemDialog::teamSelectionChanged); - connect(ui->shipTypeCombo, static_cast(&QComboBox::currentIndexChanged), this, &ShieldSystemDialog::typeSelectionChanged); - - connect(ui->teamHasShieldRadio, &QRadioButton::toggled, this, - [this](bool) { _model->setCurrentTeamShieldSys(ui->teamHasShieldRadio->isChecked() ? 0 : 1); }); - connect(ui->teamNoShieldRadio, &QRadioButton::toggled, this, - [this](bool) { _model->setCurrentTeamShieldSys(ui->teamNoShieldRadio->isChecked() ? 1 : 0); }); - connect(ui->typeHasShieldRadio, &QRadioButton::toggled, this, - [this](bool) { _model->setCurrentTypeShieldSys(ui->typeHasShieldRadio->isChecked() ? 0 : 1); }); - connect(ui->typeNoShieldRadio, &QRadioButton::toggled, this, - [this](bool) { _model->setCurrentTypeShieldSys(ui->typeNoShieldRadio->isChecked() ? 1 : 0); }); - - updateUI(); + initializeUi(); + updateUi(); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); } -ShieldSystemDialog::~ShieldSystemDialog() { + +ShieldSystemDialog::~ShieldSystemDialog() = default; + +void ShieldSystemDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close } -void ShieldSystemDialog::updateUI() { - util::SignalBlockers blockers(this); +void ShieldSystemDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close +} - updateTeam(); - updateType(); +void ShieldSystemDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window } -void ShieldSystemDialog::updateTeam() { +void ShieldSystemDialog::initializeUi() { + util::SignalBlockers blockers(this); + if (ui->shipTeamCombo->count() == 0) { for (const auto& teamName : _model->getTeamOptions()) { ui->shipTeamCombo->addItem(QString::fromStdString(teamName)); @@ -57,19 +60,6 @@ void ShieldSystemDialog::updateTeam() { ui->shipTeamCombo->setCurrentIndex(_model->getCurrentTeam()); - const int status = _model->getCurrentTeamShieldSys(); - - ui->teamHasShieldRadio->setChecked(false); - ui->teamNoShieldRadio->setChecked(false); - - if (status == 0) { - ui->teamHasShieldRadio->setChecked(true); - } else if (status == 1) { - ui->teamNoShieldRadio->setChecked(true); - } -} - -void ShieldSystemDialog::updateType() { if (ui->shipTypeCombo->count() == 0) { for (const auto& typeName : _model->getShipTypeOptions()) { ui->shipTypeCombo->addItem(QString::fromStdString(typeName)); @@ -77,51 +67,64 @@ void ShieldSystemDialog::updateType() { } ui->shipTypeCombo->setCurrentIndex(_model->getCurrentShipType()); +} - const int status = _model->getCurrentTypeShieldSys(); +void ShieldSystemDialog::updateUi() { + util::SignalBlockers blockers(this); - ui->typeHasShieldRadio->setChecked(false); - ui->typeNoShieldRadio->setChecked(false); + auto typeShieldSys = _model->getCurrentTypeShieldSys(); + ui->typeHasShieldRadio->setChecked(typeShieldSys == GlobalShieldStatus::HasShields); + ui->typeNoShieldRadio->setChecked(typeShieldSys == GlobalShieldStatus::NoShields); - if (status == 0) { - ui->typeHasShieldRadio->setChecked(true); - } else if (status == 1) { - ui->typeNoShieldRadio->setChecked(true); - } + auto teamShieldSys = _model->getCurrentTeamShieldSys(); + ui->teamHasShieldRadio->setChecked(teamShieldSys == GlobalShieldStatus::HasShields); + ui->teamNoShieldRadio->setChecked(teamShieldSys == GlobalShieldStatus::NoShields); } -void ShieldSystemDialog::teamSelectionChanged(int index) { +void ShieldSystemDialog::on_okAndCancelButtons_accepted() { + accept(); +} + +void ShieldSystemDialog::on_okAndCancelButtons_rejected() { + reject(); +} + +void ShieldSystemDialog::on_shipTypeCombo_currentIndexChanged(int index) { if (index >= 0) { _model->setCurrentTeam(index); } + updateUi(); } -void ShieldSystemDialog::typeSelectionChanged(int index) { +void ShieldSystemDialog::on_shipTeamCombo_currentIndexChanged(int index) { if (index >= 0) { _model->setCurrentShipType(index); } + updateUi(); } -void ShieldSystemDialog::keyPressEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Escape) { - // Instead of calling reject when we close a dialog it should try to close the window which will will allow the - // user to save unsaved changes - event->ignore(); - this->close(); - return; +void ShieldSystemDialog::on_typeHasShieldRadio_toggled(bool checked) { + if (checked) { + _model->setCurrentTypeShieldSys(checked); } - QDialog::keyPressEvent(event); } -void ShieldSystemDialog::closeEvent(QCloseEvent* e) { - if (!rejectOrCloseHandler(this, _model.get(), _viewport)) { - e->ignore(); - }; -} -void ShieldSystemDialog::rejectHandler() -{ - this->close(); -} +void ShieldSystemDialog::on_typeNoShieldRadio_toggled(bool checked) { + if (checked) { + _model->setCurrentTypeShieldSys(!checked); + } } + +void ShieldSystemDialog::on_teamHasShieldRadio_toggled(bool checked) { + if (checked) { + _model->setCurrentTeamShieldSys(checked); + } } + +void ShieldSystemDialog::on_teamNoShieldRadio_toggled(bool checked) { + if (checked) { + _model->setCurrentTeamShieldSys(!checked); + } } + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/ShieldSystemDialog.h b/qtfred/src/ui/dialogs/ShieldSystemDialog.h index dc7298cc4f5..7fd8a97bfca 100644 --- a/qtfred/src/ui/dialogs/ShieldSystemDialog.h +++ b/qtfred/src/ui/dialogs/ShieldSystemDialog.h @@ -5,9 +5,7 @@ #include #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class ShieldSystemDialog; @@ -21,25 +19,31 @@ class ShieldSystemDialog : public QDialog explicit ShieldSystemDialog(FredView* parent, EditorViewport* viewport); ~ShieldSystemDialog() override; + void accept() override; + void reject() override; + protected: - void keyPressEvent(QKeyEvent* event) override; - void closeEvent(QCloseEvent*) override; + void closeEvent(QCloseEvent* e) override; // funnel all Window X presses through reject() + +private slots: + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); - void rejectHandler(); + void on_shipTypeCombo_currentIndexChanged(int index); + void on_shipTeamCombo_currentIndexChanged(int index); - private: - void updateUI(); - void updateTeam(); - void updateType(); + void on_typeHasShieldRadio_toggled(bool checked); + void on_typeNoShieldRadio_toggled(bool checked); + void on_teamHasShieldRadio_toggled(bool checked); + void on_teamNoShieldRadio_toggled(bool checked); - void teamSelectionChanged(int index); - void typeSelectionChanged(int index); +private: // NOLINT(readability-redundant-access-specifiers) + void initializeUi(); + void updateUi(); EditorViewport * _viewport = nullptr; std::unique_ptr ui; std::unique_ptr _model; }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/ShieldSystemDialog.ui b/qtfred/ui/ShieldSystemDialog.ui index fc66fc7fe6d..623d134178c 100644 --- a/qtfred/ui/ShieldSystemDialog.ui +++ b/qtfred/ui/ShieldSystemDialog.ui @@ -2,109 +2,115 @@ fso::fred::dialogs::ShieldSystemDialog + + Qt::WindowModal + 0 0 - 310 - 161 + 441 + 150 + + + 0 + 0 + + + + + 16777215 + 16777215 + + Shield System Editor - - - - - All ships of type - - - - 15 - - - 9 - - - - - - - - Has shield system + + + + + + + All ships of type + + + + 15 - - typeShieldOptionsButtonGroup - - - - - - - No shield system + + 9 - - typeShieldOptionsButtonGroup - - - - - - - - - - All ships of team - - - - 9 - - - 9 - - - - - - - - Has shield system + + + + + + + Has shield system + + + typeShieldOptionsButtonGroup + + + + + + + No shield system + + + typeShieldOptionsButtonGroup + + + + + + + + + + All ships of team + + + + 9 - - teamShieldOptionsButtonGroup - - - - - - - No shield system + + 9 - - teamShieldOptionsButtonGroup - - - - - - - - - - Qt::Vertical - - - - 20 - 28 - - - + + + + + + + Has shield system + + + teamShieldOptionsButtonGroup + + + + + + + No shield system + + + teamShieldOptionsButtonGroup + + + + + + + - - + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok @@ -113,24 +119,7 @@ - - - dialogButtonBox - accepted() - fso::fred::dialogs::ShieldSystemDialog - accept() - - - 195 - 132 - - - 154 - 80 - - - - + From c2b92410aa69a284ac5c365ea3f094bc936e9da8 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 06:44:07 -0500 Subject: [PATCH 416/466] QtFRED Global Ship Flags Dialog (#6993) * Global Ship Flags Dialog * modernize old fred code --- qtfred/source_groups.cmake | 5 ++ .../dialogs/GlobalShipFlagsDialogModel.cpp | 65 +++++++++++++++++++ .../dialogs/GlobalShipFlagsDialogModel.h | 26 ++++++++ qtfred/src/ui/FredView.cpp | 7 ++ qtfred/src/ui/FredView.h | 1 + .../src/ui/dialogs/GlobalShipFlagsDialog.cpp | 62 ++++++++++++++++++ qtfred/src/ui/dialogs/GlobalShipFlagsDialog.h | 34 ++++++++++ qtfred/ui/GlobalShipFlags.ui | 49 ++++++++++++++ qtfred/ui/GlobalShipFlagsDialog.ui | 49 ++++++++++++++ 9 files changed, 298 insertions(+) create mode 100644 qtfred/src/mission/dialogs/GlobalShipFlagsDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/GlobalShipFlagsDialogModel.h create mode 100644 qtfred/src/ui/dialogs/GlobalShipFlagsDialog.cpp create mode 100644 qtfred/src/ui/dialogs/GlobalShipFlagsDialog.h create mode 100644 qtfred/ui/GlobalShipFlags.ui create mode 100644 qtfred/ui/GlobalShipFlagsDialog.ui diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index 60d643aa332..ca0f65c5521 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -50,6 +50,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/FictionViewerDialogModel.h src/mission/dialogs/FormWingDialogModel.cpp src/mission/dialogs/FormWingDialogModel.h + src/mission/dialogs/GlobalShipFlagsDialogModel.cpp + src/mission/dialogs/GlobalShipFlagsDialogModel.h src/mission/dialogs/JumpNodeEditorDialogModel.cpp src/mission/dialogs/JumpNodeEditorDialogModel.h src/mission/dialogs/LoadoutEditorDialogModel.cpp @@ -142,6 +144,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/FictionViewerDialog.h src/ui/dialogs/FormWingDialog.cpp src/ui/dialogs/FormWingDialog.h + src/ui/dialogs/GlobalShipFlagsDialog.cpp + src/ui/dialogs/GlobalShipFlagsDialog.h src/ui/dialogs/JumpNodeEditorDialog.cpp src/ui/dialogs/JumpNodeEditorDialog.h src/ui/dialogs/LoadoutDialog.cpp @@ -257,6 +261,7 @@ add_file_folder("UI" ui/FictionViewerDialog.ui ui/FormWingDialog.ui ui/FredView.ui + ui/GlobalShipFlagsDialog.ui ui/JumpNodeEditorDialog.ui ui/LoadoutDialog.ui ui/MissionCutscenesDialog.ui diff --git a/qtfred/src/mission/dialogs/GlobalShipFlagsDialogModel.cpp b/qtfred/src/mission/dialogs/GlobalShipFlagsDialogModel.cpp new file mode 100644 index 00000000000..ee316daf46a --- /dev/null +++ b/qtfred/src/mission/dialogs/GlobalShipFlagsDialogModel.cpp @@ -0,0 +1,65 @@ +#include +#include +#include +#include "mission/dialogs/GlobalShipFlagsDialogModel.h" + +namespace fso::fred::dialogs { + +GlobalShipFlagsDialogModel::GlobalShipFlagsDialogModel(QObject* parent, EditorViewport* viewport) : + AbstractDialogModel(parent, viewport) { + +} + +bool GlobalShipFlagsDialogModel::apply() { + // nothing to do + return true; +} +void GlobalShipFlagsDialogModel::reject() { + // nothing to do +} + +void GlobalShipFlagsDialogModel::setNoShieldsAll() +{ + for (auto& ship : Ships) { + if (ship.objnum >= 0) { + Objects[ship.objnum].flags.set(Object::Object_Flags::No_shields); + } + } +} + +void GlobalShipFlagsDialogModel::setNoSubspaceDriveOnFightersBombers() +{ + for (auto& ship : Ships) { + if (ship.objnum >= 0) { + // only for fighters and bombers + ship.flags.set(Ship::Ship_Flags::No_subspace_drive, + Ship_info[ship.ship_info_index].is_fighter_bomber()); + } + } +} + +void GlobalShipFlagsDialogModel::setPrimitiveSensorsOnFightersBombers() +{ + for (auto& ship : Ships) { + if (ship.objnum >= 0) { + // only for fighters and bombers + ship.flags.set(Ship::Ship_Flags::Primitive_sensors, + Ship_info[ship.ship_info_index].is_fighter_bomber()); + } + } +} + +void GlobalShipFlagsDialogModel::setAffectedByGravityOnFightersBombers() +{ + // FRED only affects fighters and bombers.. that seems really strange for this one + + for (auto& ship : Ships) { + if (ship.objnum >= 0) { + // only for fighters and bombers + ship.flags.set(Ship::Ship_Flags::Affected_by_gravity, + Ship_info[ship.ship_info_index].is_fighter_bomber()); + } + } +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/GlobalShipFlagsDialogModel.h b/qtfred/src/mission/dialogs/GlobalShipFlagsDialogModel.h new file mode 100644 index 00000000000..da90be73f35 --- /dev/null +++ b/qtfred/src/mission/dialogs/GlobalShipFlagsDialogModel.h @@ -0,0 +1,26 @@ +#pragma once + +#include "mission/dialogs/AbstractDialogModel.h" + +namespace fso::fred::dialogs { + +class GlobalShipFlagsDialogModel : public AbstractDialogModel { + Q_OBJECT + + public: + GlobalShipFlagsDialogModel(QObject* parent, EditorViewport* viewport); + ~GlobalShipFlagsDialogModel() override = default; + + bool apply() override; + void reject() override; + + static void setNoShieldsAll(); + + static void setNoSubspaceDriveOnFightersBombers(); + + static void setPrimitiveSensorsOnFightersBombers(); + + static void setAffectedByGravityOnFightersBombers(); +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 34b1abd6f70..988f0799bd0 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -1202,6 +1203,12 @@ void FredView::on_actionShield_System_triggered(bool) { dialog->show(); } +void FredView::on_actionSet_Global_Ship_Flags_triggered(bool) { + auto dialog = new dialogs::GlobalShipFlagsDialog(this, _viewport); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); +} + void FredView::on_actionVoice_Acting_Manager_triggered(bool) { auto dialog = new dialogs::VoiceActingManager(this, _viewport); dialog->setAttribute(Qt::WA_DeleteOnClose); diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index e0a55f1fceb..5d489faad78 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -147,6 +147,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionAbout_triggered(bool); void on_actionBackground_triggered(bool); void on_actionShield_System_triggered(bool); + void on_actionSet_Global_Ship_Flags_triggered(bool); void on_actionVoice_Acting_Manager_triggered(bool); void on_actionFiction_Viewer_triggered(bool); void on_actionMission_Goals_triggered(bool); diff --git a/qtfred/src/ui/dialogs/GlobalShipFlagsDialog.cpp b/qtfred/src/ui/dialogs/GlobalShipFlagsDialog.cpp new file mode 100644 index 00000000000..ea63a9a1d52 --- /dev/null +++ b/qtfred/src/ui/dialogs/GlobalShipFlagsDialog.cpp @@ -0,0 +1,62 @@ +#include +#include +#include "GlobalShipFlagsDialog.h" +#include "ui/util/SignalBlockers.h" +#include "ui_GlobalShipFlagsDialog.h" +#include "mission/util.h" + +namespace fso::fred::dialogs { + +GlobalShipFlagsDialog::GlobalShipFlagsDialog(FredView* parent, EditorViewport* viewport) : + QDialog(parent), _viewport(viewport), ui(new Ui::GlobalShipFlagsDialog()), _model(new GlobalShipFlagsDialogModel(this, viewport)) { + ui->setupUi(this); +} +GlobalShipFlagsDialog::~GlobalShipFlagsDialog() = default; + +void GlobalShipFlagsDialog::on_noShieldsButton_clicked() +{ + auto result = _viewport->dialogProvider->showButtonDialog(DialogType::Question, + "Set No Shields", + "Are you sure you want to set the No Shields flag for all ships? This is immediate and cannot be undone!", + {DialogButton::Yes, DialogButton::No}); + if (result == DialogButton::Yes) { + _model->setNoShieldsAll(); + } +} + +void GlobalShipFlagsDialog::on_noSubspaceDriveButton_clicked() +{ + auto result = _viewport->dialogProvider->showButtonDialog(DialogType::Question, + "Set No Subspace Drive", + "Are you sure you want to set the No Subspace Drive flag for all fighters and bombers? This is immediate and cannot be undone!", + {DialogButton::Yes, DialogButton::No}); + if (result == DialogButton::Yes) { + _model->setNoSubspaceDriveOnFightersBombers(); + } +} + +void GlobalShipFlagsDialog::on_primitiveSensorsButton_clicked() +{ + auto result = _viewport->dialogProvider->showButtonDialog(DialogType::Question, + "Set Primitive Sensors", + "Are you sure you want to set the Primitive Sensors flag for all fighters and bombers? This is immediate and " + "cannot be undone!", + {DialogButton::Yes, DialogButton::No}); + if (result == DialogButton::Yes) { + _model->setPrimitiveSensorsOnFightersBombers(); + } +} + +void GlobalShipFlagsDialog::on_affectedByGravityButton_clicked() +{ + auto result = _viewport->dialogProvider->showButtonDialog(DialogType::Question, + "Set Affected by Gravity", + "Are you sure you want to set the Affected by Gravity flag for all fighters and bombers? This is immediate and " + "cannot be undone!", + {DialogButton::Yes, DialogButton::No}); + if (result == DialogButton::Yes) { + _model->setAffectedByGravityOnFightersBombers(); + } +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/GlobalShipFlagsDialog.h b/qtfred/src/ui/dialogs/GlobalShipFlagsDialog.h new file mode 100644 index 00000000000..70116671b8f --- /dev/null +++ b/qtfred/src/ui/dialogs/GlobalShipFlagsDialog.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class GlobalShipFlagsDialog; +} + +class GlobalShipFlagsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit GlobalShipFlagsDialog(FredView* parent, EditorViewport* viewport); + ~GlobalShipFlagsDialog() override; + +private slots: + void on_noShieldsButton_clicked(); + void on_noSubspaceDriveButton_clicked(); + void on_primitiveSensorsButton_clicked(); + void on_affectedByGravityButton_clicked(); + +private: // NOLINT(readability-redundant-access-specifiers) + EditorViewport * _viewport = nullptr; + std::unique_ptr ui; + std::unique_ptr _model; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/GlobalShipFlags.ui b/qtfred/ui/GlobalShipFlags.ui new file mode 100644 index 00000000000..2767401109c --- /dev/null +++ b/qtfred/ui/GlobalShipFlags.ui @@ -0,0 +1,49 @@ + + + fso::fred::dialogs::GlobalShipFlagsDialog + + + + 0 + 0 + 321 + 128 + + + + Global Ship Flags + + + + + + Global No-Shields + + + + + + + Global No-Subspace-Drive + + + + + + + Global Primitive-Sensors + + + + + + + Global Affected-By-Gravity + + + + + + + + diff --git a/qtfred/ui/GlobalShipFlagsDialog.ui b/qtfred/ui/GlobalShipFlagsDialog.ui new file mode 100644 index 00000000000..87c535ea019 --- /dev/null +++ b/qtfred/ui/GlobalShipFlagsDialog.ui @@ -0,0 +1,49 @@ + + + fso::fred::dialogs::GlobalShipFlagsDialog + + + + 0 + 0 + 154 + 128 + + + + Global Ship Flags + + + + + + Global No-Shields + + + + + + + Global No-Subspace-Drive + + + + + + + Global Primitive-Sensors + + + + + + + Global Affected-By-Gravity + + + + + + + + From 109c2b5a811c2566d470efcfc694494bfe75995f Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 08:59:16 -0500 Subject: [PATCH 417/466] QtFRED Music Player Dialog (#6994) * qtfred music player dialog * cleanup * int comparisons * clang wins --- qtfred/resources/images/next.png | Bin 0 -> 2467 bytes qtfred/resources/images/prev.png | Bin 0 -> 2471 bytes qtfred/resources/images/stop.png | Bin 0 -> 2452 bytes qtfred/source_groups.cmake | 12 ++ .../dialogs/MusicPlayerDialogModel.cpp | 131 +++++++++++++++ .../mission/dialogs/MusicPlayerDialogModel.h | 60 +++++++ .../mission/dialogs/MusicTBLViewerModel.cpp | 59 +++++++ .../src/mission/dialogs/MusicTBLViewerModel.h | 18 +++ qtfred/src/ui/FredView.cpp | 8 + qtfred/src/ui/FredView.h | 1 + qtfred/src/ui/dialogs/MusicPlayerDialog.cpp | 150 ++++++++++++++++++ qtfred/src/ui/dialogs/MusicPlayerDialog.h | 50 ++++++ qtfred/src/ui/dialogs/MusicTBLViewer.cpp | 35 ++++ qtfred/src/ui/dialogs/MusicTBLViewer.h | 29 ++++ qtfred/ui/FredView.ui | 6 + qtfred/ui/MusicPlayerDialog.ui | 114 +++++++++++++ 16 files changed, 673 insertions(+) create mode 100644 qtfred/resources/images/next.png create mode 100644 qtfred/resources/images/prev.png create mode 100644 qtfred/resources/images/stop.png create mode 100644 qtfred/src/mission/dialogs/MusicPlayerDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/MusicPlayerDialogModel.h create mode 100644 qtfred/src/mission/dialogs/MusicTBLViewerModel.cpp create mode 100644 qtfred/src/mission/dialogs/MusicTBLViewerModel.h create mode 100644 qtfred/src/ui/dialogs/MusicPlayerDialog.cpp create mode 100644 qtfred/src/ui/dialogs/MusicPlayerDialog.h create mode 100644 qtfred/src/ui/dialogs/MusicTBLViewer.cpp create mode 100644 qtfred/src/ui/dialogs/MusicTBLViewer.h create mode 100644 qtfred/ui/MusicPlayerDialog.ui diff --git a/qtfred/resources/images/next.png b/qtfred/resources/images/next.png new file mode 100644 index 0000000000000000000000000000000000000000..42efffd2bfe7d3fc37bea422a1e291d49e93566c GIT binary patch literal 2467 zcmeHITW{P%6gG_%m0q3@5W$P(q6!eU$G6xX)l^|Ouq$OlNLNbw*710}Yh~?mZD)5A zsnQA|A>QB(5EA?X#5)hU&WS3s@}{XJ==N#o`?H&!zKvfDvY| zu2&5F&lotE{yYbrzn=FVmhgSNA@0|tAAj1Ar1f``;bC<+*uj*i8i{zww0W9?wItoU zKhFt0VwD`SQIZA9U%&sM$Vn6^2hIQu@(vp(z0-m{INcr6(L^gQghTmouqStT!DL@EREm%(yS^4%j?H5Fk!&K<(hWmL zzG|4*M%Z-a=2F025yg1F`)T70R)I3EsvPV3Y&O$omd1;bZuq{hBU3j`6)e>9B&*0= z&B{9~gf1&-k>pjvGg%;#kRMlp0--K*Nb?ppD;u7GF?~*Q-O!N8sWyygYnLAvliGPi zbv9us%c>G=jn+0F^NN>aeg$;<`WXkn?O@RQxHOhDZC#Yr_7u`+2)Wd{9G>J%-)AL1 zE-2fcLYQ|%HaYGTj8wcBaz0sAYHt;?Y&x1PzkiTq5ucSGTsXkGq+)?0s-_}aMa~fU z7&+KLFvkcjO$R(m;*-|YQIV&b*3j^p|wJfv#Zw1uvE;Kk`v)+QPDW+||hDF=)Sq9Jr^z*mBduqYIKHM)|CZ8g;fzQSy)uOlbg?(TsAJXGunYpE!~0Ex4pT!nF*cbW|MSg z+6Ng#kbU+|{P+U|*>@l8t1rIme;^3{3H2nCiCt-T7awH<$^FQ=zjMy-{BrVWZ};Py zH{QIVD9X+Lc6T4fa{lu5b$C8}{O5a$vaamy412I#RQIA97O+|@&d$yji^VzYpDX9p z03*y^TrU{-pD}PS{eBKQe<|JmG>7lw)$(3bzW(NxqHMes4G)XM!4BmzZm~f6qBW0G zuvV12_vR_%N1{N!7)6Pv{`J$(DvAP6J+KF4kZy@_w0)Y1`=`4@etN{+K)v??x;v); z5Q~DLc|1vSI``BHm%_Z<)>KrP6i1%ASvo|AgFUn*GlATejyWL)YP+q_v@H?x_mM#i zQ`2>gxL7x+MX1q6^`(NlEC}g-_oLbwtUPsG6e-oT*=*LDnJt-(G~IPwjTo9?V6edX zNm8&mPV(C;gs#YW7Ntcb6I3EHUmh2p3ZX7@h|>l($!nf~F>TILO>dDhr^+zkja_=2 zO)BRB*Th7`A}MmP)f?M%EDM>Bn9ulw}V0B{cLYTM9Y*MLKuSs^^NtQsa3 zCialHl-N`!FsFnpO$RcF!js0-#-xJ{bEtQyNvP>QVG5cFSizoY9B>-SEM|~!6tj`g z(qyEfhDN$2Co%)aWpq^(g8}U)dBGAc`dv?joLf;8P}>z9$6+CMgiWyNSQhT+#KDeX z8HVNCZrf(7^Igf0%gUDLmnsh=2aJEbx~AS?AZBdZ4q$MLVb|6J9GGp}G+m*GzQ02I zB#YofW0Q-hWmEwm2BOpEqHSR-v~BE$mcWcTz*Ok^PH6j< zlc~rqgrBmEL5~qx?x|rWW3&{4AE&60^`cb8m54c;KAnu`6UQ>khK#G^{a?taiRNRL zj0E&ztx85Ko7S?-+P@W0y}QuhfX!+Px@I|T`ZX+?jxQ2`&Y+)v7aPC9zu->4yE(l6 zHh=Kt`Ud*KL^~UuuhxDI7Umnb9zRll=s9b@Tzf@PKKtX^>%YHt=iBo4(eLecfBf*l G=YIo5gYMq| literal 0 HcmV?d00001 diff --git a/qtfred/resources/images/stop.png b/qtfred/resources/images/stop.png new file mode 100644 index 0000000000000000000000000000000000000000..86d42b9ebbfec7d2a323f6bca78912fd9160b201 GIT binary patch literal 2452 zcmeHIPjB2r6nCiL=FUJzU?SE>qP?D4!~M zZYzqiIocf_!1y}cZ{E5I&oB4JPZi~+vi~UDf#tHfm(8$*)pB`$e!g5TFJS*dxo8F$ zVfN~J#lZiJfy3E%3()x+`R->Wd>?n}`-bxL+qV^E6YIv-cYlBS)Dvf7zJ8Z9ALT9@HzE{p?F z%9DbLof(9Ax6URf+l8Q&EJ7)#>q_mfLsl(UchvibNyg>8eDBf$F{G97wYq8uI|#cW z_6T-~iD6DKUYQ1xC-G@#>LNTqmK~Y{VnczwXG}peoL2OO#+;E@7Ab{tEl@-mH7!7?5a@LcC6F0V^*z!1p@5YFmP9IU%@euFZJv(+W z?b~#Hekj>VUD^8lO66QK!1%|ji%ssiqK^h1vydA*4vJ{hLpFIw}r`4(?%dU|WdC0%(mL8$~@}P2d5yT|0^eb!s&?0wKFe38g!2zmLb_^L92( z)J7A8vRdoD#_A#quC?3A6@0vu=l1-3A^`L`#b0?ZWn9f^A-2ZA-3!HKTpPU1W}>(h zeohMtJw_Czuf>H-)s+yUG*>IxE^1X=i&)ax^T~KQajdfJ$hc14|AmZ}Xf~nQSU@i} znq-W+X|2j^{96IdyGsoY*lf0-Yu3}QU&Es7_#y-70{Z#;KZbw6zu?YjxE0>|g+2L7 j*_ds-_5Ev`HkI1S literal 0 HcmV?d00001 diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index ca0f65c5521..62bdcbb52a6 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -62,6 +62,10 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/MissionGoalsDialogModel.h src/mission/dialogs/MissionSpecDialogModel.cpp src/mission/dialogs/MissionSpecDialogModel.h + src/mission/dialogs/MusicPlayerDialogModel.cpp + src/mission/dialogs/MusicPlayerDialogModel.h + src/mission/dialogs/MusicTBLViewerModel.cpp + src/mission/dialogs/MusicTBLViewerModel.h src/mission/dialogs/ObjectOrientEditorDialogModel.cpp src/mission/dialogs/ObjectOrientEditorDialogModel.h src/mission/dialogs/ReinforcementsEditorDialogModel.cpp @@ -156,6 +160,10 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/MissionGoalsDialog.h src/ui/dialogs/MissionSpecDialog.cpp src/ui/dialogs/MissionSpecDialog.h + src/ui/dialogs/MusicPlayerDialog.cpp + src/ui/dialogs/MusicPlayerDialog.h + src/ui/dialogs/MusicTBLViewer.cpp + src/ui/dialogs/MusicTBLViewer.h src/ui/dialogs/ObjectOrientEditorDialog.cpp src/ui/dialogs/ObjectOrientEditorDialog.h src/ui/dialogs/ReinforcementsEditorDialog.cpp @@ -267,6 +275,7 @@ add_file_folder("UI" ui/MissionCutscenesDialog.ui ui/MissionGoalsDialog.ui ui/MissionSpecDialog.ui + ui/MusicPlayerDialog.ui ui/ObjectOrientationDialog.ui ui/ReinforcementsDialog.ui ui/SelectionDialog.ui @@ -342,8 +351,10 @@ add_file_folder("Resources/Images" resources/images/fredknows.png resources/images/fred_splash.png resources/images/green_do.png + resources/images/next.png resources/images/orbitsel.png resources/images/play.png + resources/images/prev.png resources/images/root_directive.png resources/images/root.png resources/images/rotlocal.png @@ -354,6 +365,7 @@ add_file_folder("Resources/Images" resources/images/selectrot.png resources/images/showdist.png resources/images/splash.png + resources/images/stop.png resources/images/toolbar1.png resources/images/toolbar.png resources/images/V_fred.ico diff --git a/qtfred/src/mission/dialogs/MusicPlayerDialogModel.cpp b/qtfred/src/mission/dialogs/MusicPlayerDialogModel.cpp new file mode 100644 index 00000000000..371c8a32a61 --- /dev/null +++ b/qtfred/src/mission/dialogs/MusicPlayerDialogModel.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include "mission/dialogs/MusicPlayerDialogModel.h" + +namespace fso::fred::dialogs { + +MusicPlayerDialogModel::MusicPlayerDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + // No persisted state to prefill +} + +bool MusicPlayerDialogModel::apply() +{ + // No changes to apply, just return true + return true; +} + +void MusicPlayerDialogModel::reject() +{ + stop(); // stop playback on dialog close + // No other cleanup needed +} + +void MusicPlayerDialogModel::loadTracks() +{ + _tracks.clear(); + + SCP_vector files; + cf_get_file_list(files, CF_TYPE_MUSIC, "*", CF_SORT_NAME); + + for (const auto& f : files) { + bool add = true; + for (const auto& ignored : Ignored_music_player_files) { + if (lcase_equal(ignored, f)) { + add = false; + break; + } + } + if (add) { + _tracks.push_back(f); + } + } + // Reset selection when repopulating + modify(_currentRow, -1); +} + +void MusicPlayerDialogModel::setCurrentRow(int row) +{ + if (row < -1 || row >= static_cast(_tracks.size())) + return; + modify(_currentRow, row); +} + +SCP_string MusicPlayerDialogModel::currentItemName() const +{ + if (!SCP_vector_inbounds(_tracks, _currentRow)) + return ""; + return _tracks.at(_currentRow); +} + +int MusicPlayerDialogModel::tryOpenStream(const SCP_string& baseNoExt) +{ + // cfile strips extensions in some contexts; try .wav then .ogg + int id = audiostream_open((baseNoExt + ".wav").c_str(), ASF_EVENTMUSIC); + if (id < 0) { + id = audiostream_open((baseNoExt + ".ogg").c_str(), ASF_EVENTMUSIC); + } + return id; +} + +bool MusicPlayerDialogModel::isPlaying() const +{ + return _musicId >= 0 && audiostream_is_playing(_musicId); +} + +void MusicPlayerDialogModel::play() +{ + stop(); // close any previous stream as in the original + const auto name = currentItemName(); + if (name.empty()) + return; + + _musicId = tryOpenStream(name); + if (_musicId >= 0) { + audiostream_play(_musicId, 1.0f, 0); + } else { + Warning(LOCATION, "FRED failed to open music file %s in the music player\n", name.c_str()); + } +} + +void MusicPlayerDialogModel::stop() +{ + if (_musicId >= 0) { + audiostream_close_file(_musicId, false); + _musicId = -1; + } +} + +bool MusicPlayerDialogModel::selectNext() +{ + if (_currentRow >= 0 && _currentRow < static_cast(_tracks.size()) - 1) { + modify(_currentRow, _currentRow + 1); + return true; + } + return false; +} + +bool MusicPlayerDialogModel::selectPrev() +{ + if (_currentRow > 0 && _currentRow < static_cast(_tracks.size())) { + modify(_currentRow, _currentRow - 1); + return true; + } + return false; +} + +void MusicPlayerDialogModel::tick() +{ + // If playback just finished: autoplay advances and plays; else stop + if (_musicId >= 0 && !audiostream_is_playing(_musicId)) { + if (_autoplay && selectNext()) { + play(); + } else { + stop(); + } + } +} + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/MusicPlayerDialogModel.h b/qtfred/src/mission/dialogs/MusicPlayerDialogModel.h new file mode 100644 index 00000000000..14172c46891 --- /dev/null +++ b/qtfred/src/mission/dialogs/MusicPlayerDialogModel.h @@ -0,0 +1,60 @@ +#pragma once + +#include "mission/dialogs/AbstractDialogModel.h" + +namespace fso::fred::dialogs { + +class MusicPlayerDialogModel final : public AbstractDialogModel { + Q_OBJECT + public: + explicit MusicPlayerDialogModel(QObject* parent, EditorViewport* viewport); + ~MusicPlayerDialogModel() override = default; + + bool apply() override; + void reject() override; + + // lifecycle + void loadTracks(); + + // data + const SCP_vector& tracks() const + { + return _tracks; + } + int currentRow() const + { + return _currentRow; + } + void setCurrentRow(int row); + + // playback + bool isPlaying() const; + void play(); + void stop(); + bool selectNext(); // advances selection (returns true if changed) + bool selectPrev(); // advances selection (returns true if changed) + + // autoplay + bool autoplay() const + { + return _autoplay; + } + void setAutoplay(bool on) + { + modify(_autoplay, on); + } + + // polling tick (call from a QTimer in the dialog) + void tick(); + + private: + SCP_string currentItemName() const; // without extension + static int tryOpenStream(const SCP_string& baseNoExt); // .wav first, then .ogg + + SCP_vector _tracks; + int _currentRow = -1; + int _musicId = -1; // audiostream id or -1 when none + bool _autoplay = false; +}; + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/MusicTBLViewerModel.cpp b/qtfred/src/mission/dialogs/MusicTBLViewerModel.cpp new file mode 100644 index 00000000000..c2e9b26d23d --- /dev/null +++ b/qtfred/src/mission/dialogs/MusicTBLViewerModel.cpp @@ -0,0 +1,59 @@ +#include "MusicTBLViewerModel.h" + +#include +namespace fso::fred::dialogs { +MusicTBLViewerModel::MusicTBLViewerModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + initializeData(); +} +bool MusicTBLViewerModel::apply() +{ + return true; +} +void MusicTBLViewerModel::reject() {} +void MusicTBLViewerModel::initializeData() +{ + char line[256]{}; + CFILE* fp = nullptr; + SCP_vector tbl_file_names; + + text.clear(); + + // Base table + text += "-- music.tbl -------------------------------\r\n"; + fp = cfopen("music.tbl", "r"); + Assert(fp); + while (cfgets(line, 255, fp)) { + text += line; + text += "\r\n"; + } + cfclose(fp); + + // Modular tables (*-mus.tbm), reverse sorted to match legacy behavior + const int num_files = cf_get_file_list(tbl_file_names, CF_TYPE_TABLES, NOX("*-mus.tbm"), CF_SORT_REVERSE); + for (int n = 0; n < num_files; ++n) { + tbl_file_names[n] += ".tbm"; + + text += "-- "; + text += tbl_file_names[n]; + text += " -------------------------------\r\n"; + + fp = cfopen(tbl_file_names[n].c_str(), "r"); + Assert(fp); + + memset(line, 0, sizeof(line)); + while (cfgets(line, 255, fp)) { + text += line; + text += "\r\n"; + } + cfclose(fp); + } + + modelChanged(); +} +SCP_string MusicTBLViewerModel::getText() const +{ + return text; +} +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/MusicTBLViewerModel.h b/qtfred/src/mission/dialogs/MusicTBLViewerModel.h new file mode 100644 index 00000000000..0e627a3c266 --- /dev/null +++ b/qtfred/src/mission/dialogs/MusicTBLViewerModel.h @@ -0,0 +1,18 @@ +#pragma once + +#include "AbstractDialogModel.h" + +namespace fso::fred::dialogs { +class MusicTBLViewerModel : public AbstractDialogModel { + private: + SCP_string text; + + public: + MusicTBLViewerModel(QObject* parent, EditorViewport* viewport); + bool apply() override; + void reject() override; + void initializeData(); + + SCP_string getText() const; +}; +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 988f0799bd0..0e316bc4a77 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #include #include "mission/Editor.h" @@ -1220,6 +1221,13 @@ void FredView::on_actionMission_Goals_triggered(bool) { dialog->show(); } +void FredView::on_actionMusic_Player_triggered(bool) +{ + auto dialog = new dialogs::MusicPlayerDialog(this, _viewport); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); +} + void FredView::on_actionFiction_Viewer_triggered(bool) { auto dialog = new dialogs::FictionViewerDialog(this, _viewport); dialog->setAttribute(Qt::WA_DeleteOnClose); diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 5d489faad78..98b194f681c 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -151,6 +151,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionVoice_Acting_Manager_triggered(bool); void on_actionFiction_Viewer_triggered(bool); void on_actionMission_Goals_triggered(bool); + void on_actionMusic_Player_triggered(bool); signals: /** * @brief Special version of FredApplication::onIdle which is limited to the lifetime of this object diff --git a/qtfred/src/ui/dialogs/MusicPlayerDialog.cpp b/qtfred/src/ui/dialogs/MusicPlayerDialog.cpp new file mode 100644 index 00000000000..bfae519b187 --- /dev/null +++ b/qtfred/src/ui/dialogs/MusicPlayerDialog.cpp @@ -0,0 +1,150 @@ +#include +#include +#include "MusicTBLViewer.h" +#include "MusicPlayerDialog.h" +#include "ui/util/SignalBlockers.h" +#include "ui_MusicPlayerDialog.h" + +namespace fso::fred::dialogs { + +MusicPlayerDialog::MusicPlayerDialog(FredView* parent, EditorViewport* viewport) + : QDialog(parent), _viewport(viewport), ui(new Ui::MusicPlayerDialog()), + _model(new MusicPlayerDialogModel(this, viewport)) +{ + setFocus(); + ui->setupUi(this); + + // build list + _model->loadTracks(); + populateList(); + syncButtonsEnabled(); + + // 200ms poll to mirror DoFrame() autoplay behavior + _timer.setInterval(200); + connect(&_timer, &QTimer::timeout, this, &MusicPlayerDialog::onTick); + _timer.start(); +} + +MusicPlayerDialog::~MusicPlayerDialog() = default; + +void MusicPlayerDialog::accept() +{ + // not used +} + +void MusicPlayerDialog::reject() +{ + _model->stop(); + QDialog::reject(); +} + +void MusicPlayerDialog::closeEvent(QCloseEvent* /*e*/) +{ + reject(); +} + +// --- UI sync helpers --- + +void MusicPlayerDialog::populateList() +{ + util::SignalBlockers block(this); + ui->musicList->clear(); + QStringList items; + for (const auto& track : _model->tracks()) { + // Add items without extension + items.append(QString::fromStdString(track)); + } + ui->musicList->addItems(items); +} + +void MusicPlayerDialog::syncSelectionToModel() +{ + // Map QListWidget selection to model row + int row = -1; + const auto selected = ui->musicList->selectedItems(); + if (!selected.isEmpty()) { + row = ui->musicList->row(selected.front()); + } + _model->setCurrentRow(row); + syncButtonsEnabled(); +} + +void MusicPlayerDialog::syncButtonsEnabled() +{ + const bool hasSel = _model->currentRow() >= 0; + ui->playButton->setEnabled(hasSel); + ui->stopButton->setEnabled(_model->isPlaying()); + ui->nextButton->setEnabled(_model->currentRow() >= 0 && _model->currentRow() < static_cast(_model->tracks().size()) - 1); + ui->prevButton->setEnabled(_model->currentRow() > 0); +} + +// --- Slots --- + +void MusicPlayerDialog::on_musicList_itemSelectionChanged() +{ + syncSelectionToModel(); +} + +void MusicPlayerDialog::on_playButton_clicked() +{ + _model->play(); + syncButtonsEnabled(); +} + +void MusicPlayerDialog::on_stopButton_clicked() +{ + _model->stop(); + syncButtonsEnabled(); +} + +void MusicPlayerDialog::on_nextButton_clicked() +{ + // Mirror original: only restart playback if already playing + const bool wasPlaying = _model->isPlaying(); + if (_model->selectNext() && wasPlaying) { + _model->play(); + } + // reflect selection in the list + util::SignalBlockers block(this); + ui->musicList->setCurrentRow(_model->currentRow()); + syncButtonsEnabled(); +} + +void MusicPlayerDialog::on_prevButton_clicked() +{ + const bool wasPlaying = _model->isPlaying(); + if (_model->selectPrev() && wasPlaying) { + _model->play(); + } + util::SignalBlockers block(this); + ui->musicList->setCurrentRow(_model->currentRow()); + syncButtonsEnabled(); +} + +void MusicPlayerDialog::on_autoplayCheck_toggled(bool on) +{ + _model->setAutoplay(on); +} + +void MusicPlayerDialog::on_musicTblButton_clicked() +{ + auto dialog = new MusicTBLViewer(this, _viewport); + dialog->show(); +} + +void MusicPlayerDialog::onTick() +{ + const bool wasPlaying = _model->isPlaying(); + _model->tick(); + if (wasPlaying != _model->isPlaying()) { + syncButtonsEnabled(); + } + // If autoplay advanced selection, reflect it in the list + if (ui->musicList->currentRow() != _model->currentRow()) { + util::SignalBlockers block(this); + ui->musicList->setCurrentRow(_model->currentRow()); + syncButtonsEnabled(); + } +} + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/MusicPlayerDialog.h b/qtfred/src/ui/dialogs/MusicPlayerDialog.h new file mode 100644 index 00000000000..8ba24bf0438 --- /dev/null +++ b/qtfred/src/ui/dialogs/MusicPlayerDialog.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +#include +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class MusicPlayerDialog; +} + +class MusicPlayerDialog final : public QDialog { + Q_OBJECT + public: + MusicPlayerDialog(FredView* parent, EditorViewport* viewport); + ~MusicPlayerDialog() override; + + void accept() override; // not used + void reject() override; // ensure we stop playback on close + + protected: + void closeEvent(QCloseEvent* /*e*/) override; + + private slots: + void on_musicList_itemSelectionChanged(); + void on_playButton_clicked(); + void on_stopButton_clicked(); + void on_nextButton_clicked(); + void on_prevButton_clicked(); + void on_autoplayCheck_toggled(bool on); + void on_musicTblButton_clicked(); + + // timer + void onTick(); + + private: // NOLINT(readability-redundant-access-specifiers) + void populateList(); + void syncSelectionToModel(); + void syncButtonsEnabled(); + + EditorViewport* _viewport = nullptr; + std::unique_ptr ui; + std::unique_ptr _model; + QTimer _timer; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/MusicTBLViewer.cpp b/qtfred/src/ui/dialogs/MusicTBLViewer.cpp new file mode 100644 index 00000000000..a17f783bcee --- /dev/null +++ b/qtfred/src/ui/dialogs/MusicTBLViewer.cpp @@ -0,0 +1,35 @@ +#include "MusicTBLViewer.h" + +#include "ui_ShipTBLViewer.h" + +#include + +#include + +namespace fso::fred::dialogs { +MusicTBLViewer::MusicTBLViewer(QWidget* parent, EditorViewport* viewport) + : QDialog(parent), ui(new Ui::ShipTBLViewer()), _model(new MusicTBLViewerModel(this, viewport)), + _viewport(viewport) +{ + + ui->setupUi(this); + this->setWindowTitle("Weapon TBL Data"); + connect(_model.get(), &AbstractDialogModel::modelChanged, this, &MusicTBLViewer::updateUi); + + updateUi(); + + // Resize the dialog to the minimum size + resize(QDialog::sizeHint()); +} + +MusicTBLViewer::~MusicTBLViewer() = default; +void MusicTBLViewer::closeEvent(QCloseEvent* event) +{ + QDialog::closeEvent(event); +} +void MusicTBLViewer::updateUi() +{ + util::SignalBlockers blockers(this); + ui->TBLData->setPlainText(_model->getText().c_str()); +} +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/MusicTBLViewer.h b/qtfred/src/ui/dialogs/MusicTBLViewer.h new file mode 100644 index 00000000000..a0c124d8e7f --- /dev/null +++ b/qtfred/src/ui/dialogs/MusicTBLViewer.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class ShipTBLViewer; +} +class MusicTBLViewer : public QDialog { + Q_OBJECT + + public: + explicit MusicTBLViewer(QWidget* parent, EditorViewport* viewport); + ~MusicTBLViewer() override; + + protected: + void closeEvent(QCloseEvent*) override; + + private: + std::unique_ptr ui; + std::unique_ptr _model; + EditorViewport* _viewport; + + void updateUi(); +}; +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index d927e9acdfd..71e95e6a198 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -242,6 +242,7 @@ + @@ -1540,6 +1541,11 @@ Shift+X + + + Music Player + + diff --git a/qtfred/ui/MusicPlayerDialog.ui b/qtfred/ui/MusicPlayerDialog.ui new file mode 100644 index 00000000000..84f079f51df --- /dev/null +++ b/qtfred/ui/MusicPlayerDialog.ui @@ -0,0 +1,114 @@ + + + fso::fred::dialogs::MusicPlayerDialog + + + + 0 + 0 + 286 + 333 + + + + Music Player + + + + + + Music Files + + + + + + + + + + + + + + + :/images/prev.png + + + + + + + + + + + + :/images/play.png + + + + + + + + + + + + :/images/next.png + + + + + + + + + + + + :/images/stop.png + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Autoplay + + + + + + + + + Music.tbl + + + + + + + + + + + + + From 1a1d3f040ca3da58bc0b80557753a53a7546cebf Mon Sep 17 00:00:00 2001 From: Asteroth Date: Mon, 25 Aug 2025 13:17:52 -0400 Subject: [PATCH 418/466] fix interpolate matrices (#6996) --- code/math/vecmat.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/math/vecmat.cpp b/code/math/vecmat.cpp index 46261d851bc..d356c2e00ce 100644 --- a/code/math/vecmat.cpp +++ b/code/math/vecmat.cpp @@ -2988,7 +2988,7 @@ void vm_interpolate_matrices(matrix* out_orient, const matrix* curr_orient, cons matrix rot_matrix; vm_quaternion_rotate(&rot_matrix, t * theta, &rot_axis); // get the matrix that rotates current to our interpolated matrix - vm_matrix_x_matrix(out_orient, &rot_matrix, curr_orient); // do the final rotation + vm_matrix_x_matrix(out_orient, curr_orient, &rot_matrix); // do the final rotation } From a6f9337d5a008b537c6af3f1b68cf62ae5b640b7 Mon Sep 17 00:00:00 2001 From: Kestrellius <63537900+Kestrellius@users.noreply.github.com> Date: Mon, 25 Aug 2025 19:48:13 -0700 Subject: [PATCH 419/466] add check (#6997) --- code/object/object.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/object/object.cpp b/code/object/object.cpp index c5aea41ff9a..6d88ba2aa44 100644 --- a/code/object/object.cpp +++ b/code/object/object.cpp @@ -1305,7 +1305,7 @@ void obj_move_all_post(object *objp, float frametime) weapon_process_post( objp, frametime ); // Cast light - if ( Detail.lighting > 3 ) { + if ( Deferred_lighting && Detail.lighting > 3 ) { // Weapons cast light int group_id = Weapons[objp->instance].group_id; From d2f842494dad7a741130b1e1ae2f7eff7970ff0b Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Mon, 25 Aug 2025 21:00:05 -0700 Subject: [PATCH 420/466] setup --- code/graphics/opengl/gropengldeferred.cpp | 9 +++++++++ code/lighting/lighting_profiles.cpp | 7 +++++++ code/lighting/lighting_profiles.h | 1 + 3 files changed, 17 insertions(+) diff --git a/code/graphics/opengl/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp index c571d20a7ee..ccdcc1ae864 100644 --- a/code/graphics/opengl/gropengldeferred.cpp +++ b/code/graphics/opengl/gropengldeferred.cpp @@ -384,6 +384,10 @@ void gr_opengl_deferred_lighting_finish() ? lp->cockpit_light_radius_modifier.handle(MAX(l.rada, l.radb)) : MAX(l.rada, l.radb); light_data->lightRadius = rad; + float intensity = (Lighting_mode == lighting_mode::COCKPIT) + ? lp->cockpit_light_intensity_modifier.handle(intensity) + : intensity; + light_data->lightIntensity = intensity; // A small padding factor is added to guard against potentially clipping the edges of the light with facets // of the volume mesh. light_data->scale.xyz.x = rad * 1.05f; @@ -396,6 +400,11 @@ void gr_opengl_deferred_lighting_finish() (Lighting_mode == lighting_mode::COCKPIT) ? lp->cockpit_light_radius_modifier.handle(l.radb) : l.radb; light_data->lightRadius = rad; + float intensity = + (Lighting_mode == lighting_mode::COCKPIT) ? lp->cockpit_light_intensity_modifier.handle(l.intensity) : l.intensity; + + light_data->lightIntensity = intensity; + light_data->lightType = LT_TUBE; vec3d a; diff --git a/code/lighting/lighting_profiles.cpp b/code/lighting/lighting_profiles.cpp index 4a3b27925a4..6c0c6ab1ef8 100644 --- a/code/lighting/lighting_profiles.cpp +++ b/code/lighting/lighting_profiles.cpp @@ -385,6 +385,11 @@ void profile::parse(const char* filename, const SCP_string& profile_name, const profile_name, &cockpit_light_radius_modifier); + parsed |= adjustment::parse(filename, + "$Cockpit light intensity modifier:", + profile_name, + &cockpit_light_intensity_modifier); + if (!parsed) { stuff_string(buffer, F_RAW); Warning(LOCATION, "Unhandled line in lighting profile\n\t%s", buffer.c_str()); @@ -445,6 +450,8 @@ void profile::reset() cockpit_light_radius_modifier.reset(); cockpit_light_radius_modifier.set_multiplier(1.0f); + cockpit_light_intensity_modifier.reset(); + cockpit_light_intensity_modifier.set_multiplier(1.0f); } profile& profile::operator=(const profile& rhs) = default; diff --git a/code/lighting/lighting_profiles.h b/code/lighting/lighting_profiles.h index 9ffb45f55dc..9ad7a224302 100644 --- a/code/lighting/lighting_profiles.h +++ b/code/lighting/lighting_profiles.h @@ -72,6 +72,7 @@ class profile { // Strictly speaking this should be handled by postproc but we need something for the non-postproc people. adjustment overall_brightness; adjustment cockpit_light_radius_modifier; + adjustment cockpit_light_intensity_modifier; void reset(); profile& operator=(const profile& rhs); From d279e477b34d77020d5d2d578903ba1f6b10f048 Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Mon, 25 Aug 2025 21:33:00 -0700 Subject: [PATCH 421/466] bugfixing --- code/graphics/opengl/gropengldeferred.cpp | 26 ++++++++++------------- code/graphics/opengl/gropengldeferred.h | 5 ++++- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/code/graphics/opengl/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp index ccdcc1ae864..6c3ef67ca49 100644 --- a/code/graphics/opengl/gropengldeferred.cpp +++ b/code/graphics/opengl/gropengldeferred.cpp @@ -190,16 +190,19 @@ static bool override_fog = false; graphics::deferred_light_data* // common conversion operations to translate a game light data structure into a render-ready light uniform. -prepare_light_uniforms(light& l, graphics::util::UniformAligner& uniformAligner) +prepare_light_uniforms(light& l, graphics::util::UniformAligner& uniformAligner, const ltp::profile* lp) { graphics::deferred_light_data* light_data = uniformAligner.addTypedElement(); light_data->lightType = static_cast(l.type); + float intensity = + (Lighting_mode == lighting_mode::COCKPIT) ? lp->cockpit_light_intensity_modifier.handle(l.intensity) : l.intensity; + vec3d diffuse; - diffuse.xyz.x = l.r * l.intensity; - diffuse.xyz.y = l.g * l.intensity; - diffuse.xyz.z = l.b * l.intensity; + diffuse.xyz.x = l.r * intensity; + diffuse.xyz.y = l.g * intensity; + diffuse.xyz.z = l.b * intensity; light_data->diffuseLightColor = diffuse; @@ -342,7 +345,7 @@ void gr_opengl_deferred_lighting_finish() bool first_directional = true; for (auto& l : full_frame_lights) { - auto light_data = prepare_light_uniforms(l, light_uniform_aligner); + auto light_data = prepare_light_uniforms(l, light_uniform_aligner, lp); if (l.type == Light_Type::Directional ) { if (Shadow_quality != ShadowQuality::Disabled) { @@ -372,7 +375,7 @@ void gr_opengl_deferred_lighting_finish() } } for (auto& l : sphere_lights) { - auto light_data = prepare_light_uniforms(l, light_uniform_aligner); + auto light_data = prepare_light_uniforms(l, light_uniform_aligner, lp); if (l.type == Light_Type::Cone) { light_data->dualCone = (l.flags & LF_DUAL_CONE) ? 1.0f : 0.0f; @@ -384,10 +387,7 @@ void gr_opengl_deferred_lighting_finish() ? lp->cockpit_light_radius_modifier.handle(MAX(l.rada, l.radb)) : MAX(l.rada, l.radb); light_data->lightRadius = rad; - float intensity = (Lighting_mode == lighting_mode::COCKPIT) - ? lp->cockpit_light_intensity_modifier.handle(intensity) - : intensity; - light_data->lightIntensity = intensity; + // A small padding factor is added to guard against potentially clipping the edges of the light with facets // of the volume mesh. light_data->scale.xyz.x = rad * 1.05f; @@ -395,15 +395,11 @@ void gr_opengl_deferred_lighting_finish() light_data->scale.xyz.z = rad * 1.05f; } for (auto& l : cylinder_lights) { - auto light_data = prepare_light_uniforms(l, light_uniform_aligner); + auto light_data = prepare_light_uniforms(l, light_uniform_aligner, lp); float rad = (Lighting_mode == lighting_mode::COCKPIT) ? lp->cockpit_light_radius_modifier.handle(l.radb) : l.radb; light_data->lightRadius = rad; - float intensity = - (Lighting_mode == lighting_mode::COCKPIT) ? lp->cockpit_light_intensity_modifier.handle(l.intensity) : l.intensity; - - light_data->lightIntensity = intensity; light_data->lightType = LT_TUBE; diff --git a/code/graphics/opengl/gropengldeferred.h b/code/graphics/opengl/gropengldeferred.h index cce23bf2554..31d6b60e59e 100644 --- a/code/graphics/opengl/gropengldeferred.h +++ b/code/graphics/opengl/gropengldeferred.h @@ -4,6 +4,9 @@ #include "graphics/util/UniformAligner.h" #include "graphics/util/uniform_structs.h" #include "lighting/lighting.h" +#include "lighting/lighting_profiles.h" +namespace ltp = lighting_profiles; +using namespace ltp; void gr_opengl_deferred_init(); @@ -12,7 +15,7 @@ void gr_opengl_deferred_lighting_begin(bool clearNonColorBufs = false); void gr_opengl_deferred_lighting_msaa(); void gr_opengl_deferred_lighting_end(); void gr_opengl_deferred_lighting_finish(); -graphics::deferred_light_data* prepare_light_uniforms(light& l, graphics::util::UniformAligner& uniformAligner); +graphics::deferred_light_data* prepare_light_uniforms(light& l, graphics::util::UniformAligner& uniformAligner, ltp::profile lp); void gr_opengl_deferred_light_sphere_init(int rings, int segments); void gr_opengl_deferred_light_cylinder_init(int segments); From 7918797eeceb99e655e7b5e231fbb51735ea38f9 Mon Sep 17 00:00:00 2001 From: Kestrellius <902X@comcast.net> Date: Tue, 26 Aug 2025 00:37:30 -0700 Subject: [PATCH 422/466] more bugfixing --- code/object/object.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/object/object.cpp b/code/object/object.cpp index 6d88ba2aa44..a60cca2c10c 100644 --- a/code/object/object.cpp +++ b/code/object/object.cpp @@ -1305,7 +1305,7 @@ void obj_move_all_post(object *objp, float frametime) weapon_process_post( objp, frametime ); // Cast light - if ( Deferred_lighting && Detail.lighting > 3 ) { + if ( light_deferred_enabled() && Detail.lighting > 3 ) { // Weapons cast light int group_id = Weapons[objp->instance].group_id; From 5f899634c26b5e624d656f93cfaade5b5554bb9b Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Tue, 26 Aug 2025 17:33:40 -0400 Subject: [PATCH 423/466] Fix new deferred check (#7001) --- code/object/object.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/object/object.cpp b/code/object/object.cpp index 6d88ba2aa44..0fa507ff8e8 100644 --- a/code/object/object.cpp +++ b/code/object/object.cpp @@ -1305,7 +1305,7 @@ void obj_move_all_post(object *objp, float frametime) weapon_process_post( objp, frametime ); // Cast light - if ( Deferred_lighting && Detail.lighting > 3 ) { + if ( (Detail.lighting > 3) && light_deferred_enabled() ) { // Weapons cast light int group_id = Weapons[objp->instance].group_id; From e9311ec828df2f6c6e1d47cc7bb8dbc1f0d1e635 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Wed, 27 Aug 2025 10:58:04 -0500 Subject: [PATCH 424/466] use correct array throughout --- qtfred/src/mission/Editor.cpp | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index 4832d2c9a81..f39cc6852ed 100644 --- a/qtfred/src/mission/Editor.cpp +++ b/qtfred/src/mission/Editor.cpp @@ -332,11 +332,7 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { } } } - } - - int used_pool[MAX_WEAPON_TYPES] = {}; - for (auto& pool : used_pool) - pool = 0; + } for (i = 0; i < Num_teams; i++) { generate_team_weaponry_usage_list(i, _weapon_usage[i]); @@ -345,7 +341,7 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { if ((!strlen(Team_data[i].weaponry_pool_variable[j])) && (!strlen(Team_data[i].weaponry_amount_variable[j]))) { // convert weaponry_pool to be extras available beyond the current ships weapons - Team_data[i].weaponry_count[j] -= used_pool[Team_data[i].weaponry_pool[j]]; + Team_data[i].weaponry_count[j] -= _weapon_usage[i][Team_data[i].weaponry_pool[j]]; if (Team_data[i].weaponry_count[j] < 0) { Team_data[i].weaponry_count[j] = 0; } @@ -364,7 +360,7 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { // add the weapon as a new entry Team_data[i].weaponry_pool[Team_data[i].num_weapon_choices] = j; - Team_data[i].weaponry_count[Team_data[i].num_weapon_choices] = used_pool[j]; + Team_data[i].weaponry_count[Team_data[i].num_weapon_choices] = _weapon_usage[i][j]; strcpy_s(Team_data[i].weaponry_amount_variable[Team_data[i].num_weapon_choices], ""); strcpy_s(Team_data[i].weaponry_pool_variable[Team_data[i].num_weapon_choices++], ""); } From 1310c38642df04604ae3db405424b74dd0914909 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:38:43 +0100 Subject: [PATCH 425/466] QTFRED Ship editor Modernisation 2 Part 1 (#6989) * Switch to using signals/slots rather than connect * Adds coloured persona box Adds coloured persona box/ fixes persona loading, and reduces amount of data reset on model change * Update To new flags widget Updates To new flags widget this reqires moving several non flag items to other dialogs, which also lead to adding a new feature that lets a custom guardian threshold be set in editor * Switch to size_t * Make Mission Spec work with changes to Flaglist * Remove Duplicate member * Clang Appeasement * Make static --- code/mission/missionparse.cpp | 92 +- code/mission/missionparse.h | 7 + fred2/shipflagsdlg.cpp | 2 +- qtfred/source_groups.cmake | 2 + .../ShipEditor/ShipEditorDialogModel.cpp | 21 +- .../ShipEditor/ShipEditorDialogModel.h | 5 + .../ShipEditor/ShipFlagsDialogModel.cpp | 1401 +++-------------- .../dialogs/ShipEditor/ShipFlagsDialogModel.h | 201 +-- .../ShipInitialStatusDialogModel.cpp | 16 +- .../ShipEditor/ShipInitialStatusDialogModel.h | 6 +- qtfred/src/mission/management.h | 6 + qtfred/src/mission/missionsave.cpp | 11 + qtfred/src/ui/dialogs/MissionSpecDialog.cpp | 2 +- .../dialogs/ShipEditor/ShipEditorDialog.cpp | 687 ++++---- .../ui/dialogs/ShipEditor/ShipEditorDialog.h | 102 +- .../ui/dialogs/ShipEditor/ShipFlagsDialog.cpp | 466 +----- .../ui/dialogs/ShipEditor/ShipFlagsDialog.h | 63 +- .../ShipEditor/ShipInitialStatusDialog.cpp | 5 + .../ShipEditor/ShipInitialStatusDialog.h | 4 +- qtfred/src/ui/widgets/FlagList.cpp | 18 +- qtfred/src/ui/widgets/FlagList.h | 12 +- .../src/ui/widgets/PersonaColorComboBox.cpp | 41 + qtfred/src/ui/widgets/PersonaColorComboBox.h | 15 + qtfred/ui/ShipEditorDialog.ui | 247 +-- qtfred/ui/ShipFlagsDialog.ui | 751 ++------- qtfred/ui/ShipInitialStatus.ui | 78 +- 26 files changed, 1112 insertions(+), 3149 deletions(-) create mode 100644 qtfred/src/ui/widgets/PersonaColorComboBox.cpp create mode 100644 qtfred/src/ui/widgets/PersonaColorComboBox.h diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 101eab7aec1..e8b93f700f1 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -267,6 +267,85 @@ const char *Old_game_types[OLD_MAX_GAME_TYPES] = { "Training mission" }; +flag_def_list_new Parse_ship_flags[] = { + {"cargo-known", Ship::Ship_Flags::Cargo_revealed, true, false}, + {"ignore-count", Ship::Ship_Flags::Ignore_count, true, false}, + {"reinforcement", Ship::Ship_Flags::Reinforcement, true, false}, + {"escort", Ship::Ship_Flags::Escort, true, false}, + {"no-arrival-music", Ship::Ship_Flags::No_arrival_music, true, false}, + {"no-arrival-warp", Ship::Ship_Flags::No_arrival_warp, true, false}, + {"no-departure-warp", Ship::Ship_Flags::No_departure_warp, true, false}, + {"hidden-from-sensors", Ship::Ship_Flags::Hidden_from_sensors, true, false}, + {"scannable", Ship::Ship_Flags::Scannable, true, false}, + {"red-alert-carry", Ship::Ship_Flags::Red_alert_store_status, true, false}, + {"vaporize", Ship::Ship_Flags::Vaporize, true, false}, + {"stealth", Ship::Ship_Flags::Stealth, true, false}, + {"friendly-stealth-invisible", Ship::Ship_Flags::Friendly_stealth_invis, true, false}, + {"don't-collide-invisible", Ship::Ship_Flags::Dont_collide_invis, true, false}, + {"primitive-sensors", Ship::Ship_Flags::Primitive_sensors, true, false}, + {"no-subspace-drive", Ship::Ship_Flags::No_subspace_drive, true, false}, + {"nav-carry-status", Ship::Ship_Flags::Navpoint_carry, true, false}, + {"affected-by-gravity", Ship::Ship_Flags::Affected_by_gravity, true, false}, + {"toggle-subsystem-scanning", Ship::Ship_Flags::Toggle_subsystem_scanning, true, false}, + {"no-builtin-messages", Ship::Ship_Flags::No_builtin_messages, true, false}, + {"primaries-locked", Ship::Ship_Flags::Primaries_locked, true, false}, + {"secondaries-locked", Ship::Ship_Flags::Secondaries_locked, true, false}, + {"no-death-scream", Ship::Ship_Flags::No_death_scream, true, false}, + {"always-death-scream", Ship::Ship_Flags::Always_death_scream, true, false}, + {"nav-needslink", Ship::Ship_Flags::Navpoint_needslink, true, false}, + {"hide-ship-name", Ship::Ship_Flags::Hide_ship_name, true, false}, + {"set-class-dynamically", Ship::Ship_Flags::Set_class_dynamically, true, false}, + {"lock-all-turrets", Ship::Ship_Flags::Lock_all_turrets_initially, true, false}, + {"afterburners-locked", Ship::Ship_Flags::Afterburner_locked, true, false}, + {"no-ets", Ship::Ship_Flags::No_ets, true, false}, + {"cloaked", Ship::Ship_Flags::Cloaked, true, false}, + {"ship-locked", Ship::Ship_Flags::Ship_locked, true, false}, + {"weapons-locked", Ship::Ship_Flags::Weapons_locked, true, false}, + {"scramble-messages", Ship::Ship_Flags::Scramble_messages, true, false}, + {"no-disabled-self-destruct", Ship::Ship_Flags::No_disabled_self_destruct, true, false}, + {"hide-in-mission-log", Ship::Ship_Flags::Hide_mission_log, true, false}, + {"same-arrival-warp-when-docked", Ship::Ship_Flags::Same_arrival_warp_when_docked, true, false}, + {"same-departure-warp-when-docked", Ship::Ship_Flags::Same_departure_warp_when_docked, true, false}, + {"fail-sound-locked-primary", Ship::Ship_Flags::Fail_sound_locked_primary, true, false}, + {"fail-sound-locked-secondary", Ship::Ship_Flags::Fail_sound_locked_secondary, true, false}, + {"aspect-immune", Ship::Ship_Flags::Aspect_immune, true, false}, + {"cannot-perform-scan", Ship::Ship_Flags::Cannot_perform_scan, true, false}, + {"no-targeting-limits", Ship::Ship_Flags::No_targeting_limits, true, false}, + {"force-shields-on", Ship::Ship_Flags::Force_shields_on, true, false}, + {"Destroy before Mission", Ship::Ship_Flags::Kill_before_mission,true, false}, //Not Printed to misson so can use descriptive name +} +; + +const size_t Num_Parse_ship_flags = sizeof(Parse_ship_flags) / sizeof(flag_def_list_new); + +flag_def_list_new Parse_ship_ai_flags[] = { + {"kamikaze", AI::AI_Flags::Kamikaze, true, false}, + {"no-dynamic", AI::AI_Flags::No_dynamic, true, false}, + +}; + +const size_t Num_Parse_ship_ai_flags = sizeof(Parse_ship_ai_flags) / sizeof(flag_def_list_new); + +flag_def_list_new Parse_ship_object_flags[] = { + {"protect-ship", Object::Object_Flags::Protected, true, false}, + {"no-shields", Object::Object_Flags::No_shields, true, false}, + {"player-start", Object::Object_Flags::Player_ship, true, false}, + {"invulnerable", Object::Object_Flags::Invulnerable, true, false}, + {"beam-protect-ship", Object::Object_Flags::Beam_protected, true, false}, + {"flak-protect-ship", Object::Object_Flags::Flak_protected, true, false}, + {"laser-protect-ship", Object::Object_Flags::Laser_protected, true, false}, + {"missile-protect-ship", Object::Object_Flags::Missile_protected, true, false}, + {"special-warp", Object::Object_Flags::Special_warpin, true, false}, + {"targetable-as-bomb", Object::Object_Flags::Targetable_as_bomb, true, false}, + {"don't-change-position", Object::Object_Flags::Dont_change_position, true, false}, + {"don't-change-orientation", Object::Object_Flags::Dont_change_orientation, true, false}, + {"no_collide", Object::Object_Flags::Collides, true, false}, + {"ai-attackable-if-no-collide", Object::Object_Flags::Attackable_if_no_collide, true, false}, + +}; +const size_t Num_Parse_ship_object_flags = + sizeof(Parse_ship_object_flags) / sizeof(flag_def_list_new); + // These are a little different than the object flags as they aren't used in traditional flag sexps or parsed flag lists // Instead, this list is used to popuplate QtFRED's mission specs flag checkboxes. As such the names can be more descriptive than other flag def lists // NOTE: Inactive flags and special flags are not added to the UI flag list. It is assumed that special flags exist in some other UI form @@ -2136,6 +2215,7 @@ int parse_create_object_sub(p_object *p_objp, bool standalone_ship) shipp->team = p_objp->team; shipp->display_name = p_objp->display_name; shipp->escort_priority = p_objp->escort_priority; + shipp->ship_guardian_threshold = p_objp->ship_guardian_threshold; shipp->use_special_explosion = p_objp->use_special_explosion; shipp->special_exp_damage = p_objp->special_exp_damage; shipp->special_exp_blast = p_objp->special_exp_blast; @@ -2892,9 +2972,6 @@ void resolve_parse_flags(object *objp, flagset &par if (parse_flags[Mission::Parse_Object_Flags::OF_Missile_protected]) objp->flags.set(Object::Object_Flags::Missile_protected); - if (parse_flags[Mission::Parse_Object_Flags::SF_Guardian]) - shipp->ship_guardian_threshold = SHIP_GUARDIAN_THRESHOLD_DEFAULT; - if (parse_flags[Mission::Parse_Object_Flags::SF_Vaporize]) shipp->flags.set(Ship::Ship_Flags::Vaporize); @@ -3460,6 +3537,15 @@ int parse_object(mission *pm, int /*flag*/, p_object *p_objp) stuff_int(&p_objp->escort_priority); } + if (optional_string("+Guardian Threshold:")) { + + stuff_int(&p_objp->ship_guardian_threshold); + } else { + if (p_objp->flags[Mission::Parse_Object_Flags::SF_Guardian]) { + p_objp->ship_guardian_threshold = SHIP_GUARDIAN_THRESHOLD_DEFAULT; + } + } + if (p_objp->flags[Mission::Parse_Object_Flags::OF_Player_start]) { p_objp->flags.set(Mission::Parse_Object_Flags::SF_Cargo_known); // make cargo known for players diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index 1b2333c6d58..a564675de84 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -313,6 +313,12 @@ extern flag_def_list_new Parse_mission_flags[]; extern parse_object_flag_description Parse_mission_flag_descriptions[]; extern const size_t Num_parse_mission_flags; extern char *Object_flags[]; +extern flag_def_list_new Parse_ship_flags[]; +extern const size_t Num_Parse_ship_flags; +extern flag_def_list_new Parse_ship_ai_flags[]; +extern const size_t Num_Parse_ship_ai_flags; +extern flag_def_list_new Parse_ship_object_flags[]; +extern const size_t Num_Parse_ship_object_flags; extern flag_def_list_new Parse_object_flags[]; extern parse_object_flag_description Parse_object_flag_descriptions[]; extern const size_t Num_parse_object_flags; @@ -455,6 +461,7 @@ class p_object flagset flags; // mission savable flags int escort_priority = 0; // priority in escort list + int ship_guardian_threshold; int ai_class = -1; int hotkey = -1; // hotkey number (between 0 and 9) -1 means no hotkey int score = 0; diff --git a/fred2/shipflagsdlg.cpp b/fred2/shipflagsdlg.cpp index 3bd215b0d9a..cddd3735945 100644 --- a/fred2/shipflagsdlg.cpp +++ b/fred2/shipflagsdlg.cpp @@ -296,7 +296,7 @@ BOOL ship_flags_dlg::OnInitDialog() m_escort_value.init(shipp->escort_priority); if(The_mission.game_type & MISSION_TYPE_MULTI) { - m_respawn_priority.init(shipp->escort_priority); + m_respawn_priority.init(shipp->respawn_priority); } for (j=0; j current_orders; pship_count = 0; // a total count of the player ships not marked @@ -276,8 +277,9 @@ namespace fso { _m_persona = Ships[i].persona_index; _m_alt_name = Fred_alt_names[base_ship]; _m_callsign = Fred_callsigns[base_ship]; - - + if (The_mission.game_type & MISSION_TYPE_MULTI) { + respawn_priority = Ships[i].respawn_priority; + } // we use final_death_time member of ship structure for holding the amount of time before a mission // to destroy this ship wing = Ships[i].wingnum; @@ -336,7 +338,6 @@ namespace fso { _m_player_ship = pship; - _m_persona++; if (_m_persona > 0) { int persona_index = 0; for (int i = 0; i < _m_persona; i++) { @@ -654,7 +655,10 @@ namespace fso { ship_alt_name_close(ship); ship_callsign_close(ship); - + Ships[ship].respawn_priority = 0; + if (The_mission.game_type & MISSION_TYPE_MULTI) { + Ships[ship].respawn_priority = respawn_priority; + } if ((Ships[ship].ship_info_index != _m_ship_class) && (_m_ship_class != -1)) { change_ship_type(ship, _m_ship_class); } @@ -1048,6 +1052,15 @@ namespace fso { return _m_player_ship; } + void ShipEditorDialogModel::setRespawn(const int value) { + modify(respawn_priority, value); + } + + int ShipEditorDialogModel::getRespawn() const + { + return respawn_priority; + } + void ShipEditorDialogModel::setArrivalLocationIndex(const int value) { modify(_m_arrival_location, value); diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h index 2c6c252c06d..f521f28ebe5 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.h @@ -71,6 +71,8 @@ class ShipEditorDialogModel : public AbstractDialogModel { bool texenable = true; + int respawn_priority; + public: ShipEditorDialogModel(QObject* parent, EditorViewport* viewport); void initializeData(); @@ -122,6 +124,9 @@ class ShipEditorDialogModel : public AbstractDialogModel { void setPlayer(const bool); bool getPlayer() const; + void setRespawn(const int); + int getRespawn() const; + void setArrivalLocationIndex(const int); int getArrivalLocationIndex() const; void setArrivalLocation(const ArrivalLocation); diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp index c705f223791..6816b8737f9 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.cpp @@ -3,1129 +3,164 @@ #include "ShipFlagsDialogModel.h" -#include "ui/dialogs/ShipEditor/ShipFlagsDialog.h" - -#include - -#include - -namespace fso { -namespace fred { -namespace dialogs { -int ShipFlagsDialogModel::tristate_set(int val, int cur_state) -{ - if (val) { - if (!cur_state) { - return Qt::PartiallyChecked; - } - } else { - if (cur_state) { - return Qt::PartiallyChecked; - } - } - if (cur_state == 1) { - - return Qt::Checked; - } else { - return Qt::Unchecked; - } -} -void ShipFlagsDialogModel::update_ship(const int shipnum) -{ - ship* shipp = &Ships[shipnum]; - object* objp = &Objects[shipp->objnum]; - - if (m_reinforcement != Qt::PartiallyChecked) { - // Check if we're trying to add more and we've got too many. - if ((Num_reinforcements >= MAX_REINFORCEMENTS) && (m_reinforcement == Qt::Checked)) { - SCP_string error_message; - sprintf(error_message, - "Too many reinforcements; could not add ship '%s' to reinforcement list!", - shipp->ship_name); - _viewport->dialogProvider->showButtonDialog(DialogType::Error, - "Flag Error", - error_message, - {DialogButton::Ok}); - } - // Otherwise, just update as normal. - else { - _editor->set_reinforcement(shipp->ship_name, m_reinforcement); - } - } - - switch (m_cargo_known) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Cargo_revealed])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Cargo_revealed); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Cargo_revealed]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Cargo_revealed); - break; - } - - // update the flags for IGNORE_COUNT and PROTECT_SHIP - switch (m_protect_ship) { - case Qt::Checked: - if (!(objp->flags[Object::Object_Flags::Protected])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Protected); - break; - case Qt::Unchecked: - if (objp->flags[Object::Object_Flags::Protected]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Protected); - break; - } - - switch (m_beam_protect_ship) { - case Qt::Checked: - if (!(objp->flags[Object::Object_Flags::Beam_protected])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Beam_protected); - break; - case Qt::Unchecked: - if (objp->flags[Object::Object_Flags::Beam_protected]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Beam_protected); - break; - } - - switch (m_flak_protect_ship) { - case Qt::Checked: - if (!(objp->flags[Object::Object_Flags::Flak_protected])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Flak_protected); - break; - case Qt::Unchecked: - if (objp->flags[Object::Object_Flags::Flak_protected]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Flak_protected); - break; - } - switch (m_laser_protect_ship) { - case Qt::Checked: - if (!(objp->flags[Object::Object_Flags::Laser_protected])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Laser_protected); - break; - case Qt::Unchecked: - if (objp->flags[Object::Object_Flags::Laser_protected]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Laser_protected); - break; - } - switch (m_missile_protect_ship) { - case Qt::Checked: - if (!(objp->flags[Object::Object_Flags::Missile_protected])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Missile_protected); - break; - case Qt::Unchecked: - if (objp->flags[Object::Object_Flags::Missile_protected]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Missile_protected); - break; - } - - switch (m_invulnerable) { - case Qt::Checked: - if (!(objp->flags[Object::Object_Flags::Invulnerable])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Invulnerable); - break; - case Qt::Unchecked: - if (objp->flags[Object::Object_Flags::Invulnerable]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Invulnerable); - break; - } - - switch (m_targetable_as_bomb) { - case Qt::Checked: - if (!(objp->flags[Object::Object_Flags::Targetable_as_bomb])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Targetable_as_bomb); - break; - case Qt::Unchecked: - if (objp->flags[Object::Object_Flags::Targetable_as_bomb]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Targetable_as_bomb); - break; - } - - switch (m_dont_change_position) { - case Qt::Checked: - if (!(objp->flags[Object::Object_Flags::Dont_change_position])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Dont_change_position); - break; - case Qt::Unchecked: - if (objp->flags[Object::Object_Flags::Dont_change_position]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Dont_change_position); - break; - } - - switch (m_dont_change_orientation) { - case Qt::Checked: - if (!(objp->flags[Object::Object_Flags::Dont_change_orientation])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Dont_change_orientation); - break; - case Qt::Unchecked: - if (objp->flags[Object::Object_Flags::Dont_change_orientation]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Dont_change_orientation); - break; - } - - switch (m_hidden) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Hidden_from_sensors])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Hidden_from_sensors); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Hidden_from_sensors]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Hidden_from_sensors); - break; - } - - switch (m_primitive_sensors) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Primitive_sensors])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Primitive_sensors); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Primitive_sensors]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Primitive_sensors); - break; - } - - switch (m_no_subspace_drive) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::No_subspace_drive])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::No_subspace_drive); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::No_subspace_drive]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::No_subspace_drive); - break; - } - - switch (m_affected_by_gravity) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Affected_by_gravity])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Affected_by_gravity); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Affected_by_gravity]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Affected_by_gravity); - break; - } - - switch (m_toggle_subsystem_scanning) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Toggle_subsystem_scanning])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Toggle_subsystem_scanning); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Toggle_subsystem_scanning]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Toggle_subsystem_scanning); - break; - } - switch (m_ignore_count) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Ignore_count])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Ignore_count); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Ignore_count]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Ignore_count); - break; - } - - switch (m_escort) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Escort])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Escort); - shipp->escort_priority = m_escort_value; - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Escort]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Escort); - break; - } - - // deal with updating the "destroy before the mission" stuff - switch (m_destroy) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Kill_before_mission])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Kill_before_mission); - shipp->final_death_time = m_destroy_value; - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Kill_before_mission]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Kill_before_mission); - break; - } - - switch (m_no_arrival_music) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::No_arrival_music])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::No_arrival_music); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::No_arrival_music]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::No_arrival_music); - break; - } - - switch (m_scannable) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Scannable])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Scannable); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Scannable]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Scannable); - break; - } - - switch (m_red_alert_carry) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Red_alert_store_status])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Red_alert_store_status); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Red_alert_store_status]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Red_alert_store_status); - break; - } - - switch (m_special_warpin) { - case Qt::Checked: - if (!(objp->flags[Object::Object_Flags::Special_warpin])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Special_warpin); - break; - case Qt::Unchecked: - if (objp->flags[Object::Object_Flags::Special_warpin]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Special_warpin); - break; - } - - switch (m_no_dynamic) { - case Qt::Checked: - if (!(Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::No_dynamic])) { - set_modified(); - } - Ai_info[shipp->ai_index].ai_flags.set(AI::AI_Flags::No_dynamic); - break; - case Qt::Unchecked: - if (Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::No_dynamic]) { - set_modified(); - } - Ai_info[shipp->ai_index].ai_flags.remove(AI::AI_Flags::No_dynamic); - break; - } - - switch (m_kamikaze) { - case Qt::Checked: - if (!(Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::Kamikaze])) { - set_modified(); - } - Ai_info[shipp->ai_index].ai_flags.set(AI::AI_Flags::Kamikaze); - Ai_info[shipp->ai_index].kamikaze_damage = 0; - break; - case Qt::Unchecked: - if (Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::Kamikaze]) { - set_modified(); - } - Ai_info[shipp->ai_index].ai_flags.remove(AI::AI_Flags::Kamikaze); - Ai_info[shipp->ai_index].kamikaze_damage = m_kdamage; - break; - } - - switch (m_disable_messages) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::No_builtin_messages])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::No_builtin_messages); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::No_builtin_messages]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::No_builtin_messages); - break; - } - - switch (m_set_class_dynamically) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Set_class_dynamically])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Set_class_dynamically); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Set_class_dynamically]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Set_class_dynamically); - break; - } - - switch (m_no_death_scream) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::No_death_scream])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::No_death_scream); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::No_death_scream]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::No_death_scream); - break; - } - - switch (m_always_death_scream) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Always_death_scream])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Always_death_scream); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Always_death_scream]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Always_death_scream); - break; - } - - switch (m_nav_carry) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Navpoint_needslink])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Navpoint_needslink); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Navpoint_needslink]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Navpoint_needslink); - break; - } - - switch (m_hide_ship_name) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Hide_ship_name])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Hide_ship_name); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Hide_ship_name]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Hide_ship_name); - break; - } - - switch (m_disable_ets) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::No_ets])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::No_ets); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::No_ets]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::No_ets); - break; - } - - switch (m_cloaked) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Cloaked])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Cloaked); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Cloaked]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Cloaked); - break; - } - - switch (m_guardian) { - case Qt::Checked: - if (!(shipp->ship_guardian_threshold)) { - set_modified(); - } - shipp->ship_guardian_threshold = SHIP_GUARDIAN_THRESHOLD_DEFAULT; - break; - case Qt::Unchecked: - if (shipp->ship_guardian_threshold) { - set_modified(); - } - shipp->ship_guardian_threshold = 0; - break; - } - - switch (m_vaporize) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Vaporize])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Vaporize); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Vaporize]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Vaporize); - break; - } - - switch (m_stealth) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Stealth])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Stealth); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Stealth]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Stealth); - break; - } - - switch (m_friendly_stealth_invisible) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Friendly_stealth_invis])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Friendly_stealth_invis); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Friendly_stealth_invis]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Friendly_stealth_invis); - break; - } - - switch (m_scramble_messages) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::Scramble_messages])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::Scramble_messages); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::Scramble_messages]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::Scramble_messages); - break; - } - - switch (m_no_collide) { - case Qt::Checked: - if (objp->flags[Object::Object_Flags::Collides]) { - set_modified(); - } - objp->flags.remove(Object::Object_Flags::Collides); - break; - case Qt::Unchecked: - if (!(objp->flags[Object::Object_Flags::Collides])) { - set_modified(); - } - objp->flags.set(Object::Object_Flags::Collides); - break; - } - - switch (m_no_disabled_self_destruct) { - case Qt::Checked: - if (!(shipp->flags[Ship::Ship_Flags::No_disabled_self_destruct])) { - set_modified(); - } - shipp->flags.set(Ship::Ship_Flags::No_disabled_self_destruct); - break; - case Qt::Unchecked: - if (shipp->flags[Ship::Ship_Flags::No_disabled_self_destruct]) { - set_modified(); - } - shipp->flags.remove(Ship::Ship_Flags::No_disabled_self_destruct); - break; - } - - shipp->respawn_priority = 0; - if (The_mission.game_type & MISSION_TYPE_MULTI) { - shipp->respawn_priority = m_respawn_priority; - } -} -ShipFlagsDialogModel::ShipFlagsDialogModel(QObject* parent, EditorViewport* viewport) - : AbstractDialogModel(parent, viewport) -{ - initializeData(); -} - -bool ShipFlagsDialogModel::apply() -{ - object* objp; - - objp = GET_FIRST(&obj_used_list); - while (objp != END_OF_LIST(&obj_used_list)) { - if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { - if (objp->flags[Object::Object_Flags::Marked]) { - update_ship(objp->instance); - } - } - objp = GET_NEXT(objp); - } - - return true; -} - -void ShipFlagsDialogModel::reject() {} - -void ShipFlagsDialogModel::setDestroyed(int state) -{ - modify(m_destroy, state); -} - -int ShipFlagsDialogModel::getDestroyed() const -{ - return m_destroy; -} - -void ShipFlagsDialogModel::setDestroyedSeconds(const int value) -{ - modify(m_destroy_value, value); -} - -int ShipFlagsDialogModel::getDestroyedSeconds() const -{ - return m_destroy_value; -} - -void ShipFlagsDialogModel::setScannable(const int state) -{ - modify(m_scannable, state); -} - -int ShipFlagsDialogModel::getScannable() const -{ - return m_scannable; -} - -void ShipFlagsDialogModel::setCargoKnown(const int state) -{ - modify(m_cargo_known, state); -} - -int ShipFlagsDialogModel::getCargoKnown() const -{ - return m_cargo_known; -} - -void ShipFlagsDialogModel::setSubsystemScanning(const int state) -{ - modify(m_toggle_subsystem_scanning, state); -} - -int ShipFlagsDialogModel::getSubsystemScanning() const -{ - return m_toggle_subsystem_scanning; -} - -void ShipFlagsDialogModel::setReinforcment(const int state) -{ - modify(m_reinforcement, state); -} - -int ShipFlagsDialogModel::getReinforcment() const -{ - return m_reinforcement; -} - -void ShipFlagsDialogModel::setProtectShip(const int state) -{ - modify(m_protect_ship, state); -} - -int ShipFlagsDialogModel::getProtectShip() const -{ - return m_protect_ship; -} - -void ShipFlagsDialogModel::setBeamProtect(const int state) -{ - modify(m_beam_protect_ship, state); -} - -int ShipFlagsDialogModel::getBeamProtect() const -{ - return m_beam_protect_ship; -} - -void ShipFlagsDialogModel::setFlakProtect(const int state) -{ - modify(m_flak_protect_ship, state); -} - -int ShipFlagsDialogModel::getFlakProtect() const -{ - return m_flak_protect_ship; -} - -void ShipFlagsDialogModel::setLaserProtect(const int state) -{ - modify(m_laser_protect_ship, state); -} - -int ShipFlagsDialogModel::getLaserProtect() const -{ - return m_laser_protect_ship; -} - -void ShipFlagsDialogModel::setMissileProtect(const int state) -{ - modify(m_missile_protect_ship, state); -} - -int ShipFlagsDialogModel::getMissileProtect() const -{ - return m_missile_protect_ship; -} - -void ShipFlagsDialogModel::setIgnoreForGoals(const int state) -{ - modify(m_ignore_count, state); -} - -int ShipFlagsDialogModel::getIgnoreForGoals() const -{ - return m_ignore_count; -} - -void ShipFlagsDialogModel::setEscort(const int state) -{ - modify(m_escort, state); -} - -int ShipFlagsDialogModel::getEscort() const -{ - return m_escort; -} - -void ShipFlagsDialogModel::setEscortValue(const int value) -{ - modify(m_escort_value, value); -} - -int ShipFlagsDialogModel::getEscortValue() const -{ - return m_escort_value; -} - -void ShipFlagsDialogModel::setNoArrivalMusic(const int state) -{ - modify(m_no_arrival_music, state); -} - -int ShipFlagsDialogModel::getNoArrivalMusic() const -{ - return m_no_arrival_music; -} - -void ShipFlagsDialogModel::setInvulnerable(const int state) -{ - modify(m_invulnerable, state); -} - -int ShipFlagsDialogModel::getInvulnerable() const -{ - return m_invulnerable; -} - -void ShipFlagsDialogModel::setGuardianed(const int state) -{ - modify(m_guardian, state); -} - -int ShipFlagsDialogModel::getGuardianed() const -{ - return m_guardian; -} - -void ShipFlagsDialogModel::setPrimitiveSensors(const int state) -{ - modify(m_primitive_sensors, state); -} - -int ShipFlagsDialogModel::getPrimitiveSensors() const -{ - return m_primitive_sensors; -} - -void ShipFlagsDialogModel::setNoSubspaceDrive(const int state) -{ - modify(m_no_subspace_drive, state); -} - -int ShipFlagsDialogModel::getNoSubspaceDrive() const +namespace fso::fred::dialogs { +int ShipFlagsDialogModel::tristate_set(int val, int cur_state) { - return m_no_subspace_drive; -} + if (val) { + if (!cur_state) { + return CheckState::PartiallyChecked; + } + } else { + if (cur_state) { + return CheckState::PartiallyChecked; + } + } + if (cur_state == 1) { -void ShipFlagsDialogModel::setHidden(const int state) -{ - modify(m_hidden, state); + return CheckState::Checked; + } else { + return CheckState::Unchecked; + } } - -int ShipFlagsDialogModel::getHidden() const +std::pair* ShipFlagsDialogModel::getFlag(const SCP_string& flag_name) { - return m_hidden; -} -void ShipFlagsDialogModel::setStealth(const int state) -{ - modify(m_stealth, state); + for (auto& flag : flags) { + if (!stricmp(flag_name.c_str(), flag.first.c_str())) { + return &flag; + } + } + Assertion(false, "Illegal flag name \"[%s]\"", flag_name.c_str()); + return nullptr; } -int ShipFlagsDialogModel::getStealth() const +void ShipFlagsDialogModel::setFlag(const SCP_string& flag_name, int value) { - return m_stealth; + for (auto& flag : flags) { + if (!stricmp(flag_name.c_str(), flag.first.c_str())) { + flag.second = value; + set_modified(); + } + } } -void ShipFlagsDialogModel::setFriendlyStealth(const int state) +void ShipFlagsDialogModel::setDestroyTime(int value) { - modify(m_friendly_stealth_invisible, state); + modify(destroytime, value); } -int ShipFlagsDialogModel::getFriendlyStealth() const +int ShipFlagsDialogModel::getDestroyTime() const { - return m_friendly_stealth_invisible; + return destroytime; } -void ShipFlagsDialogModel::setKamikaze(const int state) +void ShipFlagsDialogModel::setEscortPriority(int value) { - modify(m_kamikaze, state); + modify(escortp, value); } -int ShipFlagsDialogModel::getKamikaze() const +int ShipFlagsDialogModel::getEscortPriority() const { - return m_kamikaze; + return escortp; } -void ShipFlagsDialogModel::setKamikazeDamage(const int value) +void ShipFlagsDialogModel::setKamikazeDamage(int value) { - modify(m_kdamage, value); + modify(kamikazed, value); } int ShipFlagsDialogModel::getKamikazeDamage() const { - return m_kdamage; -} - -void ShipFlagsDialogModel::setDontChangePosition(const int state) -{ - modify(m_dont_change_position, state); -} - -int ShipFlagsDialogModel::getDontChangePosition() const -{ - return m_dont_change_position; -} - -void ShipFlagsDialogModel::setDontChangeOrientation(const int state) -{ - modify(m_dont_change_orientation, state); -} - -int ShipFlagsDialogModel::getDontChangeOrientation() const -{ - return m_dont_change_orientation; -} - -void ShipFlagsDialogModel::setNoDynamicGoals(const int state) -{ - modify(m_no_dynamic, state); -} - -int ShipFlagsDialogModel::getNoDynamicGoals() const -{ - return m_no_dynamic; -} - -void ShipFlagsDialogModel::setRedAlert(const int state) -{ - modify(m_red_alert_carry, state); -} - -int ShipFlagsDialogModel::getRedAlert() const -{ - return m_red_alert_carry; -} - -void ShipFlagsDialogModel::setGravity(const int state) -{ - modify(m_affected_by_gravity, state); -} - -int ShipFlagsDialogModel::getGravity() const -{ - return m_affected_by_gravity; -} - -void ShipFlagsDialogModel::setWarpin(const int state) -{ - modify(m_special_warpin, state); -} - -int ShipFlagsDialogModel::getWarpin() const -{ - return m_special_warpin; -} - -void ShipFlagsDialogModel::setTargetableAsBomb(const int state) -{ - modify(m_targetable_as_bomb, state); + return kamikazed; } -int ShipFlagsDialogModel::getTargetableAsBomb() const -{ - return m_targetable_as_bomb; -} - -void ShipFlagsDialogModel::setDisableBuiltInMessages(const int state) -{ - modify(m_disable_messages, state); -} - -int ShipFlagsDialogModel::getDisableBuiltInMessages() const -{ - return m_disable_messages; -} - -void ShipFlagsDialogModel::setNeverScream(const int state) -{ - modify(m_no_death_scream, state); -} - -int ShipFlagsDialogModel::getNeverScream() const -{ - return m_no_death_scream; -} - -void ShipFlagsDialogModel::setAlwaysScream(const int state) -{ - modify(m_always_death_scream, state); -} - -int ShipFlagsDialogModel::getAlwaysScream() const -{ - return m_always_death_scream; -} - -void ShipFlagsDialogModel::setVaporize(const int state) -{ - modify(m_vaporize, state); -} - -int ShipFlagsDialogModel::getVaporize() const -{ - return m_vaporize; -} - -void ShipFlagsDialogModel::setRespawnPriority(const int value) -{ - modify(m_respawn_priority, value); -} - -int ShipFlagsDialogModel::getRespawnPriority() const -{ - return m_respawn_priority; -} - -void ShipFlagsDialogModel::setAutoCarry(const int state) -{ - modify(m_nav_carry, state); -} - -int ShipFlagsDialogModel::getAutoCarry() const -{ - return m_nav_carry; -} - -void ShipFlagsDialogModel::setAutoLink(const int state) -{ - modify(m_nav_needslink, state); -} - -int ShipFlagsDialogModel::getAutoLink() const -{ - return m_nav_needslink; -} - -void ShipFlagsDialogModel::setHideShipName(const int state) -{ - modify(m_hide_ship_name, state); -} - -int ShipFlagsDialogModel::getHideShipName() const -{ - return m_hide_ship_name; -} - -void ShipFlagsDialogModel::setClassDynamic(const int state) -{ - modify(m_set_class_dynamically, state); -} - -int ShipFlagsDialogModel::getClassDynamic() const -{ - return m_set_class_dynamically; -} - -void ShipFlagsDialogModel::setDisableETS(const int state) -{ - modify(m_disable_ets, state); -} - -int ShipFlagsDialogModel::getDisableETS() const -{ - return m_disable_ets; -} - -void ShipFlagsDialogModel::setCloak(const int state) -{ - modify(m_cloaked, state); -} - -int ShipFlagsDialogModel::getCloak() const +void ShipFlagsDialogModel::update_ship(const int shipnum) { - return m_cloaked; + ship* shipp = &Ships[shipnum]; + object* objp = &Objects[shipp->objnum]; + for (const auto& [name, checked] : flags) { + for (size_t i = 0; i < Num_Parse_ship_flags; ++i) { + if (!stricmp(name.c_str(), Parse_ship_flags[i].name)) { + if (Parse_ship_flags[i].def == Ship::Ship_Flags::Reinforcement) { + _editor->set_reinforcement(shipp->ship_name, checked); + } else { + if (checked) { + shipp->flags.set(Parse_ship_flags[i].def); + } else { + shipp->flags.remove(Parse_ship_flags[i].def); + } + } + continue; + } + } + for (size_t i = 0; i < Num_Parse_ship_ai_flags; ++i) { + if (!stricmp(name.c_str(), Parse_ship_ai_flags[i].name)) { + if (checked) { + Ai_info[shipp->ai_index].ai_flags.set(Parse_ship_ai_flags[i].def); + } else { + Ai_info[shipp->ai_index].ai_flags.remove(Parse_ship_ai_flags[i].def); + } + continue; + } + } + for (size_t i = 0; i < Num_Parse_ship_object_flags; ++i) { + if (!stricmp(name.c_str(), Parse_ship_object_flags[i].name)) { + if (Parse_ship_object_flags[i].def == Object::Object_Flags::Collides) { + if (checked) { + objp->flags.remove(Parse_ship_object_flags[i].def); + } else { + objp->flags.set(Parse_ship_object_flags[i].def); + } + } else { + if (checked) { + objp->flags.set(Parse_ship_object_flags[i].def); + } else { + objp->flags.remove(Parse_ship_object_flags[i].def); + } + } + continue; + } + } + } + Ai_info[shipp->ai_index].kamikaze_damage = kamikazed; + shipp->escort_priority = escortp; + shipp->final_death_time = destroytime; } - -void ShipFlagsDialogModel::setScrambleMessages(const int state) +ShipFlagsDialogModel::ShipFlagsDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) { - modify(m_scramble_messages, state); + initializeData(); } -int ShipFlagsDialogModel::getScrambleMessages() const +bool ShipFlagsDialogModel::apply() { - return m_scramble_messages; -} + object* objp; -void ShipFlagsDialogModel::setNoCollide(const int state) -{ - modify(m_no_collide, state); -} + objp = GET_FIRST(&obj_used_list); + while (objp != END_OF_LIST(&obj_used_list)) { + if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { + if (objp->flags[Object::Object_Flags::Marked]) { + update_ship(objp->instance); + } + } + objp = GET_NEXT(objp); + } -int ShipFlagsDialogModel::getNoCollide() const -{ - return m_no_collide; + return true; } -void ShipFlagsDialogModel::setNoSelfDestruct(const int state) -{ - modify(m_no_disabled_self_destruct, state); -} +void ShipFlagsDialogModel::reject() {} -int ShipFlagsDialogModel::getNoSelfDestruct() const +const SCP_vector>& ShipFlagsDialogModel::getFlagsList() { - return m_no_disabled_self_destruct; + return flags; } void ShipFlagsDialogModel::initializeData() { object* objp; ship* shipp; - int j, first; + int first; first = 1; @@ -1134,154 +169,84 @@ void ShipFlagsDialogModel::initializeData() if ((objp->type == OBJ_START) || (objp->type == OBJ_SHIP)) { if (objp->flags[Object::Object_Flags::Marked]) { shipp = &Ships[objp->instance]; - if (first) { - first = 0; - m_scannable = (shipp->flags[Ship::Ship_Flags::Scannable]) ? 2 : 0; - m_red_alert_carry = (shipp->flags[Ship::Ship_Flags::Red_alert_store_status]) ? 2 : 0; - m_special_warpin = (objp->flags[Object::Object_Flags::Special_warpin]) ? 2 : 0; - m_protect_ship = (objp->flags[Object::Object_Flags::Protected]) ? 2 : 0; - m_beam_protect_ship = (objp->flags[Object::Object_Flags::Beam_protected]) ? 2 : 0; - m_flak_protect_ship = (objp->flags[Object::Object_Flags::Flak_protected]) ? 2 : 0; - m_laser_protect_ship = (objp->flags[Object::Object_Flags::Laser_protected]) ? 2 : 0; - m_missile_protect_ship = (objp->flags[Object::Object_Flags::Missile_protected]) ? 2 : 0; - m_invulnerable = (objp->flags[Object::Object_Flags::Invulnerable]) ? 2 : 0; - m_targetable_as_bomb = (objp->flags[Object::Object_Flags::Targetable_as_bomb]) ? 2 : 0; - m_dont_change_position = (objp->flags[Object::Object_Flags::Dont_change_position]) ? 2 : 0; - m_dont_change_orientation = (objp->flags[Object::Object_Flags::Dont_change_orientation]) ? 2 : 0; - m_hidden = (shipp->flags[Ship::Ship_Flags::Hidden_from_sensors]) ? 2 : 0; - m_primitive_sensors = (shipp->flags[Ship::Ship_Flags::Primitive_sensors]) ? 2 : 0; - m_no_subspace_drive = (shipp->flags[Ship::Ship_Flags::No_subspace_drive]) ? 2 : 0; - m_affected_by_gravity = (shipp->flags[Ship::Ship_Flags::Affected_by_gravity]) ? 2 : 0; - m_toggle_subsystem_scanning = (shipp->flags[Ship::Ship_Flags::Toggle_subsystem_scanning]) ? 2 : 0; - m_ignore_count = (shipp->flags[Ship::Ship_Flags::Ignore_count]) ? 2 : 0; - m_no_arrival_music = (shipp->flags[Ship::Ship_Flags::No_arrival_music]) ? 2 : 0; - m_cargo_known = (shipp->flags[Ship::Ship_Flags::Cargo_revealed]) ? 2 : 0; - m_no_dynamic = (Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::No_dynamic]) ? 2 : 0; - m_disable_messages = (shipp->flags[Ship::Ship_Flags::No_builtin_messages]) ? 2 : 0; - m_set_class_dynamically = (shipp->flags[Ship::Ship_Flags::Set_class_dynamically]) ? 2 : 0; - m_no_death_scream = (shipp->flags[Ship::Ship_Flags::No_death_scream]) ? 2 : 0; - m_always_death_scream = (shipp->flags[Ship::Ship_Flags::Always_death_scream]) ? 2 : 0; - m_guardian = (shipp->ship_guardian_threshold) ? 2 : 0; - m_vaporize = (shipp->flags[Ship::Ship_Flags::Vaporize]) ? 2 : 0; - m_stealth = (shipp->flags[Ship::Ship_Flags::Stealth]) ? 2 : 0; - m_friendly_stealth_invisible = (shipp->flags[Ship::Ship_Flags::Friendly_stealth_invis]) ? 2 : 0; - m_nav_carry = (shipp->flags[Ship::Ship_Flags::Navpoint_carry]) ? 2 : 0; - m_nav_needslink = (shipp->flags[Ship::Ship_Flags::Navpoint_needslink]) ? 2 : 0; - m_hide_ship_name = (shipp->flags[Ship::Ship_Flags::Hide_ship_name]) ? 2 : 0; - m_disable_ets = (shipp->flags[Ship::Ship_Flags::No_ets]) ? 2 : 0; - m_cloaked = (shipp->flags[Ship::Ship_Flags::Cloaked]) ? 2 : 0; - m_scramble_messages = (shipp->flags[Ship::Ship_Flags::Scramble_messages]) ? 2 : 0; - m_no_collide = (objp->flags[Object::Object_Flags::Collides]) ? 0 : 2; - m_no_disabled_self_destruct = (shipp->flags[Ship::Ship_Flags::No_disabled_self_destruct]) ? 2 : 0; - - m_destroy = (shipp->flags[Ship::Ship_Flags::Kill_before_mission]) ? 2 : 0; - m_destroy_value = shipp->final_death_time; - - m_kamikaze = (Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::Kamikaze]) ? 2 : 0; - m_kdamage = Ai_info[shipp->ai_index].kamikaze_damage; - - m_escort = (shipp->flags[Ship::Ship_Flags::Escort]) ? 2 : 0; - m_escort_value = shipp->escort_priority; - - if (The_mission.game_type & MISSION_TYPE_MULTI) { - m_respawn_priority = shipp->respawn_priority; + kamikazed = Ai_info[shipp->ai_index].kamikaze_damage; + escortp = shipp->escort_priority; + destroytime = shipp->final_death_time; + for (size_t i = 0; i < Num_Parse_ship_flags; i++) { + auto flagDef = Parse_ship_flags[i]; + + // Skip flags that have checkboxes elsewhere in the dialog + if (flagDef.def == Ship::Ship_Flags::No_arrival_warp || + flagDef.def == Ship::Ship_Flags::No_departure_warp || + flagDef.def == Ship::Ship_Flags::Same_arrival_warp_when_docked || + flagDef.def == Ship::Ship_Flags::Same_departure_warp_when_docked || + flagDef.def == Ship::Ship_Flags::Primaries_locked || + flagDef.def == Ship::Ship_Flags::Secondaries_locked || + flagDef.def == Ship::Ship_Flags::Ship_locked || + flagDef.def == Ship::Ship_Flags::Weapons_locked || + flagDef.def == Ship::Ship_Flags::Afterburner_locked || + flagDef.def == Ship::Ship_Flags::Lock_all_turrets_initially || + flagDef.def == Ship::Ship_Flags::Force_shields_on) { + continue; + } + bool checked = shipp->flags[flagDef.def]; + flags.emplace_back(flagDef.name, checked); } - - for (j = 0; j < Num_reinforcements; j++) { - if (!stricmp(Reinforcements[j].name, shipp->ship_name)) { - break; + for (size_t i = 0; i < Num_Parse_ship_ai_flags; i++) { + auto flagDef = Parse_ship_ai_flags[i]; + bool checked = Ai_info[shipp->ai_index].ai_flags[flagDef.def]; + flags.emplace_back(flagDef.name, checked); + } + for (size_t i = 0; i < Num_Parse_ship_object_flags; i++) { + auto flagDef = Parse_ship_object_flags[i]; + bool checked; + if (flagDef.def == Object::Object_Flags::Collides) { + checked = !objp->flags[flagDef.def]; + } else { + checked = objp->flags[flagDef.def]; } + flags.emplace_back(flagDef.name, checked); } - - m_reinforcement = (j < Num_reinforcements) ? 1 : 0; - } else { - - m_scannable = tristate_set(shipp->flags[Ship::Ship_Flags::Scannable], m_scannable); - m_red_alert_carry = - tristate_set(shipp->flags[Ship::Ship_Flags::Red_alert_store_status], m_red_alert_carry); - m_special_warpin = - tristate_set(objp->flags[Object::Object_Flags::Special_warpin], m_special_warpin); - m_protect_ship = tristate_set(objp->flags[Object::Object_Flags::Protected], m_protect_ship); - m_beam_protect_ship = - tristate_set(objp->flags[Object::Object_Flags::Beam_protected], m_beam_protect_ship); - m_flak_protect_ship = - tristate_set(objp->flags[Object::Object_Flags::Flak_protected], m_flak_protect_ship); - m_laser_protect_ship = - tristate_set(objp->flags[Object::Object_Flags::Laser_protected], m_laser_protect_ship); - m_missile_protect_ship = - tristate_set(objp->flags[Object::Object_Flags::Missile_protected], m_missile_protect_ship); - m_invulnerable = tristate_set(objp->flags[Object::Object_Flags::Invulnerable], m_invulnerable); - m_targetable_as_bomb = - tristate_set(objp->flags[Object::Object_Flags::Targetable_as_bomb], m_targetable_as_bomb); - m_dont_change_position = tristate_set(objp->flags[Object::Object_Flags::Dont_change_position], m_dont_change_position); - m_dont_change_orientation = tristate_set(objp->flags[Object::Object_Flags::Dont_change_orientation], m_dont_change_orientation); - m_hidden = tristate_set(shipp->flags[Ship::Ship_Flags::Hidden_from_sensors], m_hidden); - m_primitive_sensors = - tristate_set(shipp->flags[Ship::Ship_Flags::Primitive_sensors], m_primitive_sensors); - m_no_subspace_drive = - tristate_set(shipp->flags[Ship::Ship_Flags::No_subspace_drive], m_no_subspace_drive); - m_affected_by_gravity = - tristate_set(shipp->flags[Ship::Ship_Flags::Affected_by_gravity], m_affected_by_gravity); - m_toggle_subsystem_scanning = - tristate_set(shipp->flags[Ship::Ship_Flags::Toggle_subsystem_scanning], - m_toggle_subsystem_scanning); - m_ignore_count = tristate_set(shipp->flags[Ship::Ship_Flags::Ignore_count], m_ignore_count); - m_no_arrival_music = - tristate_set(shipp->flags[Ship::Ship_Flags::No_arrival_music], m_no_arrival_music); - m_cargo_known = tristate_set(shipp->flags[Ship::Ship_Flags::Cargo_revealed], m_cargo_known); - m_no_dynamic = - tristate_set(Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::No_dynamic], m_no_dynamic); - m_disable_messages = - tristate_set(shipp->flags[Ship::Ship_Flags::No_builtin_messages], m_disable_messages); - m_set_class_dynamically = - tristate_set(shipp->flags[Ship::Ship_Flags::Set_class_dynamically], m_set_class_dynamically); - m_no_death_scream = - tristate_set(shipp->flags[Ship::Ship_Flags::No_death_scream], m_no_death_scream); - m_always_death_scream = - tristate_set(shipp->flags[Ship::Ship_Flags::Always_death_scream], m_always_death_scream); - m_guardian = tristate_set(shipp->ship_guardian_threshold, m_guardian); - m_vaporize = tristate_set(shipp->flags[Ship::Ship_Flags::Vaporize], m_vaporize); - m_stealth = tristate_set(shipp->flags[Ship::Ship_Flags::Stealth], m_stealth); - m_friendly_stealth_invisible = tristate_set(shipp->flags[Ship::Ship_Flags::Friendly_stealth_invis], - m_friendly_stealth_invisible); - m_nav_carry = tristate_set(shipp->flags[Ship::Ship_Flags::Navpoint_carry], m_nav_carry); - m_nav_needslink = tristate_set(shipp->flags[Ship::Ship_Flags::Navpoint_needslink], m_nav_needslink); - m_hide_ship_name = tristate_set(shipp->flags[Ship::Ship_Flags::Hide_ship_name], m_hide_ship_name); - m_disable_ets = tristate_set(shipp->flags[Ship::Ship_Flags::No_ets], m_disable_ets); - m_cloaked = tristate_set(shipp->flags[Ship::Ship_Flags::Cloaked], m_cloaked); - m_scramble_messages = - tristate_set(shipp->flags[Ship::Ship_Flags::Scramble_messages], m_scramble_messages); - m_no_collide = tristate_set(!(objp->flags[Object::Object_Flags::Collides]), m_no_collide); - m_no_disabled_self_destruct = - tristate_set(shipp->flags[Ship::Ship_Flags::No_disabled_self_destruct], - m_no_disabled_self_destruct); - - // check the final death time and set the internal variable according to whether or not - // the final_death_time is set. Also, the value in the edit box must be set if all the - // values are the same, and cleared if the values are not the same. - m_destroy = tristate_set(shipp->flags[Ship::Ship_Flags::Kill_before_mission], m_destroy); - m_destroy_value = shipp->final_death_time; - - m_kamikaze = tristate_set(Ai_info[shipp->ai_index].ai_flags[AI::AI_Flags::Kamikaze], m_kamikaze); - m_kdamage = Ai_info[shipp->ai_index].kamikaze_damage; - - m_escort = tristate_set(shipp->flags[Ship::Ship_Flags::Escort], m_escort); - m_escort_value = shipp->escort_priority; - - if (The_mission.game_type & MISSION_TYPE_MULTI) { - m_respawn_priority = shipp->escort_priority; + for (size_t i = 0; i < Num_Parse_ship_flags; i++) { + auto flagDef = Parse_ship_flags[i]; + // Skip flags that have checkboxes elsewhere in the dialog + if (flagDef.def == Ship::Ship_Flags::No_arrival_warp || + flagDef.def == Ship::Ship_Flags::No_departure_warp || + flagDef.def == Ship::Ship_Flags::Same_arrival_warp_when_docked || + flagDef.def == Ship::Ship_Flags::Same_departure_warp_when_docked || + flagDef.def == Ship::Ship_Flags::Primaries_locked || + flagDef.def == Ship::Ship_Flags::Secondaries_locked || + flagDef.def == Ship::Ship_Flags::Ship_locked || + flagDef.def == Ship::Ship_Flags::Weapons_locked || + flagDef.def == Ship::Ship_Flags::Afterburner_locked || + flagDef.def == Ship::Ship_Flags::Lock_all_turrets_initially || + flagDef.def == Ship::Ship_Flags::Force_shields_on) { + continue; + } + bool checked = shipp->flags[flagDef.def]; + getFlag(flagDef.name)->second = tristate_set(checked, getFlag(flagDef.name)->second); } - - for (j = 0; j < Num_reinforcements; j++) { - if (!stricmp(Reinforcements[j].name, shipp->ship_name)) { - break; + for (size_t i = 0; i < Num_Parse_ship_ai_flags; i++) { + auto flagDef = Parse_ship_ai_flags[i]; + bool checked = Ai_info[shipp->ai_index].ai_flags[flagDef.def]; + getFlag(flagDef.name)->second = tristate_set(checked, getFlag(flagDef.name)->second); + } + for (size_t i = 0; i < Num_Parse_ship_object_flags; i++) { + auto flagDef = Parse_ship_object_flags[i]; + // Skip flags that have checkboxes elsewhere in the dialog + if (flagDef.def == Object::Object_Flags::No_shields) { + continue; } + bool checked; + if (flagDef.def == Object::Object_Flags::Collides) { + checked = !objp->flags[flagDef.def]; + } else { + checked = objp->flags[flagDef.def]; + } + getFlag(flagDef.name)->second = tristate_set(checked, getFlag(flagDef.name)->second); } - m_reinforcement = tristate_set(j < Num_reinforcements, m_reinforcement); - - ; } } } @@ -1290,6 +255,4 @@ void ShipFlagsDialogModel::initializeData() } modelChanged(); } -} // namespace dialogs -} // namespace fred -} // namespace fso +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.h index 504d5d3beb4..310dd94be46 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipFlagsDialogModel.h @@ -2,62 +2,18 @@ #include "../AbstractDialogModel.h" -namespace fso { -namespace fred { -namespace dialogs { +#include + +namespace fso::fred::dialogs { class ShipFlagsDialogModel : public AbstractDialogModel { private: - - int m_red_alert_carry; - int m_scannable; - int m_reinforcement; - int m_protect_ship; - int m_beam_protect_ship; - int m_flak_protect_ship; - int m_laser_protect_ship; - int m_missile_protect_ship; - int m_no_dynamic; - int m_no_arrival_music; - int m_kamikaze; - int m_invulnerable; - int m_targetable_as_bomb; - int m_dont_change_position; - int m_dont_change_orientation; - int m_ignore_count; - int m_hidden; - int m_primitive_sensors; - int m_no_subspace_drive; - int m_affected_by_gravity; - int m_toggle_subsystem_scanning; - int m_escort; - int m_destroy; - int m_cargo_known; - int m_special_warpin; - int m_disable_messages; - int m_no_death_scream; - int m_always_death_scream; - int m_guardian; - int m_vaporize; - int m_stealth; - int m_friendly_stealth_invisible; - int m_nav_carry; - int m_nav_needslink; - int m_hide_ship_name; - int m_disable_ets; - int m_cloaked; - int m_set_class_dynamically; - int m_scramble_messages; - int m_no_collide; - int m_no_disabled_self_destruct; - - int m_kdamage; - int m_destroy_value; - int m_escort_value; - int m_respawn_priority; - static int tristate_set(const int val, const int cur_state); void update_ship(const int); + SCP_vector> flags; + int destroytime; + int escortp; + int kamikazed; public: ShipFlagsDialogModel(QObject* parent, EditorViewport* viewport); @@ -66,140 +22,15 @@ class ShipFlagsDialogModel : public AbstractDialogModel { bool apply() override; void reject() override; - void setDestroyed(const int); - int getDestroyed() const; - - void setDestroyedSeconds(const int); - int getDestroyedSeconds() const; - - void setScannable(const int); - int getScannable() const; - - void setCargoKnown(const int); - int getCargoKnown() const; - - void setSubsystemScanning(const int); - int getSubsystemScanning() const; - - void setReinforcment(const int); - int getReinforcment() const; - - void setProtectShip(const int); - int getProtectShip() const; - - void setBeamProtect(const int); - int getBeamProtect() const; - - void setFlakProtect(const int); - int getFlakProtect() const; - - void setLaserProtect(const int); - int getLaserProtect() const; - - void setMissileProtect(const int); - int getMissileProtect() const; - - void setIgnoreForGoals(const int); - int getIgnoreForGoals() const; - - void setEscort(const int); - int getEscort() const; - void setEscortValue(const int); - int getEscortValue() const; - - void setNoArrivalMusic(const int); - int getNoArrivalMusic() const; - - void setInvulnerable(const int); - int getInvulnerable() const; - - void setGuardianed(const int); - int getGuardianed() const; - - void setPrimitiveSensors(const int); - int getPrimitiveSensors() const; - - void setNoSubspaceDrive(const int); - int getNoSubspaceDrive() const; - - void setHidden(const int); - int getHidden() const; - - void setStealth(const int); - int getStealth() const; + const SCP_vector>& getFlagsList(); + std::pair* getFlag(const SCP_string& flag_name); + void setFlag(const SCP_string& flag_name, int); - void setFriendlyStealth(const int); - int getFriendlyStealth() const; - - void setKamikaze(const int); - int getKamikaze() const; - void setKamikazeDamage(const int); + void setDestroyTime(int); + int getDestroyTime() const; + void setEscortPriority(int); + int getEscortPriority() const; + void setKamikazeDamage(int); int getKamikazeDamage() const; - - void setDontChangePosition(const int); - int getDontChangePosition() const; - - void setDontChangeOrientation(const int); - int getDontChangeOrientation() const; - - void setNoDynamicGoals(const int); - int getNoDynamicGoals() const; - - void setRedAlert(const int); - int getRedAlert() const; - - void setGravity(const int); - int getGravity() const; - - void setWarpin(const int); - int getWarpin() const; - - void setTargetableAsBomb(const int); - int getTargetableAsBomb() const; - - void setDisableBuiltInMessages(const int); - int getDisableBuiltInMessages() const; - - void setNeverScream(const int); - int getNeverScream() const; - - void setAlwaysScream(const int); - int getAlwaysScream() const; - - void setVaporize(const int); - int getVaporize() const; - - void setRespawnPriority(const int); - int getRespawnPriority() const; - - void setAutoCarry(const int); - int getAutoCarry() const; - - void setAutoLink(const int); - int getAutoLink() const; - - void setHideShipName(const int); - int getHideShipName() const; - - void setClassDynamic(const int); - int getClassDynamic() const; - - void setDisableETS(const int); - int getDisableETS() const; - - void setCloak(const int); - int getCloak() const; - - void setScrambleMessages(const int); - int getScrambleMessages() const; - - void setNoCollide(const int); - int getNoCollide() const; - - void setNoSelfDestruct(const int); - int getNoSelfDestruct() const; - }; -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp index 651bd8dcc0d..a298b4243f9 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp @@ -140,7 +140,7 @@ void ShipInitialStatusDialogModel::initializeData(bool multi) m_velocity = static_cast(Objects[_editor->currentObject].phys_info.speed); m_shields = static_cast(Objects[_editor->currentObject].shield_quadrant[0]); m_hull = static_cast(Objects[_editor->currentObject].hull_strength); - + guardian_threshold = Ships[m_ship].ship_guardian_threshold; if (Objects[_editor->currentObject].flags[Object::Object_Flags::No_shields]) m_has_shields = 0; else @@ -594,7 +594,7 @@ bool ShipInitialStatusDialogModel::apply() objp->flags.set(Object::Object_Flags::No_shields); } auto shipp = &Ships[get_ship_from_obj(objp)]; - + shipp->ship_guardian_threshold = guardian_threshold; // We need to ensure that we handle the inconsistent "boolean" value correctly handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Force_shields_on, m_force_shields); handle_inconsistent_flag(shipp->flags, Ship::Ship_Flags::Ship_locked, m_ship_locked); @@ -614,7 +614,7 @@ bool ShipInitialStatusDialogModel::apply() modify(Objects[_editor->currentObject].hull_strength, (float)m_hull); Objects[_editor->currentObject].flags.set(Object::Object_Flags::No_shields, m_has_shields == 0); - + Ships[m_ship].ship_guardian_threshold = guardian_threshold; // We need to ensure that we handle the inconsistent "boolean" value correctly. Not strictly needed here but // just to be safe... handle_inconsistent_flag(Ships[m_ship].flags, Ship::Ship_Flags::Force_shields_on, m_force_shields); @@ -883,6 +883,16 @@ bool ShipInitialStatusDialogModel::getIfMultpleShips() const return m_multi_edit; } +int ShipInitialStatusDialogModel::getGuardian() const +{ + return guardian_threshold; +} + +void ShipInitialStatusDialogModel::setGuardian(int value) +{ + modify(guardian_threshold, value); +} + } // namespace dialogs } // namespace fred } // namespace fso \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.h b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.h index a96449a0b24..4f53838fe55 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.h +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.h @@ -20,10 +20,9 @@ class ShipInitialStatusDialogModel : public AbstractDialogModel { private: - + int guardian_threshold; int m_ship; int cur_subsys = -1; - int m_damage; int m_shields; int m_force_shields; @@ -122,6 +121,9 @@ class ShipInitialStatusDialogModel : public AbstractDialogModel { bool getUseTeamcolours() const; bool getIfMultpleShips() const; + + int getGuardian() const; + void setGuardian(int); }; /** diff --git a/qtfred/src/mission/management.h b/qtfred/src/mission/management.h index caee0ecc4df..088c07a3e0d 100644 --- a/qtfred/src/mission/management.h +++ b/qtfred/src/mission/management.h @@ -7,6 +7,12 @@ namespace fso { namespace fred { +enum CheckState { + Unchecked = Qt::Unchecked, + PartiallyChecked = Qt::PartiallyChecked, + Checked = Qt::Checked +}; + enum class SubSystem { OS, CommandLine, diff --git a/qtfred/src/mission/missionsave.cpp b/qtfred/src/mission/missionsave.cpp index 4d411b32305..ae61f1d53e1 100644 --- a/qtfred/src/mission/missionsave.cpp +++ b/qtfred/src/mission/missionsave.cpp @@ -3959,7 +3959,18 @@ int CFred_mission_save::save_objects() fout(" %d", shipp->escort_priority); } + // Custom Guardian Thrshold + if (save_format != MissionFormat::RETAIL) { + if (shipp->ship_guardian_threshold != 0) { + if (optional_string_fred("+Guardian Threshold:", "$Name:")) { + parse_comments(); + } else { + fout("\n+Guardian Threshold:"); + } + fout(" %d", shipp->ship_guardian_threshold); + } + } // special explosions if (save_format != MissionFormat::RETAIL) { if (shipp->use_special_explosion) { diff --git a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp index 698cf44e52c..5de5794e363 100644 --- a/qtfred/src/ui/dialogs/MissionSpecDialog.cpp +++ b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp @@ -122,7 +122,7 @@ void MissionSpecDialog::updateFlags() { const auto flags = _model->getMissionFlagsList(); - QVector> toWidget; + QVector> toWidget; toWidget.reserve(static_cast(flags.size())); for (const auto& p : flags) { QString name = QString::fromUtf8(p.first.c_str()); diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index 592dbf431f6..c4fb66507bb 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp @@ -11,9 +11,7 @@ #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { ShipEditorDialog::ShipEditorDialog(FredView* parent, EditorViewport* viewport) : QDialog(parent), ui(new Ui::ShipEditorDialog()), _model(new ShipEditorDialogModel(this, viewport)), @@ -22,27 +20,12 @@ ShipEditorDialog::ShipEditorDialog(FredView* parent, EditorViewport* viewport) this->setFocus(); ui->setupUi(this); - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &ShipEditorDialog::updateUI); + connect(_model.get(), &AbstractDialogModel::modelChanged, this, [this] { updateUI(false); }); connect(this, &QDialog::accepted, _model.get(), &ShipEditorDialogModel::apply); connect(viewport->editor, &Editor::currentObjectChanged, this, &ShipEditorDialog::update); connect(viewport->editor, &Editor::objectMarkingChanged, this, &ShipEditorDialog::update); // Column One - connect(ui->shipNameEdit, (&QLineEdit::editingFinished), this, &ShipEditorDialog::shipNameChanged); - connect(ui->shipDisplayNameEdit, (&QLineEdit::editingFinished), this, &ShipEditorDialog::shipDisplayNameChanged); - - connect(ui->shipClassCombo, - static_cast(&QComboBox::currentIndexChanged), - this, - &ShipEditorDialog::shipClassChanged); - connect(ui->AIClassCombo, - static_cast(&QComboBox::currentIndexChanged), - this, - &ShipEditorDialog::aiClassChanged); - connect(ui->teamCombo, - static_cast(&QComboBox::currentIndexChanged), - this, - &ShipEditorDialog::teamChanged); connect(ui->cargoCombo->lineEdit(), (&QLineEdit::editingFinished), this, &ShipEditorDialog::cargoChanged); connect(ui->altNameCombo->lineEdit(), (&QLineEdit::textEdited), this, &ShipEditorDialog::altNameChanged); @@ -50,91 +33,7 @@ ShipEditorDialog::ShipEditorDialog(FredView* parent, EditorViewport* viewport) // ui->cargoCombo->installEventFilter(this); - // Column Two - connect(ui->hotkeyCombo, - static_cast(&QComboBox::currentIndexChanged), - this, - &ShipEditorDialog::hotkeyChanged); - - connect(ui->personaCombo, - static_cast(&QComboBox::currentIndexChanged), - this, - &ShipEditorDialog::personaChanged); - - connect(ui->killScoreEdit, - static_cast(&QSpinBox::valueChanged), - this, - &ShipEditorDialog::scoreChanged); - - connect(ui->assistEdit, - static_cast(&QSpinBox::valueChanged), - this, - &ShipEditorDialog::assistChanged); - connect(ui->playerShipCheckBox, &QCheckBox::toggled, this, &ShipEditorDialog::playerChanged); - - // Arival Box - connect(ui->arrivalLocationCombo, - static_cast(&QComboBox::currentIndexChanged), - this, - &ShipEditorDialog::arrivalLocationChanged); - connect(ui->arrivalTargetCombo, - static_cast(&QComboBox::currentIndexChanged), - this, - &ShipEditorDialog::arrivalTargetChanged); - connect(ui->arrivalDistanceEdit, - static_cast(&QSpinBox::valueChanged), - this, - &ShipEditorDialog::arrivalDistanceChanged); - connect(ui->arrivalDelaySpinBox, - static_cast(&QSpinBox::valueChanged), - this, - &ShipEditorDialog::arrivalDelayChanged); - - connect(ui->updateArrivalCueCheckBox, &QCheckBox::toggled, this, &ShipEditorDialog::ArrivalCueChanged); - - connect(ui->arrivalTree, &sexp_tree::rootNodeFormulaChanged, this, [this](int old, int node) { - // use this otherwise linux complains - - _model->setArrivalFormula(old, node); - }); - connect(ui->arrivalTree, &sexp_tree::helpChanged, this, [this](const QString& help) { - ui->helpText->setPlainText(help); - }); - connect(ui->arrivalTree, &sexp_tree::miniHelpChanged, this, [this](const QString& help) { - ui->HelpTitle->setText(help); - }); - - connect(ui->noArrivalWarpCheckBox, &QCheckBox::toggled, this, &ShipEditorDialog::arrivalWarpChanged); - - // Departure Box - connect(ui->departureLocationCombo, - static_cast(&QComboBox::currentIndexChanged), - this, - &ShipEditorDialog::departureLocationChanged); - connect(ui->departureTargetCombo, - static_cast(&QComboBox::currentIndexChanged), - this, - &ShipEditorDialog::departureTargetChanged); - connect(ui->departureDelaySpinBox, - static_cast(&QSpinBox::valueChanged), - this, - &ShipEditorDialog::departureDelayChanged); - - connect(ui->updateDepartureCueCheckBox, &QCheckBox::toggled, this, &ShipEditorDialog::DepartureCueChanged); - - connect(ui->departureTree, &sexp_tree::rootNodeFormulaChanged, this, [this](int old, int node) { - // use this otherwise linux complains - _model->setDepartureFormula(old, node); - }); - connect(ui->departureTree, &sexp_tree::helpChanged, this, [this](const QString& help) { - ui->helpText->setPlainText(help); - }); - connect(ui->departureTree, &sexp_tree::miniHelpChanged, this, [this](const QString& help) { - ui->HelpTitle->setText(help); - }); - connect(ui->noDepartureWarpCheckBox, &QCheckBox::toggled, this, &ShipEditorDialog::departureWarpChanged); - - updateUI(); + updateUI(true); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); @@ -168,7 +67,8 @@ void ShipEditorDialog::hideEvent(QHideEvent* e) { QDialog::hideEvent(e); } -void ShipEditorDialog::showEvent(QShowEvent* e) { +void ShipEditorDialog::showEvent(QShowEvent* e) +{ _model->initializeData(); QDialog::showEvent(e); } @@ -189,7 +89,8 @@ void ShipEditorDialog::on_initialStatusButton_clicked() void ShipEditorDialog::on_initialOrdersButton_clicked() { - auto dialog = new dialogs::ShipGoalsDialog(this, _viewport, getIfMultipleShips(), Ships[getSingleShip()].objnum, -1); + auto dialog = + new dialogs::ShipGoalsDialog(this, _viewport, getIfMultipleShips(), Ships[getSingleShip()].objnum, -1); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } @@ -208,162 +109,149 @@ void ShipEditorDialog::update() _model->apply(); } _model->initializeData(); + updateUI(true); } } -void ShipEditorDialog::updateUI() +void ShipEditorDialog::updateUI(bool overwrite) { util::SignalBlockers blockers(this); enableDisable(); - updateColumnOne(); - updateColumnTwo(); - updateArrival(); - updateDeparture(); + updateColumnOne(overwrite); + updateColumnTwo(overwrite); + updateArrival(overwrite); + updateDeparture(overwrite); } -void ShipEditorDialog::updateColumnOne() +void ShipEditorDialog::updateColumnOne(bool overwrite) { util::SignalBlockers blockers(this); - ui->shipNameEdit->setText(_model->getShipName().c_str()); - ui->shipDisplayNameEdit->setText(_model->getShipDisplayName().c_str()); - size_t i; - auto idx = _model->getShipClass(); - ui->shipClassCombo->clear(); - for (i = 0; i < Ship_info.size(); i++) { - ui->shipClassCombo->addItem(Ship_info[i].name, QVariant(static_cast(i))); - } - ui->shipClassCombo->setCurrentIndex(ui->shipClassCombo->findData(idx)); + int idx; + if (overwrite) { + ui->shipNameEdit->setText(_model->getShipName().c_str()); + ui->shipDisplayNameEdit->setText(_model->getShipDisplayName().c_str()); + idx = _model->getShipClass(); + ui->shipClassCombo->clear(); + for (size_t i = 0; i < Ship_info.size(); i++) { + ui->shipClassCombo->addItem(Ship_info[i].name, QVariant(static_cast(i))); + } + ui->shipClassCombo->setCurrentIndex(ui->shipClassCombo->findData(idx)); - auto ai = _model->getAIClass(); - ui->AIClassCombo->clear(); - for (int j = 0; j < Num_ai_classes; j++) { - ui->AIClassCombo->addItem(Ai_class_names[j], QVariant(j)); + auto ai = _model->getAIClass(); + ui->AIClassCombo->clear(); + for (auto j = 0; j < Num_ai_classes; j++) { + ui->AIClassCombo->addItem(Ai_class_names[j], QVariant(j)); + } + ui->AIClassCombo->setCurrentIndex(ui->AIClassCombo->findData(ai)); } - ui->AIClassCombo->setCurrentIndex(ui->AIClassCombo->findData(ai)); - if (_model->getNumSelectedPlayers()) { if (_model->getTeam() != -1) { ui->teamCombo->setEnabled(true); } else { ui->teamCombo->setEnabled(false); } - - ui->teamCombo->clear(); - for (i = 0; i < MAX_TVT_TEAMS; i++) { - ui->teamCombo->addItem(Iff_info[i].iff_name, QVariant(static_cast(i))); + if (overwrite) { + ui->teamCombo->clear(); + for (auto i = 0; i < MAX_TVT_TEAMS; i++) { + ui->teamCombo->addItem(Iff_info[i].iff_name, QVariant(static_cast(i))); + } } } else { - idx = _model->getTeam(); ui->teamCombo->setEnabled(_model->getUIEnable()); - ui->teamCombo->clear(); - for (i = 0; i < Iff_info.size(); i++) { - ui->teamCombo->addItem(Iff_info[i].iff_name, QVariant(static_cast(i))); + if (overwrite) { + idx = _model->getTeam(); + ui->teamCombo->clear(); + for (size_t i = 0; i < Iff_info.size(); i++) { + ui->teamCombo->addItem(Iff_info[i].iff_name, QVariant(static_cast(i))); + } + ui->teamCombo->setCurrentIndex(ui->teamCombo->findData(idx)); } - ui->teamCombo->setCurrentIndex(ui->teamCombo->findData(idx)); - } - auto cargo = _model->getCargo(); - ui->cargoCombo->clear(); - int j; - for (j = 0; j < Num_cargo; j++) { - ui->cargoCombo->addItem(Cargo_names[j]); } - if (ui->cargoCombo->findText(QString(cargo.c_str()))) { - ui->cargoCombo->setCurrentIndex(ui->cargoCombo->findText(QString(cargo.c_str()))); - } else { - ui->cargoCombo->addItem(cargo.c_str()); + if (overwrite) { + auto cargo = _model->getCargo(); + ui->cargoCombo->clear(); + int j; + for (j = 0; j < Num_cargo; j++) { + ui->cargoCombo->addItem(Cargo_names[j]); + } + if (ui->cargoCombo->findText(QString(cargo.c_str()))) { + ui->cargoCombo->setCurrentIndex(ui->cargoCombo->findText(QString(cargo.c_str()))); + } else { + ui->cargoCombo->addItem(cargo.c_str()); - ui->cargoCombo->setCurrentIndex(ui->cargoCombo->findText(QString(cargo.c_str()))); + ui->cargoCombo->setCurrentIndex(ui->cargoCombo->findText(QString(cargo.c_str()))); + } } - ui->altNameCombo->clear(); if (_model->getNumSelectedObjects()) { if (_model->getIfMultipleShips()) { ui->altNameCombo->setEnabled(false); } else { auto altname = _model->getAltName(); ui->altNameCombo->setEnabled(true); - ui->altNameCombo->addItem(""); - for (j = 0; j < Mission_alt_type_count; j++) { - ui->altNameCombo->addItem(Mission_alt_types[j]); - } - if (ui->altNameCombo->findText(QString(altname.c_str()))) { - ui->altNameCombo->setCurrentIndex(ui->altNameCombo->findText(QString(altname.c_str()))); - } else { - ui->altNameCombo->setCurrentIndex(ui->altNameCombo->findText("")); + if (overwrite) { + ui->altNameCombo->clear(); + ui->altNameCombo->addItem(""); + for (auto j = 0; j < Mission_alt_type_count; j++) { + ui->altNameCombo->addItem(Mission_alt_types[j]); + } + if (ui->altNameCombo->findText(QString(altname.c_str()))) { + ui->altNameCombo->setCurrentIndex(ui->altNameCombo->findText(QString(altname.c_str()))); + } else { + ui->altNameCombo->setCurrentIndex(ui->altNameCombo->findText("")); + } } } } - ui->callsignCombo->clear(); if (_model->getNumSelectedObjects()) { if (_model->getIfMultipleShips()) { ui->callsignCombo->setEnabled(false); } else { + ui->callsignCombo->clear(); auto callsign = _model->getCallsign(); - ui->callsignCombo->addItem(""); ui->callsignCombo->setEnabled(true); - for (j = 0; j < Mission_callsign_count; j++) { - ui->callsignCombo->addItem(Mission_callsigns[j], QVariant(Mission_callsigns[j])); - } + if (overwrite) { + ui->callsignCombo->addItem(""); + for (auto j = 0; j < Mission_callsign_count; j++) { + ui->callsignCombo->addItem(Mission_callsigns[j], QVariant(Mission_callsigns[j])); + } - if (ui->callsignCombo->findText(QString(callsign.c_str()))) { - ui->callsignCombo->setCurrentIndex(ui->callsignCombo->findText(QString(callsign.c_str()))); - } else { - ui->altNameCombo->setCurrentIndex(ui->callsignCombo->findText("")); + if (ui->callsignCombo->findText(QString(callsign.c_str()))) { + ui->callsignCombo->setCurrentIndex(ui->callsignCombo->findText(QString(callsign.c_str()))); + } else { + ui->altNameCombo->setCurrentIndex(ui->callsignCombo->findText("")); + } } } } } -void ShipEditorDialog::updateColumnTwo() +void ShipEditorDialog::updateColumnTwo(bool overwrite) { util::SignalBlockers blockers(this); - ui->wing->setText(_model->getWing().c_str()); - - ui->personaCombo->clear(); - ui->personaCombo->addItem("", QVariant(-1)); - for (size_t i = 0; i < Personas.size(); i++) { - if (Personas[i].flags & PERSONA_FLAG_WINGMAN) { - SCP_string persona_name = Personas[i].name; - - // see if the bitfield matches one and only one species - int species = -1; - for (size_t j = 0; j < 32 && j < Species_info.size(); j++) { - if (Personas[i].species_bitfield == (1 << j)) { - species = static_cast(j); - break; - } - } + if (overwrite) { + ui->wing->setText(_model->getWing().c_str()); - // if it is an exact species that isn't the first - if (species > 0) { - persona_name += "-"; + auto idx = _model->getPersona(); + ui->personaCombo->setCurrentIndex(ui->personaCombo->findData(idx)); - auto species_name = Species_info[species].species_name; - size_t len = strlen(species_name); - for (size_t j = 0; j < 3 && j < len; j++) - persona_name += species_name[j]; - } - - ui->personaCombo->addItem(persona_name.c_str(), QVariant(static_cast(i))); - } - } - auto idx = _model->getPersona(); - ui->personaCombo->setCurrentIndex(ui->personaCombo->findData(idx)); + ui->killScoreEdit->setValue(_model->getScore()); - ui->killScoreEdit->setValue(_model->getScore()); + ui->assistEdit->setValue(_model->getAssist()); - ui->assistEdit->setValue(_model->getAssist()); - - ui->playerShipCheckBox->setChecked(_model->getPlayer()); + ui->playerShipCheckBox->setChecked(_model->getPlayer()); + ui->respawnSpinBox->setValue(_model->getRespawn()); + } } -void ShipEditorDialog::updateArrival() +void ShipEditorDialog::updateArrival(bool overwrite) { util::SignalBlockers blockers(this); - auto idx = _model->getArrivalLocationIndex(); - int i; - ui->arrivalLocationCombo->clear(); - for (i = 0; i < MAX_ARRIVAL_NAMES; i++) { - ui->arrivalLocationCombo->addItem(Arrival_location_names[i], QVariant(i)); + if (overwrite) { + auto idx = _model->getArrivalLocationIndex(); + int i; + ui->arrivalLocationCombo->clear(); + for (i = 0; i < MAX_ARRIVAL_NAMES; i++) { + ui->arrivalLocationCombo->addItem(Arrival_location_names[i], QVariant(i)); + } + ui->arrivalLocationCombo->setCurrentIndex(ui->arrivalLocationCombo->findData(idx)); } - ui->arrivalLocationCombo->setCurrentIndex(ui->arrivalLocationCombo->findData(idx)); - object* objp; int restrict_to_players; ui->arrivalTargetCombo->clear(); @@ -402,46 +290,49 @@ void ShipEditorDialog::updateArrival() } } ui->arrivalTargetCombo->setCurrentIndex(ui->arrivalTargetCombo->findData(_model->getArrivalTarget())); + if (overwrite) { + ui->arrivalDistanceEdit->clear(); + ui->arrivalDistanceEdit->setValue(_model->getArrivalDistance()); + ui->arrivalDelaySpinBox->setValue(_model->getArrivalDelay()); - ui->arrivalDistanceEdit->clear(); - ui->arrivalDistanceEdit->setValue(_model->getArrivalDistance()); - ui->arrivalDelaySpinBox->setValue(_model->getArrivalDelay()); + ui->updateArrivalCueCheckBox->setChecked(_model->getArrivalCue()); - ui->updateArrivalCueCheckBox->setChecked(_model->getArrivalCue()); + ui->arrivalTree->initializeEditor(_viewport->editor, this); + if (_model->getNumSelectedShips()) { - ui->arrivalTree->initializeEditor(_viewport->editor, this); - if (_model->getNumSelectedShips()) { - - if (_model->getIfMultipleShips()) { - ui->arrivalTree->clear_tree(""); - } - if (_model->getUseCue()) { - ui->arrivalTree->load_tree(_model->getArrivalFormula()); + if (_model->getIfMultipleShips()) { + ui->arrivalTree->clear_tree(""); + } + if (_model->getUseCue()) { + ui->arrivalTree->load_tree(_model->getArrivalFormula()); + } else { + ui->arrivalTree->clear_tree(""); + } + if (!_model->getIfMultipleShips()) { + int j = ui->arrivalTree->select_sexp_node; + if (j != -1) { + ui->arrivalTree->hilite_item(j); + } + } } else { ui->arrivalTree->clear_tree(""); } - if (!_model->getIfMultipleShips()) { - int j = ui->arrivalTree->select_sexp_node; - if (j != -1) { - ui->arrivalTree->hilite_item(j); - } - } - } else { - ui->arrivalTree->clear_tree(""); - } - ui->noArrivalWarpCheckBox->setChecked(_model->getNoArrivalWarp()); + ui->noArrivalWarpCheckBox->setChecked(_model->getNoArrivalWarp()); + } } -void ShipEditorDialog::updateDeparture() +void ShipEditorDialog::updateDeparture(bool overwrite) { util::SignalBlockers blockers(this); - auto idx = _model->getDepartureLocationIndex(); - int i; - ui->departureLocationCombo->clear(); - for (i = 0; i < MAX_DEPARTURE_NAMES; i++) { - ui->departureLocationCombo->addItem(Departure_location_names[i], QVariant(i)); + if (overwrite) { + auto idx = _model->getDepartureLocationIndex(); + int i; + ui->departureLocationCombo->clear(); + for (i = 0; i < MAX_DEPARTURE_NAMES; i++) { + ui->departureLocationCombo->addItem(Departure_location_names[i], QVariant(i)); + } + ui->departureLocationCombo->setCurrentIndex(ui->departureLocationCombo->findData(idx)); } - ui->departureLocationCombo->setCurrentIndex(ui->departureLocationCombo->findData(idx)); object* objp; ui->departureTargetCombo->clear(); @@ -459,34 +350,35 @@ void ShipEditorDialog::updateDeparture() } } ui->departureTargetCombo->setCurrentIndex(ui->departureTargetCombo->findData(_model->getDepartureTarget())); + if (overwrite) { + ui->departureDelaySpinBox->setValue(_model->getDepartureDelay()); - ui->departureDelaySpinBox->setValue(_model->getDepartureDelay()); - - ui->departureTree->initializeEditor(_viewport->editor, this); - if (_model->getNumSelectedShips()) { + ui->departureTree->initializeEditor(_viewport->editor, this); + if (_model->getNumSelectedShips()) { - if (_model->getIfMultipleShips()) { - ui->departureTree->clear_tree(""); - } - if (_model->getUseCue()) { - ui->departureTree->load_tree(_model->getDepartureFormula(), "false"); + if (_model->getIfMultipleShips()) { + ui->departureTree->clear_tree(""); + } + if (_model->getUseCue()) { + ui->departureTree->load_tree(_model->getDepartureFormula(), "false"); + } else { + ui->departureTree->clear_tree(""); + } + if (!_model->getIfMultipleShips()) { + auto i = ui->arrivalTree->select_sexp_node; + if (i != -1) { + i = ui->departureTree->select_sexp_node; + ui->departureTree->hilite_item(i); + } + } } else { ui->departureTree->clear_tree(""); } - if (!_model->getIfMultipleShips()) { - i = ui->arrivalTree->select_sexp_node; - if (i != -1) { - i = ui->departureTree->select_sexp_node; - ui->departureTree->hilite_item(i); - } - } - } else { - ui->departureTree->clear_tree(""); - } - ui->noDepartureWarpCheckBox->setChecked(_model->getNoDepartureWarp()); + ui->noDepartureWarpCheckBox->setChecked(_model->getNoDepartureWarp()); - ui->updateDepartureCueCheckBox->setChecked(_model->getDepartureCue()); + ui->updateDepartureCueCheckBox->setChecked(_model->getDepartureCue()); + } } // Enables disbales controls based on what is selected void ShipEditorDialog::enableDisable() @@ -601,6 +493,11 @@ void ShipEditorDialog::enableDisable() ui->playerShipCheckBox->setEnabled(false); else ui->playerShipCheckBox->setEnabled(true); + if (The_mission.game_type & MISSION_TYPE_MULTI) { + ui->respawnSpinBox->setEnabled(_model->getUIEnable()); + } else { + ui->respawnSpinBox->setEnabled(false); + } // show the "set player" button only if single player if (!(The_mission.game_type & MISSION_TYPE_MULTI)) @@ -660,50 +557,6 @@ void ShipEditorDialog::enableDisable() this->setWindowTitle("Edit Ship"); } } - -/*--------------------------------------------------------- - WARNING -Do not try to optimise string entries; this convoluted method is necessary to avoid fatal errors caused by QT ------------------------------------------------------------*/ -void ShipEditorDialog::shipNameChanged() -{ - const QString entry = ui->shipNameEdit->text(); - if (!entry.isEmpty() && entry != _model->getShipName().c_str()) { - const auto textBytes = entry.toUtf8(); - const std::string NewShipName = textBytes.toStdString(); - _model->setShipName(NewShipName); - } - - // automatically determine or reset the display name - _model->setShipDisplayName(Editor::get_display_name_for_text_box(_model->getShipName())); - - // sync the variable to the edit box - ui->shipDisplayNameEdit->setText(_model->getShipDisplayName().c_str()); -} -void ShipEditorDialog::shipDisplayNameChanged() -{ - const QString entry = ui->shipDisplayNameEdit->text(); - if (entry != _model->getShipDisplayName().c_str()) { - const auto textBytes = entry.toUtf8(); - const std::string NewShipDisplayName = textBytes.toStdString(); - _model->setShipDisplayName(NewShipDisplayName); - } -} -void ShipEditorDialog::shipClassChanged(const int index) -{ - auto shipClassIdx = ui->shipClassCombo->itemData(index).value(); - _model->setShipClass(shipClassIdx); -} -void ShipEditorDialog::aiClassChanged(const int index) -{ - auto aiClassIdx = ui->AIClassCombo->itemData(index).value(); - _model->setAIClass(aiClassIdx); -} -void ShipEditorDialog::teamChanged(const int index) -{ - auto teamIdx = ui->teamCombo->itemData(index).value(); - _model->setTeam(teamIdx); -} void ShipEditorDialog::cargoChanged() { const QString entry = ui->cargoCombo->lineEdit()->text(); @@ -731,87 +584,6 @@ void ShipEditorDialog::callsignChanged() _model->setCallsign(newCallsign); } } -void ShipEditorDialog::hotkeyChanged(const int index) -{ - auto hotkeyIdx = ui->hotkeyCombo->itemData(index).value(); - _model->setHotkey(hotkeyIdx); -} -void ShipEditorDialog::personaChanged(const int index) -{ - auto personaIdx = ui->personaCombo->itemData(index).value(); - _model->setPersona(personaIdx); -} -void ShipEditorDialog::scoreChanged(const int value) -{ - _model->setScore(value); -} -void ShipEditorDialog::assistChanged(const int value) -{ - _model->setAssist(value); -} -void ShipEditorDialog::playerChanged(const bool enabled) -{ - _model->setPlayer(enabled); -} - -void ShipEditorDialog::arrivalLocationChanged(const int index) -{ - auto arrivalLocationIdx = ui->arrivalLocationCombo->itemData(index).value(); - _model->setArrivalLocationIndex(arrivalLocationIdx); -} - -void ShipEditorDialog::arrivalTargetChanged(const int index) -{ - auto arrivalLocationIdx = ui->arrivalTargetCombo->itemData(index).value(); - _model->setArrivalTarget(arrivalLocationIdx); -} - -void ShipEditorDialog::arrivalDistanceChanged(const int value) -{ - _model->setArrivalDistance(value); -} - -void ShipEditorDialog::arrivalDelayChanged(const int value) -{ - _model->setArrivalDelay(value); -} - -void ShipEditorDialog::arrivalWarpChanged(const bool enable) -{ - _model->setNoArrivalWarp(enable); -} - -void ShipEditorDialog::ArrivalCueChanged(const bool value) -{ - _model->setArrivalCue(value); -} - -void ShipEditorDialog::departureLocationChanged(const int index) -{ - auto depLocationIdx = ui->departureLocationCombo->itemData(index).value(); - _model->setDepartureLocationIndex(depLocationIdx); -} - -void ShipEditorDialog::departureTargetChanged(const int index) -{ - auto depLocationIdx = ui->departureTargetCombo->itemData(index).value(); - _model->setDepartureTarget(depLocationIdx); -} - -void ShipEditorDialog::departureDelayChanged(const int value) -{ - _model->setDepartureDelay(value); -} - -void ShipEditorDialog::departureWarpChanged(const bool value) -{ - _model->setNoDepartureWarp(value); -} - -void ShipEditorDialog::DepartureCueChanged(const bool value) -{ - _model->setDepartureCue(value); -} void ShipEditorDialog::on_textureReplacementButton_clicked() { @@ -853,7 +625,7 @@ void ShipEditorDialog::on_weaponsButton_clicked() dialog->show(); } void ShipEditorDialog::on_playerOrdersButton_clicked() - { +{ auto dialog = new dialogs::PlayerOrdersDialog(this, _viewport, getIfMultipleShips()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); @@ -888,8 +660,6 @@ void ShipEditorDialog::on_restrictArrivalPathsButton_clicked() auto dialog = new dialogs::ShipPathsDialog(this, _viewport, _model->getSingleShip(), target_class, false); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); - - } void ShipEditorDialog::on_customWarpinButton_clicked() { @@ -910,6 +680,145 @@ void ShipEditorDialog::on_customWarpoutButton_clicked() dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file + +/*--------------------------------------------------------- + WARNING +Do not try to optimise string entries; this convoluted method is necessary to avoid fatal errors caused by QT +-----------------------------------------------------------*/ +void ShipEditorDialog::on_shipNameEdit_editingFinished() +{ + const QString entry = ui->shipNameEdit->text(); + if (!entry.isEmpty() && entry != _model->getShipName().c_str()) { + const auto textBytes = entry.toUtf8(); + const std::string NewShipName = textBytes.toStdString(); + _model->setShipName(NewShipName); + } + + // automatically determine or reset the display name + _model->setShipDisplayName(Editor::get_display_name_for_text_box(_model->getShipName())); + + // sync the variable to the edit box + ui->shipDisplayNameEdit->setText(_model->getShipDisplayName().c_str()); +} +void ShipEditorDialog::on_shipDisplayNameEdit_editingFinished() +{ + const QString entry = ui->shipDisplayNameEdit->text(); + if (entry != _model->getShipDisplayName().c_str()) { + const auto textBytes = entry.toUtf8(); + const std::string NewShipDisplayName = textBytes.toStdString(); + _model->setShipDisplayName(NewShipDisplayName); + } +} +void ShipEditorDialog::on_shipClassCombo_currentIndexChanged(int index) +{ + auto shipClassIdx = ui->shipClassCombo->itemData(index).toInt(); + _model->setShipClass(shipClassIdx); +} +void ShipEditorDialog::on_AIClassCombo_currentIndexChanged(int index) +{ + auto aiClassIdx = ui->AIClassCombo->itemData(index).toInt(); + _model->setAIClass(aiClassIdx); +} +void ShipEditorDialog::on_teamCombo_currentIndexChanged(int index) +{ + auto teamIdx = ui->teamCombo->itemData(index).toInt(); + _model->setTeam(teamIdx); +} +void ShipEditorDialog::on_hotkeyCombo_currentIndexChanged(int index) +{ + auto hotkeyIdx = ui->hotkeyCombo->itemData(index).toInt(); + _model->setHotkey(hotkeyIdx); +} +void ShipEditorDialog::on_personaCombo_currentIndexChanged(int index) +{ + auto personaIdx = ui->personaCombo->itemData(index).toInt(); + _model->setPersona(personaIdx); +} +void ShipEditorDialog::on_killScoreEdit_valueChanged(int value) +{ + _model->setScore(value); +} +void ShipEditorDialog::on_assistEdit_valueChanged(int value) +{ + _model->setAssist(value); +} +void ShipEditorDialog::on_playerShipCheckBox_toggled(bool value) +{ + _model->setPlayer(value); +} +void ShipEditorDialog::on_respawnSpinBox_valueChanged(int value) { + _model->setRespawn(value); +} +void ShipEditorDialog::on_arrivalLocationCombo_currentIndexChanged(int index) +{ + auto arrivalLocationIdx = ui->arrivalLocationCombo->itemData(index).toInt(); + _model->setArrivalLocationIndex(arrivalLocationIdx); +} +void ShipEditorDialog::on_arrivalTargetCombo_currentIndexChanged(int index) +{ + auto arrivalLocationIdx = ui->arrivalTargetCombo->itemData(index).toInt(); + _model->setArrivalTarget(arrivalLocationIdx); +} +void ShipEditorDialog::on_arrivalDistanceEdit_valueChanged(int value) +{ + _model->setArrivalDistance(value); +} +void ShipEditorDialog::on_arrivalDelaySpinBox_valueChanged(int value) +{ + _model->setArrivalDelay(value); +} +void ShipEditorDialog::on_updateArrivalCueCheckBox_toggled(bool value) +{ + _model->setArrivalCue(value); +} +void ShipEditorDialog::on_noArrivalWarpCheckBox_toggled(bool value) +{ + _model->setNoArrivalWarp(value); +} +void ShipEditorDialog::on_arrivalTree_rootNodeFormulaChanged(int old, int node) +{ + _model->setArrivalFormula(old, node); +} +void ShipEditorDialog::on_arrivalTree_helpChanged(const QString& help) +{ + ui->helpText->setPlainText(help); +} +void ShipEditorDialog::on_arrivalTree_miniHelpChanged(const QString& help) +{ + ui->HelpTitle->setText(help); +} +void ShipEditorDialog::on_departureLocationCombo_currentIndexChanged(int index) +{ + auto depLocationIdx = ui->departureLocationCombo->itemData(index).toInt(); + _model->setDepartureLocationIndex(depLocationIdx); +} +void fred::dialogs::ShipEditorDialog::on_departureTargetCombo_currentIndexChanged(int index) +{ + auto depLocationIdx = ui->departureTargetCombo->itemData(index).toInt(); + _model->setDepartureTarget(depLocationIdx); +} +void fred::dialogs::ShipEditorDialog::on_departureDelaySpinBox_valueChanged(int value) +{ + _model->setDepartureDelay(value); +} +void fred::dialogs::ShipEditorDialog::on_updateDepartureCueCheckBox_toggled(bool value) +{ + _model->setDepartureCue(value); +} +void fred::dialogs::ShipEditorDialog::on_departureTree_rootNodeFormulaChanged(int old, int node) +{ + _model->setDepartureFormula(old, node); +} +void fred::dialogs::ShipEditorDialog::on_departureTree_helpChanged(const QString& help) +{ + ui->helpText->setPlainText(help); +} +void fred::dialogs::ShipEditorDialog::on_departureTree_miniHelpChanged(const QString& help) +{ + ui->HelpTitle->setText(help); +} +void fred::dialogs::ShipEditorDialog::on_noDepartureWarpCheckBox_toggled(bool value) +{ + _model->setNoDepartureWarp(value); +} +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h index 96ceb66d27a..2c0ac5e8106 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h @@ -1,35 +1,35 @@ #ifndef SHIPDEDITORDIALOG_H #define SHIPDEDITORDIALOG_H -#include -#include -#include +#include "PlayerOrdersDialog.h" +#include "ShipCustomWarpDialog.h" +#include "ShipFlagsDialog.h" #include "ShipGoalsDialog.h" #include "ShipInitialStatusDialog.h" -#include "ShipFlagsDialog.h" -#include "PlayerOrdersDialog.h" +#include "ShipPathsDialog.h" #include "ShipSpecialStatsDialog.h" -#include "ShipTextureReplacementDialog.h" #include "ShipTBLViewer.h" +#include "ShipTextureReplacementDialog.h" #include "ShipWeaponsDialog.h" #include "ShipPathsDialog.h" #include "ShipCustomWarpDialog.h" #include "ShipAltShipClass.h" -#include +#include +#include +#include +#include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class ShipEditorDialog; } /** -* @brief QTFred's Ship Editor -*/ + * @brief QTFred's Ship Editor + */ class ShipEditorDialog : public QDialog, public SexpTreeEditorInterface { Q_OBJECT @@ -87,6 +87,41 @@ class ShipEditorDialog : public QDialog, public SexpTreeEditorInterface { void on_restrictDeparturePathsButton_clicked(); void on_customWarpoutButton_clicked(); + // column one + void on_shipNameEdit_editingFinished(); + void on_shipDisplayNameEdit_editingFinished(); + void on_shipClassCombo_currentIndexChanged(int); + void on_AIClassCombo_currentIndexChanged(int); + void on_teamCombo_currentIndexChanged(int); + + // column two + void on_hotkeyCombo_currentIndexChanged(int); + void on_personaCombo_currentIndexChanged(int); + void on_killScoreEdit_valueChanged(int); + void on_assistEdit_valueChanged(int); + void on_playerShipCheckBox_toggled(bool); + void on_respawnSpinBox_valueChanged(int); + + //arrival + void on_arrivalLocationCombo_currentIndexChanged(int); + void on_arrivalTargetCombo_currentIndexChanged(int); + void on_arrivalDistanceEdit_valueChanged(int); + void on_arrivalDelaySpinBox_valueChanged(int); + void on_updateArrivalCueCheckBox_toggled(bool); + void on_noArrivalWarpCheckBox_toggled(bool); + void on_arrivalTree_rootNodeFormulaChanged(int, int); + void on_arrivalTree_helpChanged(const QString&); + void on_arrivalTree_miniHelpChanged(const QString&); + + //departure + void on_departureLocationCombo_currentIndexChanged(int); + void on_departureTargetCombo_currentIndexChanged(int); + void on_departureDelaySpinBox_valueChanged(int); + void on_updateDepartureCueCheckBox_toggled(bool); + void on_departureTree_rootNodeFormulaChanged(int, int); + void on_departureTree_helpChanged(const QString&); + void on_departureTree_miniHelpChanged(const QString&); + void on_noDepartureWarpCheckBox_toggled(bool); private: std::unique_ptr ui; std::unique_ptr _model; @@ -94,47 +129,18 @@ class ShipEditorDialog : public QDialog, public SexpTreeEditorInterface { void update(); - void updateUI(); - void updateColumnOne(); - void updateColumnTwo(); - void updateArrival(); - void updateDeparture(); + void updateUI(bool overwrite = false); + void updateColumnOne(bool overwrite = false); + void updateColumnTwo(bool ovewrite = false); + void updateArrival(bool overwrite = false); + void updateDeparture(bool overwrite = false); void enableDisable(); - //column one - void shipNameChanged(); - void shipDisplayNameChanged(); - void shipClassChanged(const int); - void aiClassChanged(const int); - void teamChanged(const int); + // column one void cargoChanged(); void altNameChanged(); void callsignChanged(); - - //column two - void hotkeyChanged(const int); - void personaChanged(const int); - void scoreChanged(const int); - void assistChanged(const int); - void playerChanged(const bool); - - //arrival - void arrivalLocationChanged(const int); - void arrivalTargetChanged(const int); - void arrivalDistanceChanged(const int); - void arrivalDelayChanged(const int); - void arrivalWarpChanged(const bool); - void ArrivalCueChanged(const bool); - - //departure - void departureLocationChanged(const int); - void departureTargetChanged(const int); - void departureDelayChanged(const int); - void departureWarpChanged(const bool); - void DepartureCueChanged(const bool); }; -} // namespace dialogs -} // namespace fred -} // namespace fso +} // namespace fso::fred::dialogs #endif // SHIPDEDITORDIALOG_H \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp index 7272df4816c..19a1818ff44 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.cpp @@ -2,13 +2,12 @@ #include "ui_ShipFlagsDialog.h" +#include #include -#include + #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { ShipFlagsDialog::ShipFlagsDialog(QWidget* parent, EditorViewport* viewport) : QDialog(parent), ui(new Ui::ShipFlagsDialog()), _model(new ShipFlagsDialogModel(this, viewport)), @@ -20,81 +19,24 @@ ShipFlagsDialog::ShipFlagsDialog(QWidget* parent, EditorViewport* viewport) connect(ui->cancelButton, &QPushButton::clicked, this, &ShipFlagsDialog::rejectHandler); connect(this, &QDialog::rejected, _model.get(), &ShipFlagsDialogModel::reject); + // Column One - connect(_model.get(), &AbstractDialogModel::modelChanged, this, &ShipFlagsDialog::updateUI); + connect(ui->flagList, &fso::fred::FlagListWidget::flagToggled, this, [this](const QString& name, bool checked) { + _model->setFlag(name.toUtf8().constData(), checked); + updateUI(); + }); - // Column One - connect(ui->destroyBeforeMissionCheckbox, - &QCheckBox::stateChanged, - this, - &ShipFlagsDialog::destroyBeforeMissionChanged); - connect(ui->destroySecondsSpinBox, - static_cast(&QSpinBox::valueChanged), - this, - &ShipFlagsDialog::destroyBeforeMissionSecondsChanged); - connect(ui->scannableCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::scannableChanged); - connect(ui->cargoKnownCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::cargoChanged); - connect(ui->toggleSubsytemScanningCheckbox, - &QCheckBox::stateChanged, - this, - &ShipFlagsDialog::subsytemScanningChanged); - connect(ui->reinforcementUnitCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::reinforcementChanged); - connect(ui->protectShipCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::protectShipChanged); - connect(ui->beamProtectCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::beamProtectChanged); - connect(ui->flakProtectCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::flakProtectChanged); - connect(ui->laserProtectCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::laserProtectChanged); - connect(ui->missileProtectCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::missileProtectChanged); - connect(ui->ignoreForGoalsCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::ignoreForGoalsChanged); - connect(ui->escortShipCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::escortChanged); - connect(ui->escortPrioritySpinBox, - static_cast(&QSpinBox::valueChanged), - this, - &ShipFlagsDialog::escortValueChanged); - connect(ui->noArrivalMusicCheckBox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::noArrivalMusicChanged); - connect(ui->invulnerableCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::invulnerableChanged); - connect(ui->guardianedCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::guardianedChanged); - connect(ui->primitiveCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::primitiveChanged); - connect(ui->noSubspaceDriveCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::noSubspaceChanged); - connect(ui->hiddenFromSensorsCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::hiddenChanged); - connect(ui->stealthCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::stealthChanged); - connect(ui->friendlyStealthCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::friendlyStealthChanged); - connect(ui->kamikazeCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::kamikazeChanged); - connect(ui->kamikazeDamageSpinBox, - static_cast(&QSpinBox::valueChanged), - this, - &ShipFlagsDialog::kamikazeDamageChanged); - connect(ui->noChangePosCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::doesNotChangePositionChanged); - connect(ui->noChangeOrientCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::doesNotChangeOrientationChanged); + const auto flags = _model->getFlagsList(); - // Column Two - connect(ui->noDynamicGoalsCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::noDynamicGoalsChanged); - connect(ui->redAlertCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::redAlertChanged); - connect(ui->gravityCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::gravityChanged); - connect(ui->specialWarpinCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::warpinChanged); - connect(ui->targetableAsBombCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::targetableAsBombChanged); - connect(ui->disableBuiltInMessagesCheckbox, - &QCheckBox::stateChanged, - this, - &ShipFlagsDialog::disableBuiltInMessagesChanged); - connect(ui->neverScreamCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::neverScreamChanged); - connect(ui->alwaysScreamCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::alwaysScreamChanged); - connect(ui->vaporizeCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::vaporizeChanged); - connect(ui->respawnPrioritySpinBox, - static_cast(&QSpinBox::valueChanged), - this, - &ShipFlagsDialog::respawnPriorityChanged); - connect(ui->autoCarryCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::autoCarryChanged); - connect(ui->autoLinkCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::autoLinkChanged); - connect(ui->hideShipNameCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::hideShipNameChanged); - connect(ui->classDynamicCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::classDynamicChanged); - connect(ui->disableETSCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::disableETSChanged); - connect(ui->cloakCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::cloakChanged); - connect(ui->scrambleMessagesCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::scrambleMessagesChanged); - connect(ui->noCollideCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::noCollideChanged); - connect(ui->noSelfDestructCheckbox, &QCheckBox::stateChanged, this, &ShipFlagsDialog::noSelfDestructChanged); + QVector> toWidget; + toWidget.reserve(static_cast(flags.size())); + for (const auto& p : flags) { + QString name = QString::fromUtf8(p.first.c_str()); + toWidget.append({name, p.second}); + } + ui->flagList->setFlags(toWidget); updateUI(); - // Resize the dialog to the minimum size resize(QDialog::sizeHint()); } @@ -111,372 +53,32 @@ void ShipFlagsDialog::rejectHandler() { this->close(); } -void ShipFlagsDialog::updateUI() -{ - util::SignalBlockers blockers(this); - - // Column One - // Destroy before mission - auto value = _model->getDestroyed(); - ui->destroyBeforeMissionCheckbox->setCheckState(Qt::CheckState(value)); - value = _model->getDestroyedSeconds(); - ui->destroySecondsSpinBox->setValue(value); - // Scannable - value = _model->getScannable(); - ui->scannableCheckbox->setCheckState(Qt::CheckState(value)); - // Cargo known - value = _model->getCargoKnown(); - ui->cargoKnownCheckbox->setCheckState(Qt::CheckState(value)); - // Toggle Subsytem Sacnning - value = _model->getSubsystemScanning(); - ui->toggleSubsytemScanningCheckbox->setCheckState(Qt::CheckState(value)); - // Reinforcement - value = _model->getReinforcment(); - ui->reinforcementUnitCheckbox->setCheckState(Qt::CheckState(value)); - // Protect Flags - value = _model->getProtectShip(); - ui->protectShipCheckbox->setCheckState(Qt::CheckState(value)); - value = _model->getBeamProtect(); - ui->beamProtectCheckbox->setCheckState(Qt::CheckState(value)); - value = _model->getFlakProtect(); - ui->flakProtectCheckbox->setCheckState(Qt::CheckState(value)); - value = _model->getLaserProtect(); - ui->laserProtectCheckbox->setCheckState(Qt::CheckState(value)); - value = _model->getMissileProtect(); - ui->missileProtectCheckbox->setCheckState(Qt::CheckState(value)); - // Ignore For goals - value = _model->getIgnoreForGoals(); - ui->ignoreForGoalsCheckbox->setCheckState(Qt::CheckState(value)); - // Escort - value = _model->getEscort(); - ui->escortShipCheckbox->setCheckState(Qt::CheckState(value)); - value = _model->getEscortValue(); - ui->escortPrioritySpinBox->setValue(value); - // No Arrival Music - value = _model->getNoArrivalMusic(); - ui->noArrivalMusicCheckBox->setCheckState(Qt::CheckState(value)); - // Invulnerable - value = _model->getInvulnerable(); - ui->invulnerableCheckbox->setCheckState(Qt::CheckState(value)); - // Guardiened - value = _model->getGuardianed(); - ui->guardianedCheckbox->setCheckState(Qt::CheckState(value)); - // Pirmitive Sensors - value = _model->getPrimitiveSensors(); - ui->primitiveCheckbox->setCheckState(Qt::CheckState(value)); - // No Subspace Drive - value = _model->getNoSubspaceDrive(); - ui->noSubspaceDriveCheckbox->setCheckState(Qt::CheckState(value)); - // Hidden From Sensors - value = _model->getHidden(); - ui->hiddenFromSensorsCheckbox->setCheckState(Qt::CheckState(value)); - // Stealth - value = _model->getStealth(); - ui->stealthCheckbox->setCheckState(Qt::CheckState(value)); - // Freindly Stealth - value = _model->getFriendlyStealth(); - ui->friendlyStealthCheckbox->setCheckState(Qt::CheckState(value)); - // Kamikaze - value = _model->getKamikaze(); - ui->kamikazeCheckbox->setCheckState(Qt::CheckState(value)); - value = _model->getKamikazeDamage(); - ui->kamikazeDamageSpinBox->setValue(value); - // Does Not Change Position - value = _model->getDontChangePosition(); - ui->noChangePosCheckbox->setCheckState(Qt::CheckState(value)); - // Does Not Change Orientation - value = _model->getDontChangeOrientation(); - ui->noChangeOrientCheckbox->setCheckState(Qt::CheckState(value)); - // Column Two - // No Dynamic Goals - value = _model->getNoDynamicGoals(); - ui->noDynamicGoalsCheckbox->setCheckState(Qt::CheckState(value)); - // Red Alert Carry - value = _model->getRedAlert(); - ui->redAlertCheckbox->setCheckState(Qt::CheckState(value)); - // Affected By Gravity - value = _model->getGravity(); - ui->gravityCheckbox->setCheckState(Qt::CheckState(value)); - // Special Warpin - value = _model->getWarpin(); - ui->specialWarpinCheckbox->setCheckState(Qt::CheckState(value)); - // Targetable As Bomb - value = _model->getTargetableAsBomb(); - ui->targetableAsBombCheckbox->setCheckState(Qt::CheckState(value)); - // Disable Built-in Messages - value = _model->getDisableBuiltInMessages(); - ui->disableBuiltInMessagesCheckbox->setCheckState(Qt::CheckState(value)); - // Never Scream On Death - value = _model->getNeverScream(); - ui->neverScreamCheckbox->setCheckState(Qt::CheckState(value)); - // Always Scream on Death - value = _model->getAlwaysScream(); - ui->alwaysScreamCheckbox->setCheckState(Qt::CheckState(value)); - // Vaporize on Death - value = _model->getVaporize(); - ui->vaporizeCheckbox->setCheckState(Qt::CheckState(value)); - // Respawn - if (The_mission.game_type & MISSION_TYPE_MULTI) { - ui->respawnPrioritySpinBox->setEnabled(true); - } else { - ui->respawnPrioritySpinBox->setEnabled(false); - } - value = _model->getRespawnPriority(); - ui->respawnPrioritySpinBox->setValue(value); - // AutoNav Carry Status - value = _model->getAutoCarry(); - ui->autoCarryCheckbox->setCheckState(Qt::CheckState(value)); - // AutoNav Needs Link - value = _model->getAutoLink(); - ui->autoLinkCheckbox->setCheckState(Qt::CheckState(value)); - // Hide Ship Name - value = _model->getHideShipName(); - ui->hideShipNameCheckbox->setCheckState(Qt::CheckState(value)); - // Set Class Dynamically - value = _model->getClassDynamic(); - ui->classDynamicCheckbox->setCheckState(Qt::CheckState(value)); - //Disable ETS - value = _model->getDisableETS(); - ui->disableETSCheckbox->setCheckState(Qt::CheckState(value)); - //Cloaked - value = _model->getCloak(); - ui->cloakCheckbox->setCheckState(Qt::CheckState(value)); - //Scramble Messages - value = _model->getScrambleMessages(); - ui->scrambleMessagesCheckbox->setCheckState(Qt::CheckState(value)); - //No Collisions - value = _model->getNoCollide(); - ui->noCollideCheckbox->setCheckState(Qt::CheckState(value)); - //No Disabled Self-Destruct - value = _model->getNoSelfDestruct(); - ui->noSelfDestructCheckbox->setCheckState(Qt::CheckState(value)); -} - -void ShipFlagsDialog::destroyBeforeMissionChanged(int value) -{ - _model->setDestroyed(value); -} - -void ShipFlagsDialog::destroyBeforeMissionSecondsChanged(int value) -{ - _model->setDestroyedSeconds(value); -} - -void ShipFlagsDialog::scannableChanged(int value) -{ - _model->setScannable(value); -} - -void ShipFlagsDialog::cargoChanged(int value) -{ - _model->setCargoKnown(value); -} - -void ShipFlagsDialog::subsytemScanningChanged(int value) -{ - _model->setSubsystemScanning(value); -} - -void ShipFlagsDialog::reinforcementChanged(int value) -{ - _model->setReinforcment(value); -} - -void ShipFlagsDialog::protectShipChanged(int value) -{ - _model->setProtectShip(value); -} - -void ShipFlagsDialog::beamProtectChanged(int value) -{ - _model->setBeamProtect(value); -} - -void ShipFlagsDialog::flakProtectChanged(int value) -{ - _model->setFlakProtect(value); -} - -void ShipFlagsDialog::laserProtectChanged(int value) -{ - _model->setLaserProtect(value); -} - -void ShipFlagsDialog::missileProtectChanged(int value) -{ - _model->setMissileProtect(value); -} - -void ShipFlagsDialog::ignoreForGoalsChanged(int value) -{ - _model->setIgnoreForGoals(value); -} - -void ShipFlagsDialog::escortChanged(int value) -{ - _model->setEscort(value); -} - -void ShipFlagsDialog::escortValueChanged(int value) -{ - _model->setEscortValue(value); -} - -void ShipFlagsDialog::noArrivalMusicChanged(int value) -{ - _model->setNoArrivalMusic(value); -} - -void ShipFlagsDialog::invulnerableChanged(int value) -{ - _model->setInvulnerable(value); -} - -void ShipFlagsDialog::guardianedChanged(int value) -{ - _model->setGuardianed(value); -} - -void ShipFlagsDialog::primitiveChanged(int value) -{ - _model->setPrimitiveSensors(value); -} - -void ShipFlagsDialog::noSubspaceChanged(int value) -{ - _model->setNoSubspaceDrive(value); -} - -void ShipFlagsDialog::hiddenChanged(int value) -{ - _model->setHidden(value); -} - -void ShipFlagsDialog::stealthChanged(int value) +void ShipFlagsDialog::on_destroySecondsSpinBox_valueChanged(int value) { - _model->setStealth(value); + _model->setDestroyTime(value); } - -void ShipFlagsDialog::friendlyStealthChanged(int value) +void ShipFlagsDialog::on_escortPrioritySpinBox_valueChanged(int value) { - _model->setFriendlyStealth(value); + _model->setEscortPriority(value); } - -void ShipFlagsDialog::kamikazeChanged(int value) -{ - _model->setKamikaze(value); -} - -void ShipFlagsDialog::kamikazeDamageChanged(int value) +void ShipFlagsDialog::on_kamikazeDamageSpinBox_valueChanged(int value) { _model->setKamikazeDamage(value); } - -void ShipFlagsDialog::doesNotChangePositionChanged(int value) -{ - _model->setDontChangePosition(value); -} - -void ShipFlagsDialog::doesNotChangeOrientationChanged(int value) -{ - _model->setDontChangeOrientation(value); -} - -void ShipFlagsDialog::noDynamicGoalsChanged(int value) -{ - _model->setNoDynamicGoals(value); -} - -void ShipFlagsDialog::redAlertChanged(int value) -{ - _model->setRedAlert(value); -} - -void ShipFlagsDialog::gravityChanged(int value) -{ - _model->setGravity(value); -} - -void ShipFlagsDialog::warpinChanged(int value) -{ - _model->setWarpin(value); -} - -void ShipFlagsDialog::targetableAsBombChanged(int value) -{ - _model->setTargetableAsBomb(value); -} - -void ShipFlagsDialog::disableBuiltInMessagesChanged(int value) -{ - _model->setDisableBuiltInMessages(value); -} - -void ShipFlagsDialog::neverScreamChanged(int value) -{ - _model->setNeverScream(value); -} - -void ShipFlagsDialog::alwaysScreamChanged(int value) -{ - _model->setAlwaysScream(value); -} - -void ShipFlagsDialog::vaporizeChanged(int value) -{ - _model->setVaporize(value); -} - -void ShipFlagsDialog::respawnPriorityChanged(int value) -{ - _model->setRespawnPriority(value); -} - -void ShipFlagsDialog::autoCarryChanged(int value) -{ - _model->setAutoCarry(value); -} - -void ShipFlagsDialog::autoLinkChanged(int value) -{ - _model->setAutoLink(value); -} - -void ShipFlagsDialog::hideShipNameChanged(int value) -{ - _model->setHideShipName(value); -} - -void ShipFlagsDialog::classDynamicChanged(int value) -{ - _model->setClassDynamic(value); -} - -void ShipFlagsDialog::disableETSChanged(int value) -{ - _model->setDisableETS(value); -} - -void ShipFlagsDialog::cloakChanged(int value) -{ - _model->setCloak(value); -} - -void ShipFlagsDialog::scrambleMessagesChanged(int value) +void ShipFlagsDialog::updateUI() { - _model->setScrambleMessages(value); -} + util::SignalBlockers blockers(this); + ui->destroySecondsSpinBox->setValue(_model->getDestroyTime()); + ui->destroyedlabel->setVisible(_model->getFlag("Destroy before Mission")->second); + ui->destroySecondsSpinBox->setVisible(_model->getFlag("Destroy before Mission")->second); + ui->destroySecondsLabel->setVisible(_model->getFlag("Destroy before Mission")->second); -void ShipFlagsDialog::noCollideChanged(int value) -{ - _model->setNoCollide(value); -} + ui->escortPrioritySpinBox->setValue(_model->getEscortPriority()); + ui->escortLabel->setVisible(_model->getFlag("escort")->second); + ui->escortPrioritySpinBox->setVisible(_model->getFlag("escort")->second); -void ShipFlagsDialog::noSelfDestructChanged(int value) -{ - _model->setNoSelfDestruct(value); + ui->kamikazeDamageSpinBox->setValue(_model->getKamikazeDamage()); + ui->kamikazeLabel->setVisible(_model->getFlag("kamikaze")->second); + ui->kamikazeDamageSpinBox->setVisible(_model->getFlag("kamikaze")->second); } - -} // namespace dialogs -} // namespace fred -} // namespace fso \ No newline at end of file +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.h index 8a828457195..f990181e96f 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipFlagsDialog.h @@ -3,11 +3,10 @@ #include #include + #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class ShipFlagsDialog; @@ -23,61 +22,17 @@ class ShipFlagsDialog : public QDialog { void closeEvent(QCloseEvent*) override; void rejectHandler(); + private slots: + void on_destroySecondsSpinBox_valueChanged(int); + void on_escortPrioritySpinBox_valueChanged(int); + void on_kamikazeDamageSpinBox_valueChanged(int); - private: + private: //NOLINT std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; void updateUI(); - - void destroyBeforeMissionChanged(int); - void destroyBeforeMissionSecondsChanged(int); - void scannableChanged(int); - void cargoChanged(int); - void subsytemScanningChanged(int); - void reinforcementChanged(int); - void protectShipChanged(int); - void beamProtectChanged(int); - void flakProtectChanged(int); - void laserProtectChanged(int); - void missileProtectChanged(int); - void ignoreForGoalsChanged(int); - void escortChanged(int); - void escortValueChanged(int); - void noArrivalMusicChanged(int); - void invulnerableChanged(int); - void guardianedChanged(int); - void primitiveChanged(int); - void noSubspaceChanged(int); - void hiddenChanged(int); - void stealthChanged(int); - void friendlyStealthChanged(int); - void kamikazeChanged(int); - void kamikazeDamageChanged(int); - void doesNotChangePositionChanged(int); - void doesNotChangeOrientationChanged(int); - void noDynamicGoalsChanged(int); - void redAlertChanged(int); - void gravityChanged(int); - void warpinChanged(int); - void targetableAsBombChanged(int); - void disableBuiltInMessagesChanged(int); - void neverScreamChanged(int); - void alwaysScreamChanged(int); - void vaporizeChanged(int); - void respawnPriorityChanged(int); - void autoCarryChanged(int); - void autoLinkChanged(int); - void hideShipNameChanged(int); - void classDynamicChanged(int); - void disableETSChanged(int); - void cloakChanged(int); - void scrambleMessagesChanged(int); - void noCollideChanged(int); - void noSelfDestructChanged(int); - }; -} // namespace dialogs -} // namespace fred -} // namespace fso +}; +} // namespace fso::fred::dialogs #endif // !SHIPFLAGDIALOG_H \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.cpp index 47595309037..d54923ecdc5 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.cpp +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.cpp @@ -91,6 +91,10 @@ namespace fso { { this->close(); } + void ShipInitialStatusDialog::on_guardianSpinBox_valueChanged(int value) + { + _model->setGuardian(value); + } void ShipInitialStatusDialog::updateUI() { util::SignalBlockers blockers(this); @@ -123,6 +127,7 @@ namespace fso { else { ui->shieldHullSpinBox->setSpecialValueText("-"); } + ui->guardianSpinBox->setValue(_model->getGuardian()); updateFlags(); updateDocks(); updateDockee(); diff --git a/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.h b/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.h index b0b4420492c..0ec9e85ca9f 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipInitialStatusDialog.h @@ -23,7 +23,9 @@ class ShipInitialStatusDialog : public QDialog { void closeEvent(QCloseEvent*) override; void rejectHandler(); - private: + private slots: + void on_guardianSpinBox_valueChanged(int); + private://NOLINT std::unique_ptr ui; std::unique_ptr _model; EditorViewport* _viewport; diff --git a/qtfred/src/ui/widgets/FlagList.cpp b/qtfred/src/ui/widgets/FlagList.cpp index 152ecd22c39..8ba216d917f 100644 --- a/qtfred/src/ui/widgets/FlagList.cpp +++ b/qtfred/src/ui/widgets/FlagList.cpp @@ -86,7 +86,7 @@ void FlagListWidget::connectSignals() connect(_btnNone, &QToolButton::clicked, this, &FlagListWidget::onClearAll); } -void FlagListWidget::setFlags(const QVector>& flags) +void FlagListWidget::setFlags(const QVector>& flags) { rebuildModel(flags); } @@ -101,7 +101,7 @@ void FlagListWidget::setFlagDescriptions(const QVector>& flags) +void FlagListWidget::rebuildModel(const QVector>& flags) { _updating = true; @@ -113,11 +113,11 @@ void FlagListWidget::rebuildModel(const QVector>& flags _model->insertRows(0, flags.size()); for (int i = 0; i < flags.size(); ++i) { const auto& name = flags[i].first; - const bool checked = flags[i].second; + const auto checked = flags[i].second; auto* item = new QStandardItem(name); item->setCheckable(true); - item->setCheckState(checked ? Qt::Checked : Qt::Unchecked); + item->setCheckState(Qt::CheckState(checked)); item->setData(name, KeyRole); // If we have a description for this flag, set it as tooltip @@ -148,7 +148,7 @@ void FlagListWidget::applyTooltipsToItems() } } -QVector> FlagListWidget::getFlags() const +QVector> FlagListWidget::getFlags() const { return snapshot(); } @@ -193,7 +193,7 @@ void FlagListWidget::onItemChanged(QStandardItem* item) return; const auto name = item->data(KeyRole).toString(); - const bool checked = (item->checkState() == Qt::Checked); + const auto checked = item->checkState(); Q_EMIT flagToggled(name, checked); Q_EMIT flagsChanged(snapshot()); @@ -232,14 +232,14 @@ void FlagListWidget::onClearAll() Q_EMIT flagsChanged(snapshot()); } -QVector> FlagListWidget::snapshot() const +QVector> FlagListWidget::snapshot() const { - QVector> out; + QVector> out; out.reserve(_model->rowCount()); for (int r = 0; r < _model->rowCount(); ++r) { if (auto* it = _model->item(r, 0)) { const auto key = it->data(KeyRole).toString(); - const bool checked = (it->checkState() == Qt::Checked); + const Qt::CheckState checked = it->checkState(); out.append({key, checked}); } } diff --git a/qtfred/src/ui/widgets/FlagList.h b/qtfred/src/ui/widgets/FlagList.h index 7bcce924937..e681a8e5f4f 100644 --- a/qtfred/src/ui/widgets/FlagList.h +++ b/qtfred/src/ui/widgets/FlagList.h @@ -25,13 +25,13 @@ class FlagListWidget final : public QWidget { explicit FlagListWidget(QWidget* parent = nullptr); ~FlagListWidget() override; - void setFlags(const QVector>& flags); + void setFlags(const QVector>& flags); // Optionally set descriptions void setFlagDescriptions(const QVector>& descriptions); // Read back the entire list and their checked states - QVector> getFlags() const; + QVector> getFlags() const; // Optional UI controls void setFilterVisible(bool visible); @@ -45,9 +45,9 @@ class FlagListWidget final : public QWidget { signals: // Emitted whenever a checkbox is toggled - void flagToggled(const QString& name, bool checked); + void flagToggled(const QString& name, int checked); // Emitted after any change that alters the entire set - void flagsChanged(const QVector>& flags); + void flagsChanged(const QVector>& flags); private slots: void onItemChanged(QStandardItem* item); @@ -62,9 +62,9 @@ class FlagListWidget final : public QWidget { void buildUi(); void connectSignals(); - void rebuildModel(const QVector>& flags); + void rebuildModel(const QVector>& flags); void applyTooltipsToItems(); - QVector> snapshot() const; + QVector> snapshot() const; QLineEdit* _filter = nullptr; QToolButton* _btnAll = nullptr; diff --git a/qtfred/src/ui/widgets/PersonaColorComboBox.cpp b/qtfred/src/ui/widgets/PersonaColorComboBox.cpp new file mode 100644 index 00000000000..a4edd22f6ed --- /dev/null +++ b/qtfred/src/ui/widgets/PersonaColorComboBox.cpp @@ -0,0 +1,41 @@ +#include "PersonaColorComboBox.h" +#include +#include + +namespace fso::fred { +PersonaColorComboBox::PersonaColorComboBox(QWidget* parent) : QComboBox(parent) +{ + fredApp->runAfterInit([this]() { setModel(getPersonaModel()); }); +} +QStandardItemModel* PersonaColorComboBox::getPersonaModel() +{ + auto itemModel = new QStandardItemModel(); + auto topitem = new QStandardItem(""); + topitem->setData(-1, Qt::UserRole); + itemModel->appendRow(topitem); + for (size_t i = 0; i < Personas.size(); i++) { + if (Personas[i].flags & PERSONA_FLAG_WINGMAN) { + SCP_string persona_name = Personas[i].name; + + // see if the bitfield matches one and only one species + int species = -1; + for (size_t j = 0; j < 32 && j < Species_info.size(); j++) { + if (Personas[i].species_bitfield == (1 << j)) { + species = static_cast(j); + break; + } + } + auto item = new QStandardItem(persona_name.c_str()); + // if it is an exact species that isn't the first + if (species >= 0) { + species_info* sinfo = &Species_info[species]; + auto brush = QBrush(QColor(sinfo->fred_color.rgb.r, sinfo->fred_color.rgb.g, sinfo->fred_color.rgb.b)); + item->setData(brush, Qt::ForegroundRole); + item->setData(static_cast(i), Qt::UserRole); + itemModel->appendRow(item); + } + } + } + return itemModel; +} +} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/src/ui/widgets/PersonaColorComboBox.h b/qtfred/src/ui/widgets/PersonaColorComboBox.h new file mode 100644 index 00000000000..23fc91a5bd6 --- /dev/null +++ b/qtfred/src/ui/widgets/PersonaColorComboBox.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include +namespace fso::fred { +class PersonaColorComboBox : public QComboBox { + Q_OBJECT + public: + PersonaColorComboBox(QWidget* parent); + + private: + static QStandardItemModel* getPersonaModel(); +}; +} // namespace fso::fred \ No newline at end of file diff --git a/qtfred/ui/ShipEditorDialog.ui b/qtfred/ui/ShipEditorDialog.ui index fa989e19395..f9eef784f4c 100644 --- a/qtfred/ui/ShipEditorDialog.ui +++ b/qtfred/ui/ShipEditorDialog.ui @@ -7,7 +7,7 @@ 0 0 468 - 688 + 754 @@ -37,24 +37,31 @@ - - + + - Ship Name + Display Name - - + + + + Alt Name + + + + + - <html><head/><body><p>Sets the ship's name</p></body></html> + <html><head/><body><p>Sets the Ship's AI</p></body></html> - - + + - Display Name + Ship Name @@ -65,96 +72,89 @@ - - - - Callsign - - - - - + + - <html><head/><body><p>Sets ship's callsign (Replaces name in messages)</p></body></html> - - - true + <html><head/><body><p>Sets the ship's team</p></body></html> - - + + - Ship Class + Cargo - - - - <html><head/><body><p>Sets the ship's class</p></body></html> + + + + AI Class - - + + - Alt Name + Team - - + + - <html><head/><body><p>Change ship's class name (i.e. GTF Ulysses -&gt; NTF Ulysses)</p></body></html> + <html><head/><body><p>Type to add Ship's Cargo or select previous</p></body></html> true - - + + - AI Class + Callsign - - + + - <html><head/><body><p>Sets the Ship's AI</p></body></html> + <html><head/><body><p>Sets the ship's name</p></body></html> - - - - Team + + + + <html><head/><body><p>Sets ship's callsign (Replaces name in messages)</p></body></html> + + + true - - + + - <html><head/><body><p>Sets the ship's team</p></body></html> + <html><head/><body><p>Change ship's class name (i.e. GTF Ulysses -&gt; NTF Ulysses)</p></body></html> - - - - - - Cargo + + true - - + + - <html><head/><body><p>Type to add Ship's Cargo or select previous</p></body></html> + <html><head/><body><p>Sets the ship's class</p></body></html> - - true + + + + + + Ship Class @@ -176,6 +176,26 @@ + + + + <html><head/><body><p>Wing the current ship is in</p></body></html> + + + Static + + + + + + + <html><head/><body><p>How many points the player gets for an assist</p></body></html> + + + QAbstractSpinBox::NoButtons + + + @@ -183,6 +203,44 @@ + + + + <html><head/><body><p>How many points the player gets for the kill</p></body></html> + + + QAbstractSpinBox::NoButtons + + + + + + + Persona + + + + + + + <html><head/><body><p>Sets the head ani &amp; voice for automated messages</p></body></html> + + + + + + + Hotkey + + + + + + + Assist % + + + @@ -240,23 +298,6 @@ - - - - <html><head/><body><p>Sets the head ani &amp; voice for automated messages</p></body></html> - - - - - - - <html><head/><body><p>How many points the player gets for an assist</p></body></html> - - - QAbstractSpinBox::NoButtons - - - @@ -264,46 +305,15 @@ - - - - <html><head/><body><p>Wing the current ship is in</p></body></html> - - - Static - - - - - - - Persona - - - - - + + - Hotkey + Respawn Priority - - - - <html><head/><body><p>How many points the player gets for the kill</p></body></html> - - - QAbstractSpinBox::NoButtons - - - - - - - Assist % - - + + @@ -878,15 +888,20 @@ + + fso::fred::ShipFlagCheckbox + QCheckBox +
ui/widgets/ShipFlagCheckbox.h
+
fso::fred::sexp_tree QTreeView
ui/widgets/sexp_tree.h
- fso::fred::ShipFlagCheckbox - QCheckBox -
ui/widgets/ShipFlagCheckbox.h
+ fso::fred::PersonaColorComboBox + QComboBox +
ui/widgets/PersonaColorComboBox.h
diff --git a/qtfred/ui/ShipFlagsDialog.ui b/qtfred/ui/ShipFlagsDialog.ui index 9ddb627b582..0808250a90f 100644 --- a/qtfred/ui/ShipFlagsDialog.ui +++ b/qtfred/ui/ShipFlagsDialog.ui @@ -7,7 +7,7 @@ 0 0 421 - 745 + 751 @@ -16,665 +16,131 @@ true - + - - - - - <html><head/><body><p>Destroys the ship before the mission starts</p></body></html> - - - Destroy Before Mission - - - true - - - + + + + 0 + 0 + + + + + 0 + 0 + + + + true + + + true + + + + + - + - - - <html><head/><body><p>How many seconds before the mission to destroy ship</p></body></html> - - + + + + + Destroyed + + + + + + + <html><head/><body><p>How many seconds before the mission to destroy ship</p></body></html> + + + + + + + Seconds before + + + + - - - Seconds - - + + + + + Escort Priority + + + + + + + <html><head/><body><p>How high up the escort list the ship is (Lower number = Higher up list)</p></body></html> + + + QAbstractSpinBox::UpDownArrows + + + + - - - - - - <html><head/><body><p>Whether the ship is scannable or not</p></body></html> - - - Scannable - - - true - - - - - - - <html><head/><body><p>Whether the player alredy knows the cargo or needs to scan for it</p></body></html> - - - Cargo Known - - - true - - - - - - - <html><head/><body><p>Switches between scanning the whole ship or individual subsystems</p></body></html> - - - Toggle Subsystem Scanning - - - true - - - - - - - <html><head/><body><p>Makes the ship available in the reinforcements editor</p></body></html> - - - Reinforcement Unit - - - true - - - - - - - <html><head/><body><p>Stops AI from attacking ship</p></body></html> - - - Protect Ship - - - true - - - - - - - Turret Threats - - - - - - <html><head/><body><p>Stops Beams from targeting this ship</p></body></html> - - - Beam Protect Ship - - - - - - - <html><head/><body><p>Stops flack weapons from targeting this ship</p></body></html> - - - Flak Protect Ship - - - - - - - <html><head/><body><p>Stops blob turrets from targeting this ship</p></body></html> - - - Laser Protect Ship - - - - - - - <html><head/><body><p>Stops missile turrets from targeting this ship</p></body></html> - - - Missile Protect Ship - - - - - - - - - - <html><head/><body><p>takes this ship out of consideration in SEXP operators like <span style=" font-style:italic;">percent-ships-destroyed</span></p></body></html> - - - Ignore for Counting Goals - - - true - - - - - - - <html><head/><body><p>Adds ship to escort list (Also makes active asteroid fields target ship)</p></body></html> - - - Escort Ship - - - true - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 10 - 20 - - - - - - - - Priority - - - - - - - <html><head/><body><p>How high up the escort list the ship is (Lower number = Higher up list)</p></body></html> - - - QAbstractSpinBox::NoButtons - - + + + + + Kamikaze Damage + + + + + + + QAbstractSpinBox::UpDownArrows + + + + - - - <html><head/><body><p>Don't change the music when the ship arrives</p></body></html> - - - No Arrival Music - - - true - - - - - - - <html><head/><body><p>Makes ship take no damage</p></body></html> - - - Invulnerable - - - true - - - - - - - <html><head/><body><p>Ship becomes invulnerable at 1% health. Threshold can be changed with SEXP <span style=" font-style:italic;">set-guardian-threshold</span></p></body></html> - - - Guardianed - - - true - - - - - - - Primitive Sensors - - - true - - - - - - - <html><head/><body><p>Makes Jump out command fail silently, Useful for In-mission jumps</p></body></html> - - - No Subspace Drive - - - true - - - - - - - <html><head/><body><p>Ship becomes untargetable and appears on rader as wobbly dot</p></body></html> - - - Hidden from Sensors - - - true - - - - - - - <html><head/><body><p>Ship becomes untargetable and does not apper on hostile radar</p></body></html> - - - Stealth - - - true - - - - - - - <html><head/><body><p>Makes stealth apply to teammates</p></body></html> - - - Invisible to Friendlies When Stealthed - - - true - - - - - - - <html><head/><body><p>Ram the ships target doing the specified amount of damage</p></body></html> - - - Kamikaze - - - true - - - - - + - + - Qt::Horizontal - - - QSizePolicy::Minimum + Qt::Vertical - 10 - 20 + 20 + 40 - - - Damage - - - - - - - QAbstractSpinBox::NoButtons - - - - - - - - - <html><head/><body><p>Ship should not change position until destroyed</p></body></html> - - - Does Not Change Position - - - true - - - - - - - <html><head/><body><p>Ship should not change orientation until destroyed</p></body></html> - - - Does Not Change Orientation - - - true - - - - - - - - - - - <html><head/><body><p>Ship goes for its objectives without self-preservation</p></body></html> - - - No Dynamic Goals - - - true - - - - - - - <html><head/><body><p>If the next mission is marked red alert this ships damage and loadaout wil be copied onto a ship woth the same name (Buggy, Recommend using Save-load script)</p></body></html> - - - Red Alert Carry Status - - - true - - - - - - - <html><head/><body><p>DEPRECATED: Does Nothing</p></body></html> - - - Affected By Gravity - - - true - - - - - - - <html><head/><body><p>DEPRECATED: Use Custom warp-in menu</p></body></html> - - - Special Warpin - - - true - - - - - - - <html><head/><body><p>Ship is targetable using the target bomb command</p></body></html> - - - Targetable as Bomb - - - true - - - - - - - <html><head/><body><p>Disables all automated messages from the ship</p></body></html> - - - Disable Built-in Messages - - - true - - - - - - - Never Scream On Death - - - true - - - - - - - Always Scream on Death - - - true - - - - - - - <html><head/><body><p>Ship disintegrates on death leaving no debris</p></body></html> - - - Vaporize on Death - - - true - - - - - - - 0 - - - - - - 0 - 0 - - - - Respawn Priority - - - - - - - <html><head/><body><p>In multi a player will respawn nect to the ship with the highest priority (low number = high priority)</p></body></html> - - - - - - - - - <html><head/><body><p>Brings ship along when the player autopilots</p></body></html> - - - AutoNav Carry Status - - - true - - - - - - - <html><head/><body><p>Player can only autopilot when in range of this ship (??? meters)</p></body></html> - - - AutoNav Needs Link - - - true - - - - - - - <html><head/><body><p>Don't show the ships name in the targeting box</p></body></html> - - - Hide Ship Name - - - true - - - - - - - <html><head/><body><p>Set the ships class based on the alt ship class menu</p></body></html> - - - Set Class Dynamically - - - true - - - - - - - <html><head/><body><p>Disables the Energy Transfer System for this ship</p></body></html> - - - Disable ETS - - - true - - - - - - - <html><head/><body><p>Ship starts invisible</p></body></html> - - - Cloaked - - - true - - - - - - - <html><head/><body><p>Messages from the ship will be garbeled with bits missing</p></body></html> - - - Scramble Messages - - - true - - - - - - - <html><head/><body><p>Ship does not interact with any other object/weapon</p></body></html> - - - No Collisions - - - true - - - - - - - <html><head/><body><p>Prevents fighters in wong from self-destructing when there engines are destroyed</p></body></html> - - - No Disabled Self-Destruct - - - true - - - - - - - - - OK - - - - - - - Cancel - - + + + + + OK + + + + + + + Cancel + + + + @@ -684,9 +150,10 @@ - fso::fred::ShipFlagCheckbox - QCheckBox -
ui/widgets/ShipFlagCheckbox.h
+ fso::fred::FlagListWidget + QWidget +
ui/widgets/FlagList.h
+ 1
diff --git a/qtfred/ui/ShipInitialStatus.ui b/qtfred/ui/ShipInitialStatus.ui index e84c7639183..dd5a759b996 100644 --- a/qtfred/ui/ShipInitialStatus.ui +++ b/qtfred/ui/ShipInitialStatus.ui @@ -19,19 +19,6 @@ - - - - - 75 - 0 - - - - Velocity - - - @@ -48,15 +35,8 @@ - - - - Cancel - - - - - + + Qt::Horizontal @@ -84,8 +64,28 @@
- - + + + + OK + + + + + + + + 75 + 0 + + + + Hull Integrity + + + + + Qt::Horizontal @@ -97,15 +97,8 @@ - - - - OK - - - - - + + 75 @@ -113,10 +106,27 @@ - Hull Integrity + Velocity + + + + + + + Cancel + + + + Guardian Threshold + + + + + + From 289de86eb9abe32735f13c0732f3faf831c28c27 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 27 Aug 2025 22:48:39 -0400 Subject: [PATCH 426/466] fix some parse error handling Parse errors that are actually errors should use `error_display(1, ...)`, rather than merely logging them (which doesn't abort parsing) or using `Error` (which omits the line number and file name). Fixes #7002. Tested with a modified script file that produces the same error. --- code/parse/parselo.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/code/parse/parselo.cpp b/code/parse/parselo.cpp index 07d0d034405..42e7f60c2f1 100644 --- a/code/parse/parselo.cpp +++ b/code/parse/parselo.cpp @@ -960,7 +960,7 @@ char* alloc_text_until(const char* instr, const char* endstr) if(foundstr == NULL) { - Error(LOCATION, "Missing [%s] in file", endstr); + error_display(1, "Looking for [%s], but never found it.\n", endstr); throw parse::ParseException("End string not found"); } else @@ -994,7 +994,7 @@ void copy_text_until(char *outstr, const char *instr, const char *endstr, int ma auto foundstr = stristr(instr, endstr); if (foundstr == NULL) { - nprintf(("Error", "Error. Looking for [%s], but never found it.\n", endstr)); + error_display(1, "Looking for [%s], but never found it.\n", endstr); throw parse::ParseException("End string not found"); } @@ -1003,9 +1003,8 @@ void copy_text_until(char *outstr, const char *instr, const char *endstr, int ma outstr[foundstr - instr] = 0; } else { - nprintf(("Error", "Error. Too much text (" SIZE_T_ARG " chars, %i allowed) before %s\n", - foundstr - instr + strlen(endstr), max_chars, endstr)); - + error_display(1, "Too much text (" SIZE_T_ARG " chars, %i allowed) before %s\n", + foundstr - instr + strlen(endstr), max_chars, endstr); throw parse::ParseException("Too much text found"); } @@ -1020,7 +1019,7 @@ void copy_text_until(SCP_string &outstr, const char *instr, const char *endstr) auto foundstr = stristr(instr, endstr); if (foundstr == NULL) { - nprintf(("Error", "Error. Looking for [%s], but never found it.\n", endstr)); + error_display(1, "Looking for [%s], but never found it.\n", endstr); throw parse::ParseException("End string not found"); } @@ -1117,7 +1116,7 @@ char* alloc_block(const char* startstr, const char* endstr, int extra_chars) //Check that we left the file if(level > 0) { - Error(LOCATION, "Unclosed pair of \"%s\" and \"%s\" on line %d in file", startstr, endstr, get_line_num()); + error_display(1, "Unclosed pair of \"%s\" and \"%s\"", startstr, endstr); throw parse::ParseException("End string not found"); } else From b970e22410dbcd1bec0786ef1304b374fbac5b35 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 28 Aug 2025 01:02:06 -0400 Subject: [PATCH 427/466] add additional double quote conversion to FRED and qtFRED Any place in FRED that uses XSTR is susceptible to double quotes interfering with the standard XSTR format, not just the expected briefing, debriefing, messages, etc. This PR adds `lcl_fred_replace_stuff()` - which handles not only quotes but semicolons and slashes as well - to the other places where XSTR is used. (Note that quotes are handled just fine in non-XSTR fields. Semicolons will still truncate non-XSTR fields, but addressing that is beyond the scope of this PR as it would require editing every text field in FRED. Fortunately, errant semicolons merely modify the text as opposed to corrupting the mission file.) Fixes #7005 in both FRED and qtFRED; tested in both. Caveat aedificator: the briefing editor is not yet implemented in qtFRED, so the relevant fixes to briefing icon labels and closeup labels cannot yet be ported over. --- fred2/briefingeditordlg.cpp | 2 ++ fred2/eventeditor.cpp | 2 ++ fred2/initialstatus.cpp | 1 + fred2/jumpnodedlg.cpp | 4 +++- fred2/missiongoalsdlg.cpp | 1 + fred2/shipeditordlg.cpp | 3 +++ qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp | 3 +++ qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp | 2 ++ .../src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp | 4 ++++ .../dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp | 2 ++ qtfred/src/ui/dialogs/EventEditorDialog.cpp | 2 ++ 11 files changed, 25 insertions(+), 1 deletion(-) diff --git a/fred2/briefingeditordlg.cpp b/fred2/briefingeditordlg.cpp index f793fad4e9c..1f62d1c798f 100644 --- a/fred2/briefingeditordlg.cpp +++ b/fred2/briefingeditordlg.cpp @@ -467,6 +467,7 @@ void briefing_editor_dlg::update_data(int update) ptr->icons[m_last_icon].id = m_id; string_copy(buf, m_icon_label, MAX_LABEL_LEN - 1); + lcl_fred_replace_stuff(buf, MAX_LABEL_LEN - 1); if (stricmp(ptr->icons[m_last_icon].label, buf) && !m_change_local) { set_modified(); reset_icon_loop(m_last_stage); @@ -476,6 +477,7 @@ void briefing_editor_dlg::update_data(int update) strcpy_s(ptr->icons[m_last_icon].label, buf); string_copy(buf, m_icon_closeup_label, MAX_LABEL_LEN - 1); + lcl_fred_replace_stuff(buf, MAX_LABEL_LEN - 1); if (stricmp(ptr->icons[m_last_icon].closeup_label, buf) && !m_change_local) { set_modified(); reset_icon_loop(m_last_stage); diff --git a/fred2/eventeditor.cpp b/fred2/eventeditor.cpp index 51c76812beb..d47a3f289e9 100644 --- a/fred2/eventeditor.cpp +++ b/fred2/eventeditor.cpp @@ -911,6 +911,8 @@ void event_editor::save_event(int e) } // handle objective text + lcl_fred_replace_stuff(m_obj_text); + lcl_fred_replace_stuff(m_obj_key_text); m_events[e].objective_text = (LPCTSTR)m_obj_text; m_events[e].objective_key_text = (LPCTSTR)m_obj_key_text; diff --git a/fred2/initialstatus.cpp b/fred2/initialstatus.cpp index e745958fd16..fbc642d83b2 100644 --- a/fred2/initialstatus.cpp +++ b/fred2/initialstatus.cpp @@ -599,6 +599,7 @@ void initial_status::change_subsys() // update cargo name if (strlen(m_cargo_name) > 0) { //-V805 + lcl_fred_replace_stuff(m_cargo_name); cargo_index = string_lookup(m_cargo_name, Cargo_names, Num_cargo); if (cargo_index == -1) { if (Num_cargo < MAX_CARGO); diff --git a/fred2/jumpnodedlg.cpp b/fred2/jumpnodedlg.cpp index eb934f2ed68..e0b1a41c833 100644 --- a/fred2/jumpnodedlg.cpp +++ b/fred2/jumpnodedlg.cpp @@ -303,7 +303,9 @@ int jumpnode_dlg::update_data() m_name = _T(jnp->GetName()); UpdateData(FALSE); } - + + lcl_fred_replace_stuff(m_display); + strcpy_s(old_name, jnp->GetName()); jnp->SetName((LPCSTR) m_name); jnp->SetDisplayName((m_display.CompareNoCase("") == 0) ? m_name : m_display); diff --git a/fred2/missiongoalsdlg.cpp b/fred2/missiongoalsdlg.cpp index 87fa03d07e6..518704b37ab 100644 --- a/fred2/missiongoalsdlg.cpp +++ b/fred2/missiongoalsdlg.cpp @@ -436,6 +436,7 @@ void CMissionGoalsDlg::OnChangeGoalDesc() } UpdateData(TRUE); + lcl_fred_replace_stuff(m_goal_desc); string_copy(m_goals[cur_goal].message, m_goal_desc); } diff --git a/fred2/shipeditordlg.cpp b/fred2/shipeditordlg.cpp index cc3555b536a..a398fb5a9db 100644 --- a/fred2/shipeditordlg.cpp +++ b/fred2/shipeditordlg.cpp @@ -1291,6 +1291,8 @@ int CShipEditorDlg::update_ship(int ship) CComboBox *box; int persona; + lcl_fred_replace_stuff(m_ship_display_name); + // the display name was precalculated, so now just assign it if (m_ship_display_name == m_ship_name || m_ship_display_name.CompareNoCase("") == 0) { @@ -1332,6 +1334,7 @@ int CShipEditorDlg::update_ship(int ship) MODIFY(Ships[ship].weapons.ai_class, m_ai_class); } if (strlen(m_cargo1)) { + lcl_fred_replace_stuff(m_cargo1); z = string_lookup(m_cargo1, Cargo_names, Num_cargo); if (z == -1) { if (Num_cargo < MAX_CARGO) { diff --git a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp index 93d9c9e37fe..2034ef5484b 100644 --- a/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp @@ -2,6 +2,7 @@ #include "globalincs/linklist.h" #include +#include #include #include #include @@ -39,6 +40,8 @@ bool JumpNodeEditorDialogModel::apply() std::strncpy(old_name_buf, jnp->GetName(), NAME_LENGTH - 1); old_name_buf[NAME_LENGTH - 1] = '\0'; + lcl_fred_replace_stuff(_display); + jnp->SetName(_name.c_str()); jnp->SetDisplayName(lcase_equal(_display, "") ? _name.c_str() : _display.c_str()); diff --git a/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp index f13c7b81dee..4f1f8c0db13 100644 --- a/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp @@ -1,3 +1,4 @@ +#include #include "MissionGoalsDialogModel.h" @@ -167,6 +168,7 @@ mission_goal& MissionGoalsDialogModel::createNewGoal() { void MissionGoalsDialogModel::setCurrentGoalMessage(const char* text) { Assertion(isCurrentGoalValid(), "Current goal is not valid!"); getCurrentGoal().message = text; + lcl_fred_replace_stuff(getCurrentGoal().message); set_modified(); modelChanged(); diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp index 89f398e221a..b9b4ac9285f 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipEditorDialogModel.cpp @@ -14,6 +14,7 @@ #include "mission/missionmessage.h" #include +#include #include #include @@ -636,6 +637,8 @@ namespace fso { int z, d; SCP_string str; + lcl_fred_replace_stuff(_m_ship_display_name); + // the display name was precalculated, so now just assign it if (_m_ship_display_name == _m_ship_name || stricmp(_m_ship_display_name.c_str(), "") == 0) { @@ -671,6 +674,7 @@ namespace fso { Ships[ship].weapons.ai_class = _m_ai_class; } if (!_m_cargo1.empty()) { + lcl_fred_replace_stuff(_m_cargo1); z = string_lookup(_m_cargo1.c_str(), Cargo_names, Num_cargo); if (z == -1) { if (Num_cargo < MAX_CARGO) { diff --git a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp index 651bd8dcc0d..093bc93dbb9 100644 --- a/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipInitialStatusDialogModel.cpp @@ -3,6 +3,7 @@ #include "mission/object.h" #include +#include #include #include @@ -795,6 +796,7 @@ void ShipInitialStatusDialogModel::change_subsys(const int new_subsys) // update cargo name if (!m_cargo_name.empty()) { //-V805 + lcl_fred_replace_stuff(m_cargo_name); cargo_index = string_lookup(m_cargo_name.c_str(), Cargo_names, Num_cargo); if (cargo_index == -1) { if (Num_cargo < MAX_CARGO) { diff --git a/qtfred/src/ui/dialogs/EventEditorDialog.cpp b/qtfred/src/ui/dialogs/EventEditorDialog.cpp index 7800fad4a02..b0900640bce 100644 --- a/qtfred/src/ui/dialogs/EventEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/EventEditorDialog.cpp @@ -130,6 +130,7 @@ void EventEditorDialog::initEventWidgets() { return; } m_events[cur_event].objective_text = value.toUtf8().constData(); + lcl_fred_replace_stuff(m_events[cur_event].objective_text); updateEventBitmap(); }); @@ -138,6 +139,7 @@ void EventEditorDialog::initEventWidgets() { return; } m_events[cur_event].objective_key_text = value.toUtf8().constData(); + lcl_fred_replace_stuff(m_events[cur_event].objective_key_text); }); connectLogState(ui->checkLogTrue, MLF_SEXP_TRUE); connectLogState(ui->checkLogFalse, MLF_SEXP_FALSE); From 2d37a24b4d53a726408c99c4a56cc48ad3d5d61b Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Thu, 28 Aug 2025 01:20:51 -0400 Subject: [PATCH 428/466] fix formatting of alt names and callsigns when saving a mission --- fred2/missionsave.cpp | 14 ++++++++++++-- qtfred/src/mission/missionsave.cpp | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/fred2/missionsave.cpp b/fred2/missionsave.cpp index 8c4b9f21c70..3aa01720525 100644 --- a/fred2/missionsave.cpp +++ b/fred2/missionsave.cpp @@ -3624,12 +3624,22 @@ int CFred_mission_save::save_objects() // optional alternate type name if (strlen(Fred_alt_names[i])) { - fout("\n$Alt: %s\n", Fred_alt_names[i]); + if (optional_string_fred("$Alt:", "$Team:")) { + parse_comments(); + } else { + fout("\n$Alt:"); + } + fout(" %s", Fred_alt_names[i]); } // optional callsign if (Mission_save_format != FSO_FORMAT_RETAIL && strlen(Fred_callsigns[i])) { - fout("\n$Callsign: %s\n", Fred_callsigns[i]); + if (optional_string_fred("$Callsign:", "$Team:")) { + parse_comments(); + } else { + fout("\n$Callsign:"); + } + fout(" %s", Fred_callsigns[i]); } required_string_fred("$Team:"); diff --git a/qtfred/src/mission/missionsave.cpp b/qtfred/src/mission/missionsave.cpp index 4d411b32305..ed188facfe5 100644 --- a/qtfred/src/mission/missionsave.cpp +++ b/qtfred/src/mission/missionsave.cpp @@ -3538,12 +3538,22 @@ int CFred_mission_save::save_objects() // optional alternate type name if (strlen(Fred_alt_names[i])) { - fout("\n$Alt: %s\n", Fred_alt_names[i]); + if (optional_string_fred("$Alt:", "$Team:")) { + parse_comments(); + } else { + fout("\n$Alt:"); + } + fout(" %s", Fred_alt_names[i]); } // optional callsign if (save_format != MissionFormat::RETAIL && strlen(Fred_callsigns[i])) { - fout("\n$Callsign: %s\n", Fred_callsigns[i]); + if (optional_string_fred("$Callsign:", "$Team:")) { + parse_comments(); + } else { + fout("\n$Callsign:"); + } + fout(" %s", Fred_callsigns[i]); } required_string_fred("$Team:"); From a531fd9fedfe66a0daf1448b29168eb661b764ac Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 13:11:45 -0500 Subject: [PATCH 429/466] wire up the bitmap section --- qtfred/source_groups.cmake | 2 + .../dialogs/BackgroundEditorDialogModel.cpp | 336 ++++ .../dialogs/BackgroundEditorDialogModel.h | 65 + .../src/ui/dialogs/BackgroundEditorDialog.cpp | 221 ++- .../src/ui/dialogs/BackgroundEditorDialog.h | 40 +- qtfred/ui/BackgroundEditor.ui | 1422 +++++++++-------- 6 files changed, 1363 insertions(+), 723 deletions(-) create mode 100644 qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index a25c2303b29..cefc430e1bf 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -42,6 +42,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/AbstractDialogModel.h src/mission/dialogs/AsteroidEditorDialogModel.cpp src/mission/dialogs/AsteroidEditorDialogModel.h + src/mission/dialogs/BackgroundEditorDialogModel.h + src/mission/dialogs/BackgroundEditorDialogModel.cpp src/mission/dialogs/CampaignEditorDialogModel.cpp src/mission/dialogs/CampaignEditorDialogModel.h src/mission/dialogs/CommandBriefingDialogModel.cpp diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp new file mode 100644 index 00000000000..08cd99039ea --- /dev/null +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -0,0 +1,336 @@ +#include "FredApplication.h" +#include "BackgroundEditorDialogModel.h" + +//#include "mission/missionparse.h" + +// TODO move this to common for both FREDs. Do not pass review if this is not done +const static float delta = .00001f; + +namespace fso::fred::dialogs { +BackgroundEditorDialogModel::BackgroundEditorDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + // may not need these because I don't think anything else can modify data that this dialog works with + connect(_editor, &Editor::currentObjectChanged, this, &BackgroundEditorDialogModel::onEditorSelectionChanged); + connect(_editor, &Editor::missionChanged, this, &BackgroundEditorDialogModel::onEditorMissionChanged); + + auto& bg = getActiveBackground(); + auto& list = bg.bitmaps; + if (!list.empty()) { + _selectedBitmapIndex = 0; + } +} + +bool BackgroundEditorDialogModel::apply() +{ + // do the OnClose stuff here + return true; +} + +void BackgroundEditorDialogModel::reject() +{ + // do nothing? +} + +void BackgroundEditorDialogModel::onEditorSelectionChanged(int) +{ + // reload? +} + +void BackgroundEditorDialogModel::onEditorMissionChanged() +{ + // reload? +} + +background_t& BackgroundEditorDialogModel::getActiveBackground() const +{ + if (!SCP_vector_inbounds(Backgrounds, Cur_background)) { + // Fall back to first background if Cur_background isn’t set + Cur_background = 0; + } + return Backgrounds[Cur_background]; +} + +starfield_list_entry* BackgroundEditorDialogModel::getActiveBitmap() const +{ + auto& bg = getActiveBackground(); + auto& list = bg.bitmaps; + if (!SCP_vector_inbounds(list, _selectedBitmapIndex)) { + return nullptr; + } + return &list[_selectedBitmapIndex]; +} + +SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() const +{ + SCP_vector out; + const int count = stars_get_num_entries(/*is_a_sun=*/false, /*bitmap_count=*/true); + out.reserve(count); + for (int i = 0; i < count; ++i) { + if (const char* name = stars_get_name_FRED(i, /*is_a_sun=*/false)) { + out.emplace_back(name); + } + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getMissionBitmapNames() const +{ + SCP_vector out; + const auto& vec = getActiveBackground().bitmaps; + out.reserve(vec.size()); + for (const auto& sle : vec) { + out.emplace_back(sle.filename); + } + return out; +} + +void BackgroundEditorDialogModel::refreshBackgroundPreview() +{ + stars_load_background(Cur_background); // rebuild instances from Backgrounds[] + if (_viewport) { + _viewport->needsUpdate(); // schedule a repaint + } +} + +void BackgroundEditorDialogModel::setSelectedBitmapIndex(int index) +{ + const auto& bg = getActiveBackground(); + const auto& list = bg.bitmaps; + if (!SCP_vector_inbounds(list, index)) { + _selectedBitmapIndex = -1; + return; + } + _selectedBitmapIndex = index; +} + +int BackgroundEditorDialogModel::getSelectedBitmapIndex() const +{ + return _selectedBitmapIndex; +} + +void BackgroundEditorDialogModel::addMissionBitmapByName(const SCP_string& name) +{ + if (name.empty()) + return; + + // Must exist in tables + if (stars_find_bitmap(name.c_str()) < 0) + return; + + starfield_list_entry sle{}; + std::strncpy(sle.filename, name.c_str(), MAX_FILENAME_LEN - 1); + sle.ang.p = 0.0f; + sle.ang.b = 0.0f; + sle.ang.h = 0.0f; + sle.scale_x = 1.0f; + sle.scale_y = 1.0f; + sle.div_x = 1; + sle.div_y = 1; + + auto& list = getActiveBackground().bitmaps; + list.push_back(sle); + + _selectedBitmapIndex = static_cast(list.size()) - 1; + + set_modified(); + refreshBackgroundPreview(); +} + +void BackgroundEditorDialogModel::removeMissionBitmap() +{ + auto& list = getActiveBackground().bitmaps; + + // Make sure we have an active bitmap + if (getActiveBitmap() == nullptr) { + return; + } + + list.erase(list.begin() + _selectedBitmapIndex); + + // choose a sensible new selection + if (list.empty()) { + _selectedBitmapIndex = -1; + } else { + _selectedBitmapIndex = std::min(_selectedBitmapIndex, static_cast(list.size()) - 1); + } + + set_modified(); + refreshBackgroundPreview(); +} + +SCP_string BackgroundEditorDialogModel::getBitmapName() const +{ + auto bm = getActiveBitmap(); + if (bm == nullptr) { + return ""; + } + + return bm->filename; +} + +void BackgroundEditorDialogModel::setBitmapName(const SCP_string& name) +{ + if (name.empty()) + return; + + // Must exist in tables + if (stars_find_bitmap(name.c_str()) < 0) + return; + + auto bm = getActiveBitmap(); + if (bm != nullptr) { + strcpy_s(bm->filename, name.c_str()); + set_modified(); + refreshBackgroundPreview(); + } +} + +int BackgroundEditorDialogModel::getBitmapPitch() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return -1; + + return fl2ir(fl_degrees(bm->ang.p) + delta); +} + +void BackgroundEditorDialogModel::setBitmapPitch(int deg) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(bm->ang.p, fl_radians(deg)); + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getBitmapBank() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return -1; + + return fl2ir(fl_degrees(bm->ang.b) + delta); +} + +void BackgroundEditorDialogModel::setBitmapBank(int deg) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(bm->ang.b, fl_radians(deg)); + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getBitmapHeading() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return -1; + + return fl2ir(fl_degrees(bm->ang.h) + delta); +} + +void BackgroundEditorDialogModel::setBitmapHeading(int deg) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(bm->ang.h, fl_radians(deg)); + + refreshBackgroundPreview(); +} + +float BackgroundEditorDialogModel::getBitmapScaleX() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return -1.0f; + + return bm->scale_x; +} + +void BackgroundEditorDialogModel::setBitmapScaleX(float v) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(v, getScaleLimit().first, getScaleLimit().second); + modify(bm->scale_x, v); + + refreshBackgroundPreview(); +} + +float BackgroundEditorDialogModel::getBitmapScaleY() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return -1.0f; + + return bm->scale_y; +} + +void BackgroundEditorDialogModel::setBitmapScaleY(float v) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(v, getScaleLimit().first, getScaleLimit().second); + modify(bm->scale_y, v); + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getBitmapDivX() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return -1; + + return bm->div_x; +} + +void BackgroundEditorDialogModel::setBitmapDivX(int v) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(v, getDivisionLimit().first, getDivisionLimit().second); + modify(bm->div_x, v); + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getBitmapDivY() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return -1; + + return bm->div_y; +} + +void BackgroundEditorDialogModel::setBitmapDivY(int v) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(v, getDivisionLimit().first, getDivisionLimit().second); + modify(bm->div_y, v); + + refreshBackgroundPreview(); +} + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h new file mode 100644 index 00000000000..6cf777dab4f --- /dev/null +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -0,0 +1,65 @@ +#pragma once + +#include "globalincs/pstypes.h" + +#include "AbstractDialogModel.h" + +#include "starfield/starfield.h" +#include + +namespace fso::fred::dialogs { + +/** + * @brief QTFred's Wing Editor's Model + */ +class BackgroundEditorDialogModel : public AbstractDialogModel { + Q_OBJECT + + public: + BackgroundEditorDialogModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; + + // limits + std::pair getOrientLimit() const { return {0, 359}; } + std::pair getScaleLimit() const { return {0.001f, 18.0f}; } + std::pair getDivisionLimit() const { return {1, 5}; } + + // bitmap group box + SCP_vector getAvailableBitmapNames() const; + SCP_vector getMissionBitmapNames() const; + void setSelectedBitmapIndex(int index); + int getSelectedBitmapIndex() const; + void addMissionBitmapByName(const SCP_string& name); + void removeMissionBitmap(); + SCP_string getBitmapName() const; + void setBitmapName(const SCP_string& name); + int getBitmapPitch() const; + void setBitmapPitch(int deg); + int getBitmapBank() const; + void setBitmapBank(int deg); + int getBitmapHeading() const; + void setBitmapHeading(int deg); + float getBitmapScaleX() const; + void setBitmapScaleX(float v); + float getBitmapScaleY() const; + void setBitmapScaleY(float v); + int getBitmapDivX() const; + void setBitmapDivX(int v); + int getBitmapDivY() const; + void setBitmapDivY(int v); + + private slots: + void onEditorSelectionChanged(int); // currentObjectChanged + void onEditorMissionChanged(); // missionChanged + + private: // NOLINT(readability-redundant-access-specifiers) + void refreshBackgroundPreview(); + background_t& getActiveBackground() const; + starfield_list_entry* getActiveBitmap() const; + + int _selectedBitmapIndex = -1; // index into Backgrounds[Cur_background].bitmaps + +}; +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp index 937cc7082f9..01519d855f8 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp @@ -1,23 +1,226 @@ #include "BackgroundEditorDialog.h" - +#include "ui/util/SignalBlockers.h" +#include "ui/dialogs/General/ImagePickerDialog.h" #include "ui_BackgroundEditor.h" -namespace fso { -namespace fred { -namespace dialogs { +#include + +namespace fso::fred::dialogs { -BackgroundEditorDialog::BackgroundEditorDialog(FredView* parent, EditorViewport* viewport) : - QDialog(parent), ui(new Ui::BackgroundEditor()), - _viewport(viewport) { - ui->setupUi(this); +BackgroundEditorDialog::BackgroundEditorDialog(FredView* parent, EditorViewport* viewport) : QDialog(parent), + ui(new Ui::BackgroundEditor()), _model(new BackgroundEditorDialogModel(this, viewport)), _viewport(viewport) { + + ui->setupUi(this); + + initializeUi(); // Resize the dialog to the minimum size resize(QDialog::sizeHint()); } -BackgroundEditorDialog::~BackgroundEditorDialog() { +BackgroundEditorDialog::~BackgroundEditorDialog() = default; + +void BackgroundEditorDialog::initializeUi() +{ + util::SignalBlockers blockers(this); + + // Backgrounds + // TODO + + // Bitmaps + ui->bitmapPitchSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); + ui->bitmapBankSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); + ui->bitmapHeadingSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); + ui->bitmapScaleXDoubleSpinBox->setRange(_model->getScaleLimit().first, _model->getScaleLimit().second); + ui->bitmapScaleYDoubleSpinBox->setRange(_model->getScaleLimit().first, _model->getScaleLimit().second); + ui->bitmapDivXSpinBox->setRange(_model->getDivisionLimit().first, _model->getDivisionLimit().second); + ui->bitmapDivYSpinBox->setRange(_model->getDivisionLimit().first, _model->getDivisionLimit().second); + refreshBitmapList(); + + const auto& names = _model->getAvailableBitmapNames(); + for (const auto& s : names){ + ui->bitmapTypeCombo->addItem(QString::fromStdString(s)); + } + +} + +void BackgroundEditorDialog::updateUi() +{ + util::SignalBlockers blockers(this); + // Backgrounds + // TODO + // Bitmaps + refreshBitmapList(); +} + +void BackgroundEditorDialog::refreshBitmapList() +{ + util::SignalBlockers blockers(this); + + const auto names = _model->getMissionBitmapNames(); + + const int oldRow = ui->bitmapListWidget->currentRow(); + ui->bitmapListWidget->setUpdatesEnabled(false); + ui->bitmapListWidget->clear(); + + QStringList items; + items.reserve(static_cast(names.size())); + for (const auto& s : names) + items << QString::fromStdString(s); + ui->bitmapListWidget->addItems(items); + + if (!items.isEmpty()) { + const int clamped = qBound(0, oldRow, ui->bitmapListWidget->count() - 1); + ui->bitmapListWidget->setCurrentRow(clamped); + } + + ui->bitmapListWidget->setUpdatesEnabled(true); + + updateBitmapControls(); +} + +void BackgroundEditorDialog::updateBitmapControls() +{ + util::SignalBlockers blockers(this); + + bool enabled = (_model->getSelectedBitmapIndex() >= 0); + + ui->changeBitmapButton->setEnabled(enabled); + ui->deleteBitmapButton->setEnabled(enabled); + ui->bitmapTypeCombo->setEnabled(enabled); + ui->bitmapPitchSpin->setEnabled(enabled); + ui->bitmapBankSpin->setEnabled(enabled); + ui->bitmapHeadingSpin->setEnabled(enabled); + ui->bitmapScaleXDoubleSpinBox->setEnabled(enabled); + ui->bitmapScaleYDoubleSpinBox->setEnabled(enabled); + ui->bitmapDivXSpinBox->setEnabled(enabled); + ui->bitmapDivYSpinBox->setEnabled(enabled); + + const int index = ui->bitmapTypeCombo->findText(QString::fromStdString(_model->getBitmapName())); + ui->bitmapTypeCombo->setCurrentIndex(index); + + ui->bitmapPitchSpin->setValue(_model->getBitmapPitch()); + ui->bitmapBankSpin->setValue(_model->getBitmapBank()); + ui->bitmapHeadingSpin->setValue(_model->getBitmapHeading()); + ui->bitmapScaleXDoubleSpinBox->setValue(_model->getBitmapScaleX()); + ui->bitmapScaleYDoubleSpinBox->setValue(_model->getBitmapScaleY()); + ui->bitmapDivXSpinBox->setValue(_model->getBitmapDivX()); + ui->bitmapDivYSpinBox->setValue(_model->getBitmapDivY()); +} + +void BackgroundEditorDialog::on_bitmapListWidget_currentRowChanged(int row) +{ + _model->setSelectedBitmapIndex(row); + updateBitmapControls(); +} + +void BackgroundEditorDialog::on_bitmapTypeCombo_currentIndexChanged(int index) +{ + if (index < 0) + return; + + const QString text = ui->bitmapTypeCombo->itemText(index); + _model->setBitmapName(text.toUtf8().constData()); + refreshBitmapList(); +} + +void BackgroundEditorDialog::on_bitmapPitchSpin_valueChanged(int arg1) +{ + _model->setBitmapPitch(arg1); +} + +void BackgroundEditorDialog::on_bitmapBankSpin_valueChanged(int arg1) +{ + _model->setBitmapBank(arg1); } +void BackgroundEditorDialog::on_bitmapHeadingSpin_valueChanged(int arg1) +{ + _model->setBitmapHeading(arg1); } + +void BackgroundEditorDialog::on_bitmapScaleXDoubleSpinBox_valueChanged(double arg1) +{ + _model->setBitmapScaleX(static_cast(arg1)); +} + +void BackgroundEditorDialog::on_bitmapScaleYDoubleSpinBox_valueChanged(double arg1) +{ + _model->setBitmapScaleY(static_cast(arg1)); } + +void BackgroundEditorDialog::on_bitmapDivXSpinBox_valueChanged(int arg1) +{ + _model->setBitmapDivX(arg1); } + +void BackgroundEditorDialog::on_bitmapDivYSpinBox_valueChanged(int arg1) +{ + _model->setBitmapDivY(arg1); +} + +void BackgroundEditorDialog::on_addBitmapButton_clicked() +{ + const auto files = _model->getAvailableBitmapNames(); + if (files.empty()) { + QMessageBox::information(this, "Select Background Bitmap", "No bitmaps found."); + return; + } + + QStringList qnames; + qnames.reserve(static_cast(files.size())); + for (const auto& s : files) + qnames << QString::fromStdString(s); + + ImagePickerDialog dlg(this); + dlg.setWindowTitle("Select Background Bitmap"); + dlg.setImageFilenames(qnames); + + // Optional: preselect current + //dlg.setInitialSelection(QString::fromStdString(_model->getSquadLogo())); + + if (dlg.exec() != QDialog::Accepted) + return; + + const SCP_string chosen = dlg.selectedFile().toUtf8().constData(); + _model->addMissionBitmapByName(chosen); + + refreshBitmapList(); +} + +void BackgroundEditorDialog::on_changeBitmapButton_clicked() +{ + const auto files = _model->getAvailableBitmapNames(); + if (files.empty()) { + QMessageBox::information(this, "Select Background Bitmap", "No bitmaps found."); + return; + } + + QStringList qnames; + qnames.reserve(static_cast(files.size())); + for (const auto& s : files) + qnames << QString::fromStdString(s); + + ImagePickerDialog dlg(this); + dlg.setWindowTitle("Select Background Bitmap"); + dlg.setImageFilenames(qnames); + + // preselect current + dlg.setInitialSelection(QString::fromStdString(_model->getBitmapName())); + + if (dlg.exec() != QDialog::Accepted) + return; + + const SCP_string chosen = dlg.selectedFile().toUtf8().constData(); + _model->setBitmapName(chosen); + + refreshBitmapList(); +} + +void BackgroundEditorDialog::on_deleteBitmapButton_clicked() +{ + _model->removeMissionBitmap(); + refreshBitmapList(); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h index 5827277ea65..bb8773be8ea 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -1,32 +1,48 @@ #pragma once +#include "mission/dialogs/BackgroundEditorDialogModel.h" #include #include -namespace fso { -namespace fred { -namespace dialogs { +namespace fso::fred::dialogs { namespace Ui { class BackgroundEditor; } -class BackgroundEditorDialog : public QDialog -{ +class BackgroundEditorDialog : public QDialog { Q_OBJECT public: explicit BackgroundEditorDialog(FredView* parent, EditorViewport* viewport); - // TODO shouldn't all QDialog subclasses have a virtual destructor? ~BackgroundEditorDialog() override; -private: +private slots: + + // Bitmaps + void on_bitmapListWidget_currentRowChanged(int row); + void on_bitmapTypeCombo_currentIndexChanged(int index); + void on_bitmapPitchSpin_valueChanged(int arg1); + void on_bitmapBankSpin_valueChanged(int arg1); + void on_bitmapHeadingSpin_valueChanged(int arg1); + void on_bitmapScaleXDoubleSpinBox_valueChanged(double arg1); + void on_bitmapScaleYDoubleSpinBox_valueChanged(double arg1); + void on_bitmapDivXSpinBox_valueChanged(int arg1); + void on_bitmapDivYSpinBox_valueChanged(int arg1); + void on_addBitmapButton_clicked(); + void on_changeBitmapButton_clicked(); + void on_deleteBitmapButton_clicked(); + +private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; - //std::unique_ptr _model; - EditorViewport* _viewport; + std::unique_ptr _model; + EditorViewport* _viewport; + + void initializeUi(); + void updateUi(); + void refreshBitmapList(); + void updateBitmapControls(); }; -} -} -} +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/BackgroundEditor.ui b/qtfred/ui/BackgroundEditor.ui index bd06bef4caa..62b1ca32ba3 100644 --- a/qtfred/ui/BackgroundEditor.ui +++ b/qtfred/ui/BackgroundEditor.ui @@ -6,877 +6,895 @@ 0 0 - 639 - 681 + 981 + 814 Background Editor - + - + - - - - + + + + 150 + 0 + + + + + + + + Add + + + + + + + Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Import... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Swap With + + + + + + + + 150 + 0 + + + + + + + + + + - - - Import... + + + Bitmaps + + + + + + + + + + + + Add + + + + + + + Change + + + + + + + Delete + + + + + + + + + + + + + + + + + + Pitch + + + + + + + 16777215 + + + + + + + Bank + + + + + + + 16777215 + + + + + + + Heading + + + + + + + 16777215 + + + + + + + + + Scale (x/y) + + + Qt::AlignCenter + + + + + + + + + 16777215.000000000000000 + + + + + + + 16777215.000000000000000 + + + + + + + + + # divisions (x/y) + + + Qt::AlignCenter + + + + + + + + + 16777215 + + + + + + + 16777215 + + + + + + + + - - - Qt::Horizontal - - - - 40 - 20 - + + + Nebula - - - - - - - - Bitmaps - - - - - - - + - + - + - Add + Full Nebula - - - Qt::Horizontal + + + + + Range + + + + + + + Pattern + + + + + + + + + + Lightning storm + + + + + + + + + + 16777215 + + + + + + + + + Toggle ship trails - - - 40 - 20 - + + + + + + + + Fog Near Multiplier + + + + + + + Fog Far Multiplier + + + + + + + 16777215.000000000000000 + + + + + + + 16777215.000000000000000 + + + + + + + + + Display background bitmaps in nebula - + - + - Delete + Override Nebula Fog Palette + + + + + + + 0 + 0 + + + + R + + + + + + + 255 + + + + + + + G + + + + + + + 255 + + + + + + + B + + + + + + + 255 + + + + + - - - - - + + + + + Poofs + + + + + + + + + + + + + + Old Nebula + + - - - + + + - Heading + Pattern - - + + + + + + + Color + + + + + + + + + - + Pitch - + Bank - - - - - - - - - - Qt::Horizontal + + + + Heading - + + + + + - 40 - 20 + 100 + 0 - - - - - - Qt::Horizontal + + 16777215 - + + + + + - 40 - 20 + 100 + 0 - - - - - - Qt::Horizontal + + 16777215 - + + + + + - 40 - 20 + 100 + 0 - + + 16777215 + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + suns + + - - - - - Qt::Horizontal - - - - 40 - 20 - - - - + - - - Scale (x/y) - - + - - - Qt::Horizontal - - - - 40 - 20 - - - + + + + + Add + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Delete + + + + - + - + - + + + + + Pitch + + + + + + + 16777215 + + + + + + + Bank + + + + + + + 16777215 + + + + + + + Heading + + + + + + + 16777215 + + + + + + + Scale + + + + + + + + 75 + 0 + + + + 16777215.000000000000000 + + + + + + + + + + + Ambient light + + - - - + + + + + B: 0 + + + + + + + G: 0 + + + + + Qt::Horizontal - - - 40 - 20 - - - + - - - - # divisions (x/y) + + + + Qt::Horizontal - - + + Qt::Horizontal - - - 40 - 20 - + + + + + + R: 0 - + + + + + + + + Skybox + + - + - + + + Skybox Model + + - + - - - - - - - - - Nebula - - - - - - - - Full Nebula - - - - - - + + + - Range + Pitch - - + + + + 16777215 + + - - + + - Pattern + Bank - - - - - - - Lightning storm + + + + 16777215 - - - - - - - - - Toggle ship trails - - - - - - - + + - Fog Near + Heading - - - - - - - Fog Far Multiplier + + + + 16777215 - - - - - - - - - - - Poofs - - - - - - - PoofGreen01 - - - - - - - PoofGreen02 - - - - - - - PoofRed01 - - - - - - - PoofRed02 - - - - - - - PoofPurp01 - - - - - - - PoofPurp02 - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - - Old Nebula - - - - - - - - Pattern - - - - - - - - - - Color - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - Pitch - - - - - - - - - - Bank - - - - - - - - - - Heading - - - - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Swap With - - - - - - - - - - - - suns - - - - - - - - - - - - Add - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - + + + - Delete + No lighting - - - - - - - - - - - - - + + - Heading + Transparent - - - - - + + - Pitch + Force clamp - + - Bank + No z-buffer - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - + + + No cull - + - - - Qt::Horizontal - - - - 40 - 20 - - - - - - + - Scale + No glow maps - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - Ambient light - - - - - - - - Qt::Horizontal - - - - - - - Red: 0 - - - - - - - Qt::Horizontal - - - - - - - Green: 0 - - - - - - - Qt::Horizontal - - - - - - - Blue: 0 - - - - - - - - - - - - Skybox - - - - - - - - Skybox Model - - - - - - - - - - - - - - Pitch - - - - - - - - - - Bank - - - - - - + + + + + + Misc + + - + - Heading + Number of stars: - - - - - - - - - - No lighting - - - - - - - Transparent - - - - - - - Force clamp - - - - - - - No z-buffer - - - - - - - No cull - - - - - - - No glow maps + + + Qt::Horizontal - - - - - - - - - Misc - - - - - - Number of stars: 500 - - - - - - - Qt::Horizontal - - - - - - - Takes place inside subspace - - - - - - + - Environment Map + Takes place inside subspace - + + + + + Environment Map + + + + + + + + + + Lighting Profile + + + + + + + - - - + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + From f691966675b1a7d10634b1e2f836f1c2413200b5 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 14:15:19 -0500 Subject: [PATCH 430/466] sun section --- .../dialogs/BackgroundEditorDialogModel.cpp | 245 +++++++++++++++--- .../dialogs/BackgroundEditorDialogModel.h | 27 +- .../src/ui/dialogs/BackgroundEditorDialog.cpp | 166 +++++++++++- .../src/ui/dialogs/BackgroundEditorDialog.h | 12 + qtfred/ui/BackgroundEditor.ui | 103 ++++++-- 5 files changed, 482 insertions(+), 71 deletions(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index 08cd99039ea..e301d549666 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -10,15 +10,16 @@ namespace fso::fred::dialogs { BackgroundEditorDialogModel::BackgroundEditorDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) { - // may not need these because I don't think anything else can modify data that this dialog works with - connect(_editor, &Editor::currentObjectChanged, this, &BackgroundEditorDialogModel::onEditorSelectionChanged); - connect(_editor, &Editor::missionChanged, this, &BackgroundEditorDialogModel::onEditorMissionChanged); - auto& bg = getActiveBackground(); - auto& list = bg.bitmaps; - if (!list.empty()) { + auto& bm_list = bg.bitmaps; + if (!bm_list.empty()) { _selectedBitmapIndex = 0; } + + auto& sun_list = bg.suns; + if (!sun_list.empty()) { + _selectedSunIndex = 0; + } } bool BackgroundEditorDialogModel::apply() @@ -32,14 +33,10 @@ void BackgroundEditorDialogModel::reject() // do nothing? } -void BackgroundEditorDialogModel::onEditorSelectionChanged(int) -{ - // reload? -} - -void BackgroundEditorDialogModel::onEditorMissionChanged() +void BackgroundEditorDialogModel::refreshBackgroundPreview() { - // reload? + stars_load_background(Cur_background); // rebuild instances from Backgrounds[] + _editor->missionChanged(); } background_t& BackgroundEditorDialogModel::getActiveBackground() const @@ -61,6 +58,16 @@ starfield_list_entry* BackgroundEditorDialogModel::getActiveBitmap() const return &list[_selectedBitmapIndex]; } +starfield_list_entry* BackgroundEditorDialogModel::getActiveSun() const +{ + auto& bg = getActiveBackground(); + auto& list = bg.suns; + if (!SCP_vector_inbounds(list, _selectedSunIndex)) { + return nullptr; + } + return &list[_selectedSunIndex]; +} + SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() const { SCP_vector out; @@ -85,14 +92,6 @@ SCP_vector BackgroundEditorDialogModel::getMissionBitmapNames() cons return out; } -void BackgroundEditorDialogModel::refreshBackgroundPreview() -{ - stars_load_background(Cur_background); // rebuild instances from Backgrounds[] - if (_viewport) { - _viewport->needsUpdate(); // schedule a repaint - } -} - void BackgroundEditorDialogModel::setSelectedBitmapIndex(int index) { const auto& bg = getActiveBackground(); @@ -190,7 +189,7 @@ int BackgroundEditorDialogModel::getBitmapPitch() const { auto* bm = getActiveBitmap(); if (!bm) - return -1; + return 0; return fl2ir(fl_degrees(bm->ang.p) + delta); } @@ -211,7 +210,7 @@ int BackgroundEditorDialogModel::getBitmapBank() const { auto* bm = getActiveBitmap(); if (!bm) - return -1; + return 0; return fl2ir(fl_degrees(bm->ang.b) + delta); } @@ -232,7 +231,7 @@ int BackgroundEditorDialogModel::getBitmapHeading() const { auto* bm = getActiveBitmap(); if (!bm) - return -1; + return 0; return fl2ir(fl_degrees(bm->ang.h) + delta); } @@ -253,7 +252,7 @@ float BackgroundEditorDialogModel::getBitmapScaleX() const { auto* bm = getActiveBitmap(); if (!bm) - return -1.0f; + return 0; return bm->scale_x; } @@ -264,7 +263,7 @@ void BackgroundEditorDialogModel::setBitmapScaleX(float v) if (!bm) return; - CLAMP(v, getScaleLimit().first, getScaleLimit().second); + CLAMP(v, getBitmapScaleLimit().first, getBitmapScaleLimit().second); modify(bm->scale_x, v); refreshBackgroundPreview(); @@ -274,7 +273,7 @@ float BackgroundEditorDialogModel::getBitmapScaleY() const { auto* bm = getActiveBitmap(); if (!bm) - return -1.0f; + return 0; return bm->scale_y; } @@ -285,7 +284,7 @@ void BackgroundEditorDialogModel::setBitmapScaleY(float v) if (!bm) return; - CLAMP(v, getScaleLimit().first, getScaleLimit().second); + CLAMP(v, getBitmapScaleLimit().first, getBitmapScaleLimit().second); modify(bm->scale_y, v); refreshBackgroundPreview(); @@ -295,7 +294,7 @@ int BackgroundEditorDialogModel::getBitmapDivX() const { auto* bm = getActiveBitmap(); if (!bm) - return -1; + return 0; return bm->div_x; } @@ -316,7 +315,7 @@ int BackgroundEditorDialogModel::getBitmapDivY() const { auto* bm = getActiveBitmap(); if (!bm) - return -1; + return 0; return bm->div_y; } @@ -333,4 +332,190 @@ void BackgroundEditorDialogModel::setBitmapDivY(int v) refreshBackgroundPreview(); } +SCP_vector BackgroundEditorDialogModel::getAvailableSunNames() const +{ + SCP_vector out; + const int count = stars_get_num_entries(/*is_a_sun=*/true, /*bitmap_count=*/true); + out.reserve(count); + for (int i = 0; i < count; ++i) { + if (const char* name = stars_get_name_FRED(i, /*is_a_sun=*/true)) { // table order + out.emplace_back(name); + } + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getMissionSunNames() const +{ + SCP_vector out; + const auto& vec = getActiveBackground().suns; + out.reserve(vec.size()); + for (const auto& sle : vec) + out.emplace_back(sle.filename); + return out; +} + +void BackgroundEditorDialogModel::setSelectedSunIndex(int index) +{ + const auto& list = getActiveBackground().suns; + if (!SCP_vector_inbounds(list, index)) { + _selectedSunIndex = -1; + return; + } + _selectedSunIndex = index; +} + +int BackgroundEditorDialogModel::getSelectedSunIndex() const +{ + return _selectedSunIndex; +} + +void BackgroundEditorDialogModel::addMissionSunByName(const SCP_string& name) +{ + if (name.empty()) + return; + + if (stars_find_sun(name.c_str()) < 0) + return; // must exist in sun table + + starfield_list_entry sle{}; + std::strncpy(sle.filename, name.c_str(), MAX_FILENAME_LEN - 1); + sle.ang.p = sle.ang.b = sle.ang.h = 0.0f; + sle.scale_x = 1.0f; + sle.scale_y = 1.0f; + sle.div_x = 1; + sle.div_y = 1; + + auto& list = getActiveBackground().suns; + list.push_back(sle); + _selectedSunIndex = static_cast(list.size()) - 1; + + set_modified(); + refreshBackgroundPreview(); +} + +void BackgroundEditorDialogModel::removeMissionSun() +{ + auto& list = getActiveBackground().suns; + if (getActiveSun() == nullptr) + return; + + list.erase(list.begin() + _selectedSunIndex); + if (list.empty()) + _selectedSunIndex = -1; + else + _selectedSunIndex = std::min(_selectedSunIndex, static_cast(list.size()) - 1); + + set_modified(); + refreshBackgroundPreview(); +} + +SCP_string BackgroundEditorDialogModel::getSunName() const +{ + auto* s = getActiveSun(); + if (!s) + return ""; + + return s->filename; +} + +void BackgroundEditorDialogModel::setSunName(const SCP_string& name) +{ + if (name.empty()) + return; + + if (stars_find_sun(name.c_str()) < 0) + return; + + auto* s = getActiveSun(); + if (!s) + return; + + strcpy_s(s->filename, name.c_str()); + set_modified(); + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getSunPitch() const +{ + auto* s = getActiveSun(); + if (!s) + return 0; + + return fl2ir(fl_degrees(s->ang.p) + delta); +} + +void BackgroundEditorDialogModel::setSunPitch(int deg) +{ + auto* s = getActiveSun(); + if (!s) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(s->ang.p, fl_radians(deg)); + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getSunBank() const +{ + // Bank is not used for suns but this is added for consistency + /*auto* s = getActiveSun(); + if (!s) + return 0; + + return fl2ir(fl_degrees(s->ang.b) + delta);*/ +} + +void BackgroundEditorDialogModel::setSunBank(int deg) +{ + // Bank is not used for suns but this is added for consistency + /*auto* s = getActiveSun(); + if (!s) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(s->ang.b, fl_radians(deg)); + refreshBackgroundPreview();*/ +} + +int BackgroundEditorDialogModel::getSunHeading() const +{ + auto* s = getActiveSun(); + if (!s) + return 0; + + return fl2ir(fl_degrees(s->ang.h) + delta); +} + +void BackgroundEditorDialogModel::setSunHeading(int deg) +{ + auto* s = getActiveSun(); + if (!s) + return; + + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + modify(s->ang.h, fl_radians(deg)); + refreshBackgroundPreview(); +} + +float BackgroundEditorDialogModel::getSunScale() const +{ + auto* s = getActiveSun(); + if (!s) + return 0; + + return s->scale_x; // suns store scale in X; Y remains 1.0 +} + +void BackgroundEditorDialogModel::setSunScale(float v) +{ + auto* s = getActiveSun(); + if (!s) + return; + + CLAMP(v, getSunScaleLimit().first, getSunScaleLimit().second); + modify(s->scale_x, v); + refreshBackgroundPreview(); +} + } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index 6cf777dab4f..9045ec3c677 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -23,10 +23,11 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { // limits std::pair getOrientLimit() const { return {0, 359}; } - std::pair getScaleLimit() const { return {0.001f, 18.0f}; } + std::pair getBitmapScaleLimit() const { return {0.001f, 18.0f}; } + std::pair getSunScaleLimit() const{ return {0.1f, 50.0f}; } std::pair getDivisionLimit() const { return {1, 5}; } - // bitmap group box + // bitmap group SCP_vector getAvailableBitmapNames() const; SCP_vector getMissionBitmapNames() const; void setSelectedBitmapIndex(int index); @@ -50,16 +51,32 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { int getBitmapDivY() const; void setBitmapDivY(int v); - private slots: - void onEditorSelectionChanged(int); // currentObjectChanged - void onEditorMissionChanged(); // missionChanged + // sun group + SCP_vector getAvailableSunNames() const; + SCP_vector getMissionSunNames() const; + void setSelectedSunIndex(int index); + int getSelectedSunIndex() const; + void addMissionSunByName(const SCP_string& name); + void removeMissionSun(); + SCP_string getSunName() const; + void setSunName(const SCP_string& name); + int getSunPitch() const; + void setSunPitch(int deg); + int getSunBank() const; // unused for suns but added for consistency + void setSunBank(int deg); // unused for suns but added for consistency + int getSunHeading() const; + void setSunHeading(int deg); + float getSunScale() const; // uses scale_x for both x and y + void setSunScale(float v); private: // NOLINT(readability-redundant-access-specifiers) void refreshBackgroundPreview(); background_t& getActiveBackground() const; starfield_list_entry* getActiveBitmap() const; + starfield_list_entry* getActiveSun() const; int _selectedBitmapIndex = -1; // index into Backgrounds[Cur_background].bitmaps + int _selectedSunIndex = -1; // index into Backgrounds[Cur_background].suns }; } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp index 01519d855f8..0ca26887512 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp @@ -31,26 +31,38 @@ void BackgroundEditorDialog::initializeUi() ui->bitmapPitchSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); ui->bitmapBankSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); ui->bitmapHeadingSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); - ui->bitmapScaleXDoubleSpinBox->setRange(_model->getScaleLimit().first, _model->getScaleLimit().second); - ui->bitmapScaleYDoubleSpinBox->setRange(_model->getScaleLimit().first, _model->getScaleLimit().second); + ui->bitmapScaleXDoubleSpinBox->setRange(_model->getBitmapScaleLimit().first, _model->getBitmapScaleLimit().second); + ui->bitmapScaleYDoubleSpinBox->setRange(_model->getBitmapScaleLimit().first, _model->getBitmapScaleLimit().second); ui->bitmapDivXSpinBox->setRange(_model->getDivisionLimit().first, _model->getDivisionLimit().second); ui->bitmapDivYSpinBox->setRange(_model->getDivisionLimit().first, _model->getDivisionLimit().second); - refreshBitmapList(); const auto& names = _model->getAvailableBitmapNames(); for (const auto& s : names){ ui->bitmapTypeCombo->addItem(QString::fromStdString(s)); } + refreshBitmapList(); + + // Suns + ui->sunPitchSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); + ui->sunHeadingSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); + ui->sunScaleDoubleSpinBox->setRange(_model->getSunScaleLimit().first, _model->getSunScaleLimit().second); + + const auto& sun_names = _model->getAvailableSunNames(); + for (const auto& s : sun_names) { + ui->sunSelectionCombo->addItem(QString::fromStdString(s)); + } + + refreshSunList(); + } void BackgroundEditorDialog::updateUi() { util::SignalBlockers blockers(this); - // Backgrounds - // TODO - // Bitmaps + refreshBitmapList(); + refreshSunList(); } void BackgroundEditorDialog::refreshBitmapList() @@ -108,6 +120,53 @@ void BackgroundEditorDialog::updateBitmapControls() ui->bitmapDivYSpinBox->setValue(_model->getBitmapDivY()); } +void BackgroundEditorDialog::refreshSunList() +{ + util::SignalBlockers blockers(this); + + const auto names = _model->getMissionSunNames(); + + const int oldRow = ui->sunsListWidget->currentRow(); + ui->sunsListWidget->setUpdatesEnabled(false); + ui->sunsListWidget->clear(); + + QStringList items; + items.reserve(static_cast(names.size())); + for (const auto& s : names) + items << QString::fromStdString(s); + ui->sunsListWidget->addItems(items); + + if (!items.isEmpty()) { + const int clamped = qBound(0, oldRow, ui->sunsListWidget->count() - 1); + ui->sunsListWidget->setCurrentRow(clamped); + } + + ui->sunsListWidget->setUpdatesEnabled(true); + + updateSunControls(); +} + +void BackgroundEditorDialog::updateSunControls() +{ + util::SignalBlockers blockers(this); + + bool enabled = (_model->getSelectedSunIndex() >= 0); + + ui->changeSunButton->setEnabled(enabled); + ui->deleteSunButton->setEnabled(enabled); + ui->sunSelectionCombo->setEnabled(enabled); + ui->sunPitchSpin->setEnabled(enabled); + ui->sunHeadingSpin->setEnabled(enabled); + ui->sunScaleDoubleSpinBox->setEnabled(enabled); + + const int index = ui->sunSelectionCombo->findText(QString::fromStdString(_model->getSunName())); + ui->sunSelectionCombo->setCurrentIndex(index); + + ui->sunPitchSpin->setValue(_model->getSunPitch()); + ui->sunHeadingSpin->setValue(_model->getSunHeading()); + ui->sunScaleDoubleSpinBox->setValue(_model->getSunScale()); +} + void BackgroundEditorDialog::on_bitmapListWidget_currentRowChanged(int row) { _model->setSelectedBitmapIndex(row); @@ -176,9 +235,6 @@ void BackgroundEditorDialog::on_addBitmapButton_clicked() dlg.setWindowTitle("Select Background Bitmap"); dlg.setImageFilenames(qnames); - // Optional: preselect current - //dlg.setInitialSelection(QString::fromStdString(_model->getSquadLogo())); - if (dlg.exec() != QDialog::Accepted) return; @@ -223,4 +279,96 @@ void BackgroundEditorDialog::on_deleteBitmapButton_clicked() refreshBitmapList(); } +void BackgroundEditorDialog::on_sunListWidget_currentRowChanged(int row) +{ + _model->setSelectedSunIndex(row); + updateSunControls(); +} + +void BackgroundEditorDialog::on_sunSelectionCombo_currentIndexChanged(int index) +{ + if (index < 0) + return; + + const QString text = ui->sunSelectionCombo->itemText(index); + _model->setSunName(text.toUtf8().constData()); + refreshSunList(); +} + +void BackgroundEditorDialog::on_sunPitchSpin_valueChanged(int arg1) +{ + _model->setSunPitch(arg1); +} + +void BackgroundEditorDialog::on_sunHeadingSpin_valueChanged(int arg1) +{ + _model->setSunHeading(arg1); +} + +void BackgroundEditorDialog::on_sunScaleDoubleSpinBox_valueChanged(double arg1) +{ + _model->setSunScale(static_cast(arg1)); +} + +void BackgroundEditorDialog::on_addSunButton_clicked() +{ + const auto files = _model->getAvailableSunNames(); + if (files.empty()) { + QMessageBox::information(this, "Select Background Sun", "No suns found."); + return; + } + + QStringList qnames; + qnames.reserve(static_cast(files.size())); + for (const auto& s : files) + qnames << QString::fromStdString(s); + + ImagePickerDialog dlg(this); + dlg.setWindowTitle("Select Background Sun"); + dlg.setImageFilenames(qnames); + + if (dlg.exec() != QDialog::Accepted) + return; + + const SCP_string chosen = dlg.selectedFile().toUtf8().constData(); + _model->addMissionSunByName(chosen); + + refreshSunList(); +} + +void BackgroundEditorDialog::on_changeSunButton_clicked() +{ + const auto files = _model->getAvailableSunNames(); + if (files.empty()) { + QMessageBox::information(this, "Select Background Sun", "No suns found."); + return; + } + + QStringList qnames; + qnames.reserve(static_cast(files.size())); + for (const auto& s : files) + qnames << QString::fromStdString(s); + + ImagePickerDialog dlg(this); + dlg.setWindowTitle("Select Background Sun"); + dlg.setImageFilenames(qnames); + + // preselect current + dlg.setInitialSelection(QString::fromStdString(_model->getSunName())); + + if (dlg.exec() != QDialog::Accepted) + return; + + const SCP_string chosen = dlg.selectedFile().toUtf8().constData(); + _model->setSunName(chosen); + + refreshSunList(); +} + +void BackgroundEditorDialog::on_deleteSunButton_clicked() +{ + _model->removeMissionSun(); + refreshSunList(); +} + } // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h index bb8773be8ea..939482a4482 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -34,6 +34,16 @@ private slots: void on_changeBitmapButton_clicked(); void on_deleteBitmapButton_clicked(); + // Suns + void on_sunListWidget_currentRowChanged(int row); + void on_sunSelectionCombo_currentIndexChanged(int index); + void on_sunPitchSpin_valueChanged(int arg1); + void on_sunHeadingSpin_valueChanged(int arg1); + void on_sunScaleDoubleSpinBox_valueChanged(double arg1); + void on_addSunButton_clicked(); + void on_changeSunButton_clicked(); + void on_deleteSunButton_clicked(); + private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; @@ -43,6 +53,8 @@ private slots: void updateUi(); void refreshBitmapList(); void updateBitmapControls(); + void refreshSunList(); + void updateSunControls(); }; } // namespace fso::fred::dialogs diff --git a/qtfred/ui/BackgroundEditor.ui b/qtfred/ui/BackgroundEditor.ui index 62b1ca32ba3..e3850df101b 100644 --- a/qtfred/ui/BackgroundEditor.ui +++ b/qtfred/ui/BackgroundEditor.ui @@ -150,6 +150,9 @@ + + true + 16777215 @@ -164,6 +167,9 @@ + + true + 16777215 @@ -178,6 +184,9 @@ + + true + 16777215 @@ -202,6 +211,9 @@ 16777215.000000000000000 + + 0.100000000000000 + @@ -209,6 +221,9 @@ 16777215.000000000000000 + + 0.100000000000000 + @@ -327,6 +342,9 @@ 16777215.000000000000000 + + 0.100000000000000 + @@ -334,6 +352,9 @@ 16777215.000000000000000 + + 0.100000000000000 + @@ -484,6 +505,9 @@ 0 + + true + 16777215 @@ -497,6 +521,9 @@ 0 + + true + 16777215 @@ -510,6 +537,9 @@ 0 + + true + 16777215 @@ -558,17 +588,11 @@ - - - Qt::Horizontal - - - - 40 - 20 - + + + Change - + @@ -597,47 +621,39 @@ - - 16777215 - - - - - - - Bank + + true - - - - 16777215 - + Heading - + + + true + 16777215 - + Scale - + @@ -648,6 +664,9 @@ 16777215.000000000000000 + + 0.100000000000000 + @@ -681,6 +700,12 @@ + + 1 + + + 255 + Qt::Horizontal @@ -688,6 +713,12 @@ + + 1 + + + 255 + Qt::Horizontal @@ -695,6 +726,12 @@ + + 1 + + + 255 + Qt::Horizontal @@ -743,6 +780,9 @@ + + true + 16777215 @@ -757,6 +797,9 @@ + + true + 16777215 @@ -771,6 +814,9 @@ + + true + 16777215 @@ -842,6 +888,9 @@ + + 2000 + Qt::Horizontal From fe17997ca314aba88c59e895bccea5b087f3c2a0 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 16:37:34 -0500 Subject: [PATCH 431/466] nebula groups --- .../dialogs/BackgroundEditorDialogModel.cpp | 349 ++++++++++- .../dialogs/BackgroundEditorDialogModel.h | 47 +- .../src/ui/dialogs/BackgroundEditorDialog.cpp | 212 +++++++ .../src/ui/dialogs/BackgroundEditorDialog.h | 24 + qtfred/ui/BackgroundEditor.ui | 561 ++++++++++-------- 5 files changed, 939 insertions(+), 254 deletions(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index e301d549666..6dcd0527455 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -1,10 +1,15 @@ #include "FredApplication.h" #include "BackgroundEditorDialogModel.h" -//#include "mission/missionparse.h" +#include "math/bitarray.h" +#include "mission/missionparse.h" +#include "nebula/neb.h" +#include "nebula/neblightning.h" +#include "starfield/nebula.h" // TODO move this to common for both FREDs. Do not pass review if this is not done const static float delta = .00001f; +const static float default_nebula_range = 3000.0f; namespace fso::fred::dialogs { BackgroundEditorDialogModel::BackgroundEditorDialogModel(QObject* parent, EditorViewport* viewport) @@ -24,13 +29,19 @@ BackgroundEditorDialogModel::BackgroundEditorDialogModel(QObject* parent, Editor bool BackgroundEditorDialogModel::apply() { - // do the OnClose stuff here + // override dumb values with reasonable ones + // this is what original FRED does but it was a text edit field using atoi + // ours is a limited spinbox so this probably isn't necessary anymore?? + // Does this mean range can never be 0????????? + if (Neb2_awacs <= 0.00000001f) { + Neb2_awacs = 3000.0f; + } return true; } void BackgroundEditorDialogModel::reject() { - // do nothing? + // do nothing } void BackgroundEditorDialogModel::refreshBackgroundPreview() @@ -518,4 +529,336 @@ void BackgroundEditorDialogModel::setSunScale(float v) refreshBackgroundPreview(); } +SCP_vector BackgroundEditorDialogModel::getLightningNames() const +{ + SCP_vector out; + out.emplace_back(""); // legacy default + for (const auto& st : Storm_types) { + out.emplace_back(st.name); + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getNebulaPatternNames() const +{ + SCP_vector out; + out.emplace_back(""); // matches legacy combo where index 0 = none + for (const auto& neb : Neb2_bitmap_filenames) { + out.emplace_back(neb); + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getPoofNames() const +{ + SCP_vector out; + out.reserve(Poof_info.size()); + for (const auto& p : Poof_info) { + out.emplace_back(p.name); + } + return out; +} + +bool BackgroundEditorDialogModel::getFullNebulaEnabled() const +{ + return The_mission.flags[Mission::Mission_Flags::Fullneb]; +} + +void BackgroundEditorDialogModel::setFullNebulaEnabled(bool enabled) +{ + const bool currentlyEnabled = getFullNebulaEnabled(); + if (enabled == currentlyEnabled) { + return; + } + + if (enabled) { + The_mission.flags.set(Mission::Mission_Flags::Fullneb); + + // Set defaults if needed + if (Neb2_awacs <= 0.0f) { + modify(Neb2_awacs, default_nebula_range); + } + } else { + // Disable full nebula + The_mission.flags.remove(Mission::Mission_Flags::Fullneb); + modify(Neb2_awacs, -1.0f); + } + + set_modified(); +} + +float BackgroundEditorDialogModel::getFullNebulaRange() const +{ + // May be -1 if full nebula is disabled + return Neb2_awacs; +} + +void BackgroundEditorDialogModel::setFullNebulaRange(float range) +{ + modify(Neb2_awacs, range); +} + +SCP_string BackgroundEditorDialogModel::getNebulaFullPattern() const +{ + return (Neb2_texture_name[0] != '\0') ? SCP_string(Neb2_texture_name) : SCP_string(""); +} + +void BackgroundEditorDialogModel::setNebulaFullPattern(const SCP_string& name) +{ + if (lcase_equal(name, "")) { + strcpy_s(Neb2_texture_name, ""); + } else { + strcpy_s(Neb2_texture_name, name.c_str()); + } + + set_modified(); +} + +SCP_string BackgroundEditorDialogModel::getLightning() const +{ + // Return "" when engine stores "none" or empty + if (Mission_parse_storm_name[0] == '\0') + return ""; + SCP_string s = Mission_parse_storm_name; + if (lcase_equal(s, "none")) + return ""; + return s; +} + +void BackgroundEditorDialogModel::setLightning(const SCP_string& name) +{ + // Engine convention is the literal "none" for no storm + if (lcase_equal(name, "")) { + strcpy_s(Mission_parse_storm_name, "none"); + } else { + strcpy_s(Mission_parse_storm_name, name.c_str()); + } + set_modified(); +} + +SCP_vector BackgroundEditorDialogModel::getSelectedPoofs() const +{ + SCP_vector out; + for (size_t i = 0; i < Poof_info.size(); ++i) { + if (get_bit(Neb2_poof_flags.get(), i)) + out.emplace_back(Poof_info[i].name); + } + return out; +} + +void BackgroundEditorDialogModel::setSelectedPoofs(const SCP_vector& names) +{ + // Clear all, then set matching names + clear_all_bits(Neb2_poof_flags.get(), Poof_info.size()); + for (const auto& want : names) { + for (size_t i = 0; i < Poof_info.size(); ++i) { + if (!_stricmp(Poof_info[i].name, want.c_str())) { + set_bit(Neb2_poof_flags.get(), i); + break; + } + } + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getShipTrailsToggled() const +{ + return The_mission.flags[Mission::Mission_Flags::Toggle_ship_trails]; +} + +void BackgroundEditorDialogModel::setShipTrailsToggled(bool on) +{ + The_mission.flags.set(Mission::Mission_Flags::Toggle_ship_trails, on); + set_modified(); +} + +float BackgroundEditorDialogModel::getFogNearMultiplier() const +{ + return Neb2_fog_near_mult; +} + +void BackgroundEditorDialogModel::setFogNearMultiplier(float v) +{ + modify(Neb2_fog_near_mult, v); +} + +float BackgroundEditorDialogModel::getFogFarMultiplier() const +{ + return Neb2_fog_far_mult; +} + +void BackgroundEditorDialogModel::setFogFarMultiplier(float v) +{ + modify(Neb2_fog_far_mult, v); +} + +bool BackgroundEditorDialogModel::getDisplayBackgroundBitmaps() const +{ + return The_mission.flags[Mission::Mission_Flags::Fullneb_background_bitmaps]; +} + +void BackgroundEditorDialogModel::setDisplayBackgroundBitmaps(bool on) +{ + The_mission.flags.set(Mission::Mission_Flags::Fullneb_background_bitmaps, on); + set_modified(); +} + +bool BackgroundEditorDialogModel::getFogPaletteOverride() const +{ + return The_mission.flags[Mission::Mission_Flags::Neb2_fog_color_override]; +} + +void BackgroundEditorDialogModel::setFogPaletteOverride(bool on) +{ + The_mission.flags.set(Mission::Mission_Flags::Neb2_fog_color_override, on); + set_modified(); +} + +int BackgroundEditorDialogModel::getFogR() const +{ + return Neb2_fog_color[0]; +} + +void BackgroundEditorDialogModel::setFogR(int r) +{ + CLAMP(r, 0, 255) + const ubyte v = static_cast(r); + modify(Neb2_fog_color[0], v); +} + +int BackgroundEditorDialogModel::getFogG() const +{ + return Neb2_fog_color[1]; +} + +void BackgroundEditorDialogModel::setFogG(int g) +{ + CLAMP(g, 0, 255) + const ubyte v = static_cast(g); + modify(Neb2_fog_color[1], v); +} + +int BackgroundEditorDialogModel::getFogB() const +{ + return Neb2_fog_color[2]; +} + +void BackgroundEditorDialogModel::setFogB(int b) +{ + CLAMP(b, 0, 255) + const ubyte v = static_cast(b); + modify(Neb2_fog_color[0], v); +} + +SCP_vector BackgroundEditorDialogModel::getOldNebulaPatternOptions() const +{ + SCP_vector out; + out.emplace_back(""); + for (auto& neb : Nebula_filenames) { + out.emplace_back(neb); + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getOldNebulaColorOptions() const +{ + SCP_vector out; + out.reserve(NUM_NEBULA_COLORS); + for (auto& color : Nebula_colors) { + out.emplace_back(color); + } + return out; +} + +SCP_string BackgroundEditorDialogModel::getOldNebulaPattern() const +{ + if (Nebula_index < 0) + return ""; + + if (Nebula_index >= 0 && Nebula_index < NUM_NEBULAS) { + return Nebula_filenames[Nebula_index]; + } + + return SCP_string{}; +} + +void BackgroundEditorDialogModel::setOldNebulaPattern(const SCP_string& name) +{ + int newIndex = -1; + if (!name.empty() && stricmp(name.c_str(), "") != 0) { + for (int i = 0; i < NUM_NEBULAS; ++i) { + if (!stricmp(Nebula_filenames[i], name.c_str())) { + newIndex = i; + break; + } + } + } + + modify(Nebula_index, newIndex); +} + +SCP_string BackgroundEditorDialogModel::getOldNebulaColorName() const +{ + if (Mission_palette >= 0 && Mission_palette < NUM_NEBULA_COLORS) { + return Nebula_colors[Mission_palette]; + } + return SCP_string{}; +} + +void BackgroundEditorDialogModel::setOldNebulaColorName(const SCP_string& name) +{ + if (name.empty()) + return; + for (int i = 0; i < NUM_NEBULA_COLORS; ++i) { + if (!stricmp(Nebula_colors[i], name.c_str())) { + modify(Mission_palette, i); + return; + } + } + // name not found: ignore +} + +int BackgroundEditorDialogModel::getOldNebulaPitch() const +{ + return Nebula_pitch; +} + +void BackgroundEditorDialogModel::setOldNebulaPitch(int deg) +{ + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + if (Nebula_pitch != deg) { + Nebula_pitch = deg; + modify(Nebula_pitch, deg); + } +} + +int BackgroundEditorDialogModel::getOldNebulaBank() const +{ + return Nebula_bank; +} + +void BackgroundEditorDialogModel::setOldNebulaBank(int deg) +{ + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + if (Nebula_bank != deg) { + Nebula_bank = deg; + modify(Nebula_bank, deg); + } +} + +int BackgroundEditorDialogModel::getOldNebulaHeading() const +{ + return Nebula_heading; +} + +void BackgroundEditorDialogModel::setOldNebulaHeading(int deg) +{ + CLAMP(deg, getOrientLimit().first, getOrientLimit().second); + if (Nebula_heading != deg) { + Nebula_heading = deg; + modify(Nebula_heading, deg); + } +} + } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index 9045ec3c677..a3c13c1f9c3 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -69,7 +69,52 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { float getSunScale() const; // uses scale_x for both x and y void setSunScale(float v); - private: // NOLINT(readability-redundant-access-specifiers) + // nebula group + SCP_vector getLightningNames() const; + SCP_vector getNebulaPatternNames() const; + SCP_vector getPoofNames() const; + bool getFullNebulaEnabled() const; + void setFullNebulaEnabled(bool enabled); + float getFullNebulaRange() const; + void setFullNebulaRange(float range); + SCP_string getNebulaFullPattern() const; + void setNebulaFullPattern(const SCP_string& name); + SCP_string getLightning() const; + void setLightning(const SCP_string& name); + SCP_vector getSelectedPoofs() const; + void setSelectedPoofs(const SCP_vector& names); + bool getShipTrailsToggled() const; + void setShipTrailsToggled(bool on); + float getFogNearMultiplier() const; + void setFogNearMultiplier(float v); + float getFogFarMultiplier() const; + void setFogFarMultiplier(float v); + bool getDisplayBackgroundBitmaps() const; + void setDisplayBackgroundBitmaps(bool on); + bool getFogPaletteOverride() const; + void setFogPaletteOverride(bool on); + int getFogR() const; + void setFogR(int r); + int getFogG() const; + void setFogG(int g); + int getFogB() const; + void setFogB(int b); + + // old nebula group + SCP_vector getOldNebulaPatternOptions() const; + SCP_vector getOldNebulaColorOptions() const; + SCP_string getOldNebulaColorName() const; + void setOldNebulaColorName(const SCP_string& name); + SCP_string getOldNebulaPattern() const; + void setOldNebulaPattern(const SCP_string& name); + int getOldNebulaPitch() const; + void setOldNebulaPitch(int deg); + int getOldNebulaBank() const; + void setOldNebulaBank(int deg); + int getOldNebulaHeading() const; + void setOldNebulaHeading(int deg); + + private: void refreshBackgroundPreview(); background_t& getActiveBackground() const; starfield_list_entry* getActiveBitmap() const; diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp index 0ca26887512..c8a6a00c45d 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp @@ -55,6 +55,37 @@ void BackgroundEditorDialog::initializeUi() refreshSunList(); + // Nebula + const auto& nebula_names = _model->getNebulaPatternNames(); + for (const auto& s : nebula_names) { + ui->nebulaPatternCombo->addItem(QString::fromStdString(s)); + } + + const auto& lightning_names = _model->getLightningNames(); + for (const auto& s : lightning_names) { + ui->nebulaLightningCombo->addItem(QString::fromStdString(s)); + } + + const auto& poof_names = _model->getPoofNames(); + for (const auto& s : poof_names) { + ui->poofsListWidget->addItem(QString::fromStdString(s)); + } + + updateNebulaControls(); + + // Old nebula + const auto& old_nebula_names = _model->getOldNebulaPatternOptions(); + for (const auto& s : old_nebula_names) { + ui->oldNebulaPatternCombo->addItem(QString::fromStdString(s)); + } + + const auto& old_nebula_colors = _model->getOldNebulaColorOptions(); + for (const auto& s : old_nebula_colors) { + ui->oldNebulaColorCombo->addItem(QString::fromStdString(s)); + } + + updateOldNebulaControls(); + } void BackgroundEditorDialog::updateUi() @@ -167,6 +198,71 @@ void BackgroundEditorDialog::updateSunControls() ui->sunScaleDoubleSpinBox->setValue(_model->getSunScale()); } +void BackgroundEditorDialog::updateNebulaControls() +{ + util::SignalBlockers blockers(this); + + bool enabled = _model->getFullNebulaEnabled(); + ui->rangeSpinBox->setEnabled(enabled); + ui->nebulaPatternCombo->setEnabled(enabled); + ui->nebulaLightningCombo->setEnabled(enabled); + ui->poofsListWidget->setEnabled(enabled); + ui->shipTrailsCheckBox->setEnabled(enabled); + ui->fogNearDoubleSpinBox->setEnabled(enabled); + ui->fogFarDoubleSpinBox->setEnabled(enabled); + ui->displayBgsInNebulaCheckbox->setEnabled(enabled); + ui->overrideFogPaletteCheckBox->setEnabled(enabled); + + bool override = _model->getFogPaletteOverride(); + ui->fogOverrideRedSpinBox->setEnabled(enabled && override); + ui->fogOverrideGreenSpinBox->setEnabled(enabled && override); + ui->fogOverrideBlueSpinBox->setEnabled(enabled && override); + + ui->fullNebulaCheckBox->setChecked(enabled); + ui->rangeSpinBox->setValue(_model->getFullNebulaRange()); + ui->nebulaPatternCombo->setCurrentIndex(ui->nebulaPatternCombo->findText(QString::fromStdString(_model->getNebulaFullPattern()))); + ui->nebulaLightningCombo->setCurrentIndex(ui->nebulaLightningCombo->findText(QString::fromStdString(_model->getLightning()))); + + const auto& selected_poofs = _model->getSelectedPoofs(); + for (auto& poof : selected_poofs) { + auto items = ui->poofsListWidget->findItems(QString::fromStdString(poof), Qt::MatchExactly); + for (auto* item : items) { + item->setSelected(true); + } + } + + ui->shipTrailsCheckBox->setChecked(_model->getShipTrailsToggled()); + ui->fogNearDoubleSpinBox->setValue(static_cast(_model->getFogNearMultiplier())); + ui->fogFarDoubleSpinBox->setValue(static_cast(_model->getFogFarMultiplier())); + ui->displayBgsInNebulaCheckbox->setChecked(_model->getDisplayBackgroundBitmaps()); + ui->overrideFogPaletteCheckBox->setChecked(override); + ui->fogOverrideRedSpinBox->setValue(_model->getFogR()); + ui->fogOverrideGreenSpinBox->setValue(_model->getFogG()); + ui->fogOverrideBlueSpinBox->setValue(_model->getFogB()); + + updateOldNebulaControls(); +} + +void BackgroundEditorDialog::updateOldNebulaControls() +{ + util::SignalBlockers blockers(this); + + const bool enabled = !_model->getFullNebulaEnabled(); + const bool old_enabled = _model->getOldNebulaPattern() != ""; + + ui->oldNebulaPatternCombo->setEnabled(enabled); + ui->oldNebulaColorCombo->setEnabled(enabled && old_enabled); + ui->oldNebulaPitchSpinBox->setEnabled(enabled && old_enabled); + ui->oldNebulaBankSpinBox->setEnabled(enabled && old_enabled); + ui->oldNebulaHeadingSpinBox->setEnabled(enabled && old_enabled); + + ui->oldNebulaPatternCombo->setCurrentIndex(ui->oldNebulaPatternCombo->findText(QString::fromStdString(_model->getOldNebulaPattern()))); + ui->oldNebulaColorCombo->setCurrentIndex(ui->oldNebulaColorCombo->findText(QString::fromStdString(_model->getOldNebulaColorName()))); + ui->oldNebulaPitchSpinBox->setValue(_model->getOldNebulaPitch()); + ui->oldNebulaBankSpinBox->setValue(_model->getOldNebulaBank()); + ui->oldNebulaHeadingSpinBox->setValue(_model->getOldNebulaHeading()); +} + void BackgroundEditorDialog::on_bitmapListWidget_currentRowChanged(int row) { _model->setSelectedBitmapIndex(row); @@ -371,4 +467,120 @@ void BackgroundEditorDialog::on_deleteSunButton_clicked() refreshSunList(); } +void BackgroundEditorDialog::on_fullNebulaCheckBox_toggled(bool checked) +{ + _model->setFullNebulaEnabled(checked); + updateNebulaControls(); +} + +void BackgroundEditorDialog::on_rangeSpinBox_valueChanged(int arg1) +{ + _model->setFullNebulaRange(static_cast(arg1)); +} + +void BackgroundEditorDialog::on_nebulaPatternCombo_currentIndexChanged(int index) +{ + if (index < 0) + return; + + const QString text = ui->nebulaPatternCombo->itemText(index); + _model->setNebulaFullPattern(text.toUtf8().constData()); +} + +void BackgroundEditorDialog::on_nebulaLightningCombo_currentIndexChanged(int index) +{ + if (index < 0) + return; + + const QString text = ui->nebulaLightningCombo->itemText(index); + _model->setLightning(text.toUtf8().constData()); +} + +void BackgroundEditorDialog::on_poofsListWidget_itemSelectionChanged() +{ + QStringList selected; + for (auto* item : ui->poofsListWidget->selectedItems()) { + selected << item->text(); + } + SCP_vector selected_std; + selected_std.reserve(static_cast(selected.size())); + for (const auto& s : selected) { + selected_std.emplace_back(s.toUtf8().constData()); + } + _model->setSelectedPoofs(selected_std); +} + +void BackgroundEditorDialog::on_shipTrailsCheckBox_toggled(bool checked) +{ + _model->setShipTrailsToggled(checked); +} + +void BackgroundEditorDialog::on_fogNearDoubleSpinBox_valueChanged(double arg1) +{ + _model->setFogNearMultiplier(static_cast(arg1)); +} + +void BackgroundEditorDialog::on_fogFarDoubleSpinBox_valueChanged(double arg1) +{ + _model->setFogFarMultiplier(static_cast(arg1)); +} + +void BackgroundEditorDialog::on_displayBgsInNebulaCheckbox_toggled(bool checked) +{ + _model->setDisplayBackgroundBitmaps(checked); +} + +void BackgroundEditorDialog::on_overrideFogPaletteCheckBox_toggled(bool checked) +{ + _model->setFogPaletteOverride(checked); + updateNebulaControls(); +} + +void BackgroundEditorDialog::on_fogOverrideRedSpinBox_valueChanged(int arg1) +{ + _model->setFogR(arg1); +} + +void BackgroundEditorDialog::on_fogOverrideGreenSpinBox_valueChanged(int arg1) +{ + _model->setFogG(arg1); +} + +void BackgroundEditorDialog::on_fogOverrideBlueSpinBox_valueChanged(int arg1) +{ + _model->setFogB(arg1); +} + +void BackgroundEditorDialog::on_oldNebulaPatternCombo_currentIndexChanged(int index) +{ + if (index < 0) + return; + const QString text = ui->oldNebulaPatternCombo->itemText(index); + _model->setOldNebulaPattern(text.toUtf8().constData()); + updateOldNebulaControls(); +} + +void BackgroundEditorDialog::on_oldNebulaColorCombo_currentIndexChanged(int index) +{ + if (index < 0) + return; + const QString text = ui->oldNebulaColorCombo->itemText(index); + _model->setOldNebulaColorName(text.toUtf8().constData()); +} + +void BackgroundEditorDialog::on_oldNebulaPitchSpinBox_valueChanged(int arg1) +{ + _model->setOldNebulaPitch(arg1); +} + +void BackgroundEditorDialog::on_oldNebulaBankSpinBox_valueChanged(int arg1) +{ + _model->setOldNebulaBank(arg1); +} + +void BackgroundEditorDialog::on_oldNebulaHeadingSpinBox_valueChanged(int arg1) +{ + _model->setOldNebulaHeading(arg1); +} + } // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h index 939482a4482..599d2834905 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -44,6 +44,28 @@ private slots: void on_changeSunButton_clicked(); void on_deleteSunButton_clicked(); + // Nebula + void on_fullNebulaCheckBox_toggled(bool checked); + void on_rangeSpinBox_valueChanged(int arg1); + void on_nebulaPatternCombo_currentIndexChanged(int index); + void on_nebulaLightningCombo_currentIndexChanged(int index); + void on_poofsListWidget_itemSelectionChanged(); + void on_shipTrailsCheckBox_toggled(bool checked); + void on_fogNearDoubleSpinBox_valueChanged(double arg1); + void on_fogFarDoubleSpinBox_valueChanged(double arg1); + void on_displayBgsInNebulaCheckbox_toggled(bool checked); + void on_overrideFogPaletteCheckBox_toggled(bool checked); + void on_fogOverrideRedSpinBox_valueChanged(int arg1); + void on_fogOverrideGreenSpinBox_valueChanged(int arg1); + void on_fogOverrideBlueSpinBox_valueChanged(int arg1); + + // Old Nebula + void on_oldNebulaPatternCombo_currentIndexChanged(int index); + void on_oldNebulaColorCombo_currentIndexChanged(int index); + void on_oldNebulaPitchSpinBox_valueChanged(int arg1); + void on_oldNebulaBankSpinBox_valueChanged(int arg1); + void on_oldNebulaHeadingSpinBox_valueChanged(int arg1); + private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; @@ -55,6 +77,8 @@ private slots: void updateBitmapControls(); void refreshSunList(); void updateSunControls(); + void updateNebulaControls(); + void updateOldNebulaControls(); }; } // namespace fso::fred::dialogs diff --git a/qtfred/ui/BackgroundEditor.ui b/qtfred/ui/BackgroundEditor.ui index e3850df101b..e63f2f8d3db 100644 --- a/qtfred/ui/BackgroundEditor.ui +++ b/qtfred/ui/BackgroundEditor.ui @@ -6,8 +6,8 @@ 0 0 - 981 - 814 + 1041 + 862 @@ -93,174 +93,331 @@ - + - - - - - Bitmaps - - + + + Bitmaps + + + + - + + + + - + + + Add + + - - - - - Add - - - - - - - Change - - - - - - - Delete - - - - + + + Change + + + + + + + Delete + + + + + + + + + - - - + + + + + Pitch + + + + + + true + + + 16777215 + + + + + + + Bank + + + + + + + true + + + 16777215 + + + + + + + Heading + + + + + + + true + + + 16777215 + + + + + + + + + Scale (x/y) + + + Qt::AlignCenter + + + + + - - - - - Pitch - - - - - - - true - - - 16777215 - - - - - - - Bank - - - - - - - true - - - 16777215 - - - - - - - Heading - - - - - - - true - - - 16777215 - - - - + + + 16777215.000000000000000 + + + 0.100000000000000 + + - - - Scale (x/y) + + + 16777215.000000000000000 - - Qt::AlignCenter + + 0.100000000000000 + + + + + + # divisions (x/y) + + + Qt::AlignCenter + + + + + - - - - - 16777215.000000000000000 - - - 0.100000000000000 - - - - - - - 16777215.000000000000000 - - - 0.100000000000000 - - - - + + + 16777215 + + - + + + 16777215 + + + + + + + + + + + + + + Suns + + + + + + + + + + + - # divisions (x/y) + Add - - Qt::AlignCenter + + + + + + Change - - - - - 16777215 - - - - - - - 16777215 - - - - + + + Delete + + - - + + + + + + + + + + + + Pitch + + + + + + + true + + + 16777215 + + + + + + + Heading + + + + + + + true + + + 16777215 + + + + + + + Scale + + + + + + + + 75 + 0 + + + + 16777215.000000000000000 + + + 0.100000000000000 + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Save sun/bitmap angles to mission in correct format + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + + + + @@ -310,6 +467,9 @@ 16777215 + + 3000 + @@ -345,6 +505,9 @@ 0.100000000000000 + + 1.000000000000000 + @@ -355,6 +518,9 @@ 0.100000000000000 + + 1.000000000000000 + @@ -437,7 +603,11 @@ - + + + QAbstractItemView::MultiSelection + + @@ -567,115 +737,6 @@ - - - - suns - - - - - - - - - - - - - Add - - - - - - - Change - - - - - - - Delete - - - - - - - - - - - - - - - - - - Pitch - - - - - - - true - - - 16777215 - - - - - - - Heading - - - - - - - true - - - 16777215 - - - - - - - Scale - - - - - - - - 75 - 0 - - - - 16777215.000000000000000 - - - 0.100000000000000 - - - - - - - - - - From cb4e132df2a2a24ceca9347b8cbd9d85c87ae1f6 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 17:12:34 -0500 Subject: [PATCH 432/466] ambient controls --- .../dialogs/BackgroundEditorDialogModel.cpp | 57 +++++++++++- .../dialogs/BackgroundEditorDialogModel.h | 8 ++ .../src/ui/dialogs/BackgroundEditorDialog.cpp | 87 +++++++++++++++++++ .../src/ui/dialogs/BackgroundEditorDialog.h | 8 ++ qtfred/ui/BackgroundEditor.ui | 50 +++++++---- 5 files changed, 191 insertions(+), 19 deletions(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index 6dcd0527455..444646f3507 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -1,6 +1,7 @@ #include "FredApplication.h" #include "BackgroundEditorDialogModel.h" +#include "graphics/light.h" #include "math/bitarray.h" #include "mission/missionparse.h" #include "nebula/neb.h" @@ -748,7 +749,7 @@ void BackgroundEditorDialogModel::setFogB(int b) { CLAMP(b, 0, 255) const ubyte v = static_cast(b); - modify(Neb2_fog_color[0], v); + modify(Neb2_fog_color[2], v); } SCP_vector BackgroundEditorDialogModel::getOldNebulaPatternOptions() const @@ -861,4 +862,58 @@ void BackgroundEditorDialogModel::setOldNebulaHeading(int deg) } } +int BackgroundEditorDialogModel::getAmbientR() const +{ + return The_mission.ambient_light_level & 0xff; +} + +void BackgroundEditorDialogModel::setAmbientR(int r) +{ + CLAMP(r, 1, 255); + + const int g = getAmbientG(), b = getAmbientB(); + const int newCol = (r) | (g << 8) | (b << 16); + + modify(The_mission.ambient_light_level, newCol); + + gr_set_ambient_light(r, g, b); + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getAmbientG() const +{ + return (The_mission.ambient_light_level >> 8) & 0xff; +} + +void BackgroundEditorDialogModel::setAmbientG(int g) +{ + CLAMP(g, 1, 255); + + const int r = getAmbientR(), b = getAmbientB(); + const int newCol = (r) | (g << 8) | (b << 16); + + modify(The_mission.ambient_light_level, newCol); + + gr_set_ambient_light(r, g, b); + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getAmbientB() const +{ + return (The_mission.ambient_light_level >> 16) & 0xff; +} + +void BackgroundEditorDialogModel::setAmbientB(int b) +{ + CLAMP(b, 1, 255); + + const int r = getAmbientR(), g = getAmbientG(); + const int newCol = (r) | (g << 8) | (b << 16); + + modify(The_mission.ambient_light_level, newCol); + + gr_set_ambient_light(r, g, b); + refreshBackgroundPreview(); +} + } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index a3c13c1f9c3..364707dacf7 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -114,6 +114,14 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { int getOldNebulaHeading() const; void setOldNebulaHeading(int deg); + // ambient light group + int getAmbientR() const; + void setAmbientR(int r); + int getAmbientG() const; + void setAmbientG(int g); + int getAmbientB() const; + void setAmbientB(int b); + private: void refreshBackgroundPreview(); background_t& getActiveBackground() const; diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp index c8a6a00c45d..599bdf30020 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp @@ -71,6 +71,8 @@ void BackgroundEditorDialog::initializeUi() ui->poofsListWidget->addItem(QString::fromStdString(s)); } + ui->fogSwatch->setFrameShape(QFrame::Box); + updateNebulaControls(); // Old nebula @@ -86,6 +88,12 @@ void BackgroundEditorDialog::initializeUi() updateOldNebulaControls(); + // Ambient light + ui->ambientSwatch->setMinimumSize(28, 28); + ui->ambientSwatch->setFrameShape(QFrame::Box); + + updateAmbientLightControls(); + } void BackgroundEditorDialog::updateUi() @@ -240,9 +248,23 @@ void BackgroundEditorDialog::updateNebulaControls() ui->fogOverrideGreenSpinBox->setValue(_model->getFogG()); ui->fogOverrideBlueSpinBox->setValue(_model->getFogB()); + updateFogSwatch(); + updateOldNebulaControls(); } +void BackgroundEditorDialog::updateFogSwatch() +{ + const int r = _model->getFogR(); + const int g = _model->getFogG(); + const int b = _model->getFogB(); + ui->fogSwatch->setStyleSheet(QString("background: rgb(%1,%2,%3);" + "border: 1px solid #444; border-radius: 3px;") + .arg(r) + .arg(g) + .arg(b)); +} + void BackgroundEditorDialog::updateOldNebulaControls() { util::SignalBlockers blockers(this); @@ -263,6 +285,29 @@ void BackgroundEditorDialog::updateOldNebulaControls() ui->oldNebulaHeadingSpinBox->setValue(_model->getOldNebulaHeading()); } +void BackgroundEditorDialog::updateAmbientLightControls() +{ + util::SignalBlockers blockers(this); + + const int r = _model->getAmbientR(); + const int g = _model->getAmbientG(); + const int b = _model->getAmbientB(); + + ui->ambientLightRedSlider->setValue(r); + ui->ambientLightGreenSlider->setValue(g); + ui->ambientLightBlueSlider->setValue(b); + + QString redText = "R: " + QString::number(r); + QString greenText = "G: " + QString::number(g); + QString blueText = "B: " + QString::number(b); + + ui->ambientLightRedLabel->setText(redText); + ui->ambientLightGreenLabel->setText(greenText); + ui->ambientLightBlueLabel->setText(blueText); + + updateAmbientSwatch(); +} + void BackgroundEditorDialog::on_bitmapListWidget_currentRowChanged(int row) { _model->setSelectedBitmapIndex(row); @@ -539,16 +584,19 @@ void BackgroundEditorDialog::on_overrideFogPaletteCheckBox_toggled(bool checked) void BackgroundEditorDialog::on_fogOverrideRedSpinBox_valueChanged(int arg1) { _model->setFogR(arg1); + updateFogSwatch(); } void BackgroundEditorDialog::on_fogOverrideGreenSpinBox_valueChanged(int arg1) { _model->setFogG(arg1); + updateFogSwatch(); } void BackgroundEditorDialog::on_fogOverrideBlueSpinBox_valueChanged(int arg1) { _model->setFogB(arg1); + updateFogSwatch(); } void BackgroundEditorDialog::on_oldNebulaPatternCombo_currentIndexChanged(int index) @@ -583,4 +631,43 @@ void BackgroundEditorDialog::on_oldNebulaHeadingSpinBox_valueChanged(int arg1) _model->setOldNebulaHeading(arg1); } +void BackgroundEditorDialog::on_ambientLightRedSlider_valueChanged(int value) +{ + _model->setAmbientR(value); + + QString text = "R: " + QString::number(value); + ui->ambientLightRedLabel->setText(text); + updateAmbientSwatch(); +} + +void BackgroundEditorDialog::on_ambientLightGreenSlider_valueChanged(int value) +{ + _model->setAmbientG(value); + + QString text = "G: " + QString::number(value); + ui->ambientLightGreenLabel->setText(text); + updateAmbientSwatch(); +} + +void BackgroundEditorDialog::on_ambientLightBlueSlider_valueChanged(int value) +{ + _model->setAmbientB(value); + + QString text = "B: " + QString::number(value); + ui->ambientLightBlueLabel->setText(text); + updateAmbientSwatch(); +} + +void BackgroundEditorDialog::updateAmbientSwatch() +{ + const int r = _model->getAmbientR(); + const int g = _model->getAmbientG(); + const int b = _model->getAmbientB(); + ui->ambientSwatch->setStyleSheet(QString("background: rgb(%1,%2,%3);" + "border: 1px solid #444; border-radius: 3px;") + .arg(r) + .arg(g) + .arg(b)); +} + } // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h index 599d2834905..ee9e5e3d316 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -66,6 +66,11 @@ private slots: void on_oldNebulaBankSpinBox_valueChanged(int arg1); void on_oldNebulaHeadingSpinBox_valueChanged(int arg1); + // Ambient Light + void on_ambientLightRedSlider_valueChanged(int value); + void on_ambientLightGreenSlider_valueChanged(int value); + void on_ambientLightBlueSlider_valueChanged(int value); + private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; @@ -78,7 +83,10 @@ private slots: void refreshSunList(); void updateSunControls(); void updateNebulaControls(); + void updateFogSwatch(); void updateOldNebulaControls(); + void updateAmbientLightControls(); + void updateAmbientSwatch(); }; } // namespace fso::fred::dialogs diff --git a/qtfred/ui/BackgroundEditor.ui b/qtfred/ui/BackgroundEditor.ui index e63f2f8d3db..e81784f23ac 100644 --- a/qtfred/ui/BackgroundEditor.ui +++ b/qtfred/ui/BackgroundEditor.ui @@ -539,6 +539,13 @@ + + + + + + + @@ -745,13 +752,6 @@ - - - - B: 0 - - - @@ -772,8 +772,22 @@ - - + + + + B: 0 + + + + + + + R: 0 + + + + + 1 @@ -785,8 +799,15 @@ - - + + + + + + + + + 1 @@ -798,13 +819,6 @@ - - - - R: 0 - - - From aa7fc6dafdac83e30bdb2d21ba37a41828349d10 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 19:20:29 -0500 Subject: [PATCH 433/466] skybox section --- .../dialogs/BackgroundEditorDialogModel.cpp | 192 ++++++++++++++++++ .../dialogs/BackgroundEditorDialogModel.h | 22 ++ .../src/ui/dialogs/BackgroundEditorDialog.cpp | 109 ++++++++++ .../src/ui/dialogs/BackgroundEditorDialog.h | 15 ++ qtfred/ui/BackgroundEditor.ui | 4 +- 5 files changed, 340 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index 444646f3507..32b8e0aeb61 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -48,6 +48,8 @@ void BackgroundEditorDialogModel::reject() void BackgroundEditorDialogModel::refreshBackgroundPreview() { stars_load_background(Cur_background); // rebuild instances from Backgrounds[] + stars_set_background_model(The_mission.skybox_model, nullptr, The_mission.skybox_flags); // rebuild skybox + stars_set_background_orientation(&The_mission.skybox_orientation); _editor->missionChanged(); } @@ -916,4 +918,194 @@ void BackgroundEditorDialogModel::setAmbientB(int b) refreshBackgroundPreview(); } +std::string BackgroundEditorDialogModel::getSkyboxModelName() const +{ + return The_mission.skybox_model; +} +void BackgroundEditorDialogModel::setSkyboxModelName(const std::string& name) +{ + // empty string = no skybox + if (std::strncmp(The_mission.skybox_model, name.c_str(), NAME_LENGTH) != 0) { + std::memset(The_mission.skybox_model, 0, sizeof(The_mission.skybox_model)); + std::strncpy(The_mission.skybox_model, name.c_str(), NAME_LENGTH - 1); + } + + set_modified(); + refreshBackgroundPreview(); +} + +bool BackgroundEditorDialogModel::getSkyboxNoLighting() const +{ + return (The_mission.skybox_flags & MR_NO_LIGHTING) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxNoLighting(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_NO_LIGHTING; + } else { + The_mission.skybox_flags &= ~MR_NO_LIGHTING; + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getSkyboxAllTransparent() const +{ + return (The_mission.skybox_flags & MR_ALL_XPARENT) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxAllTransparent(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_ALL_XPARENT; + } else { + The_mission.skybox_flags &= ~MR_ALL_XPARENT; + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getSkyboxNoZbuffer() const +{ + return (The_mission.skybox_flags & MR_NO_ZBUFFER) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxNoZbuffer(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_NO_ZBUFFER; + } else { + The_mission.skybox_flags &= ~MR_NO_ZBUFFER; + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getSkyboxNoCull() const +{ + return (The_mission.skybox_flags & MR_NO_CULL) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxNoCull(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_NO_CULL; + } else { + The_mission.skybox_flags &= ~MR_NO_CULL; + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getSkyboxNoGlowmaps() const +{ + return (The_mission.skybox_flags & MR_NO_GLOWMAPS) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxNoGlowmaps(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_NO_GLOWMAPS; + } else { + The_mission.skybox_flags &= ~MR_NO_GLOWMAPS; + } + + set_modified(); +} + +bool BackgroundEditorDialogModel::getSkyboxForceClamp() const +{ + return (The_mission.skybox_flags & MR_FORCE_CLAMP) != 0; +} + +void BackgroundEditorDialogModel::setSkyboxForceClamp(bool on) +{ + if (on) { + The_mission.skybox_flags |= MR_FORCE_CLAMP; + } else { + The_mission.skybox_flags &= ~MR_FORCE_CLAMP; + } + + set_modified(); +} + +int BackgroundEditorDialogModel::getSkyboxPitch() const +{ + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + int d = static_cast(fl2ir(fl_degrees(a.p))); + if (d < 0) + d += 360; + if (d >= 360) + d -= 360; + return d; +} + +void BackgroundEditorDialogModel::setSkyboxPitch(int deg) +{ + CLAMP(deg, 0, 359); + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + const int cur = static_cast(fl2ir(fl_degrees(a.p))); + if (cur != deg) { + a.p = fl_radians(static_cast(deg)); + vm_angles_2_matrix(&The_mission.skybox_orientation, &a); + set_modified(); + refreshBackgroundPreview(); + } +} + +int BackgroundEditorDialogModel::getSkyboxBank() const +{ + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + int d = static_cast(fl2ir(fl_degrees(a.b))); + if (d < 0) + d += 360; + if (d >= 360) + d -= 360; + return d; +} + +void BackgroundEditorDialogModel::setSkyboxBank(int deg) +{ + CLAMP(deg, 0, 359); + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + const int cur = static_cast(fl2ir(fl_degrees(a.b))); + if (cur != deg) { + a.b = fl_radians(static_cast(deg)); + vm_angles_2_matrix(&The_mission.skybox_orientation, &a); + set_modified(); + refreshBackgroundPreview(); + } +} + +int BackgroundEditorDialogModel::getSkyboxHeading() const +{ + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + int d = static_cast(fl2ir(fl_degrees(a.h))); + if (d < 0) + d += 360; + if (d >= 360) + d -= 360; + return d; +} + +void BackgroundEditorDialogModel::setSkyboxHeading(int deg) +{ + CLAMP(deg, 0, 359); + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + const int cur = static_cast(fl2ir(fl_degrees(a.h))); + if (cur != deg) { + a.h = fl_radians(static_cast(deg)); + vm_angles_2_matrix(&The_mission.skybox_orientation, &a); + set_modified(); + refreshBackgroundPreview(); + } +} + } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index 364707dacf7..1b077acc162 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -122,6 +122,28 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { int getAmbientB() const; void setAmbientB(int b); + // skybox group + std::string getSkyboxModelName() const; + void setSkyboxModelName(const std::string& name); + bool getSkyboxNoLighting() const; + void setSkyboxNoLighting(bool on); + bool getSkyboxAllTransparent() const; + void setSkyboxAllTransparent(bool on); + bool getSkyboxNoZbuffer() const; + void setSkyboxNoZbuffer(bool on); + bool getSkyboxNoCull() const; + void setSkyboxNoCull(bool on); + bool getSkyboxNoGlowmaps() const; + void setSkyboxNoGlowmaps(bool on); + bool getSkyboxForceClamp() const; + void setSkyboxForceClamp(bool on); + int getSkyboxPitch() const; + void setSkyboxPitch(int deg); + int getSkyboxBank() const; + void setSkyboxBank(int deg); + int getSkyboxHeading() const; + void setSkyboxHeading(int deg); + private: void refreshBackgroundPreview(); background_t& getActiveBackground() const; diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp index 599bdf30020..977f7689002 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp @@ -4,6 +4,9 @@ #include "ui_BackgroundEditor.h" #include +#include +#include +#include namespace fso::fred::dialogs { @@ -94,6 +97,10 @@ void BackgroundEditorDialog::initializeUi() updateAmbientLightControls(); + // Skybox + + updateSkyboxControls(); + } void BackgroundEditorDialog::updateUi() @@ -308,6 +315,34 @@ void BackgroundEditorDialog::updateAmbientLightControls() updateAmbientSwatch(); } +void BackgroundEditorDialog::updateSkyboxControls() +{ + util::SignalBlockers blockers(this); + + bool enabled = !_model->getSkyboxModelName().empty(); + + ui->skyboxPitchSpin->setEnabled(enabled); + ui->skyboxBankSpin->setEnabled(enabled); + ui->skyboxHeadingSpin->setEnabled(enabled); + ui->noLightingCheckBox->setEnabled(enabled); + ui->transparentCheckBox->setEnabled(enabled); + ui->forceClampCheckBox->setEnabled(enabled); + ui->noZBufferCheckBox->setEnabled(enabled); + ui->noCullCheckBox->setEnabled(enabled); + ui->noGlowMapsCheckBox->setEnabled(enabled); + + ui->skyboxEdit->setText(QString::fromStdString(_model->getSkyboxModelName())); + ui->skyboxPitchSpin->setValue(_model->getSkyboxPitch()); + ui->skyboxBankSpin->setValue(_model->getSkyboxBank()); + ui->skyboxHeadingSpin->setValue(_model->getSkyboxHeading()); + ui->noLightingCheckBox->setChecked(_model->getSkyboxNoLighting()); + ui->transparentCheckBox->setChecked(_model->getSkyboxAllTransparent()); + ui->forceClampCheckBox->setChecked(_model->getSkyboxForceClamp()); + ui->noZBufferCheckBox->setChecked(_model->getSkyboxNoZbuffer()); + ui->noCullCheckBox->setChecked(_model->getSkyboxNoCull()); + ui->noGlowMapsCheckBox->setChecked(_model->getSkyboxNoGlowmaps()); +} + void BackgroundEditorDialog::on_bitmapListWidget_currentRowChanged(int row) { _model->setSelectedBitmapIndex(row); @@ -670,4 +705,78 @@ void BackgroundEditorDialog::updateAmbientSwatch() .arg(b)); } +void BackgroundEditorDialog::on_skyboxButton_clicked() +{ + QSettings settings("QtFRED", "BackgroundEditor"); + const QString lastDir = settings.value("skybox/lastDir", QDir::homePath()).toString(); + + const QString path = + QFileDialog::getOpenFileName(this, tr("Select Skybox Model"), lastDir, tr("FS2 Models (*.pof);;All Files (*)")); + if (path.isEmpty()) + return; + + const QFileInfo fi(path); + settings.setValue("skybox/lastDir", fi.absolutePath()); + + const QString baseName = fi.completeBaseName(); + _model->setSkyboxModelName(baseName.toStdString()); + + updateSkyboxControls(); +} + +void BackgroundEditorDialog::on_skyboxEdit_textChanged(const QString& arg1) +{ + _model->setSkyboxModelName(arg1.toUtf8().constData()); +} + +void BackgroundEditorDialog::on_skyboxPitchSpin_valueChanged(int arg1) +{ + _model->setSkyboxPitch(arg1); +} + +void BackgroundEditorDialog::on_skyboxBankSpin_valueChanged(int arg1) +{ + _model->setSkyboxBank(arg1); +} + +void BackgroundEditorDialog::on_skyboxHeadingSpin_valueChanged(int arg1) +{ + _model->setSkyboxHeading(arg1); +} + +void BackgroundEditorDialog::on_skyboxNoLightingCheckBox_toggled(bool checked) +{ + _model->setSkyboxNoLighting(checked); +} + +void BackgroundEditorDialog::on_noLightingCheckBox_toggled(bool checked) +{ + _model->setSkyboxNoLighting(checked); +} + +void BackgroundEditorDialog::on_transparentCheckBox_toggled(bool checked) +{ + _model->setSkyboxAllTransparent(checked); +} + +void BackgroundEditorDialog::on_forceClampCheckBox_toggled(bool checked) +{ + _model->setSkyboxForceClamp(checked); +} + +void BackgroundEditorDialog::on_noZBufferCheckBox_toggled(bool checked) +{ + _model->setSkyboxNoZbuffer(checked); +} + +void BackgroundEditorDialog::on_noCullCheckBox_toggled(bool checked) +{ + _model->setSkyboxNoCull(checked); +} + +void BackgroundEditorDialog::on_noGlowmapsCheckBox_toggled(bool checked) +{ + _model->setSkyboxNoGlowmaps(checked); +} + } // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h index ee9e5e3d316..ec8a5bcbfdd 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -71,6 +71,20 @@ private slots: void on_ambientLightGreenSlider_valueChanged(int value); void on_ambientLightBlueSlider_valueChanged(int value); + // Skybox + void on_skyboxButton_clicked(); + void on_skyboxEdit_textChanged(const QString& arg1); + void on_skyboxPitchSpin_valueChanged(int arg1); + void on_skyboxBankSpin_valueChanged(int arg1); + void on_skyboxHeadingSpin_valueChanged(int arg1); + void on_skyboxNoLightingCheckBox_toggled(bool checked); + void on_noLightingCheckBox_toggled(bool checked); + void on_transparentCheckBox_toggled(bool checked); + void on_forceClampCheckBox_toggled(bool checked); + void on_noZBufferCheckBox_toggled(bool checked); + void on_noCullCheckBox_toggled(bool checked); + void on_noGlowmapsCheckBox_toggled(bool checked); + private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; @@ -87,6 +101,7 @@ private slots: void updateOldNebulaControls(); void updateAmbientLightControls(); void updateAmbientSwatch(); + void updateSkyboxControls(); }; } // namespace fso::fred::dialogs diff --git a/qtfred/ui/BackgroundEditor.ui b/qtfred/ui/BackgroundEditor.ui index e81784f23ac..6800a49dfe4 100644 --- a/qtfred/ui/BackgroundEditor.ui +++ b/qtfred/ui/BackgroundEditor.ui @@ -930,14 +930,14 @@ - + No cull - + No glow maps From 614f510d0ab6f0ee47d75159d368f38600b22840 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 19:42:22 -0500 Subject: [PATCH 434/466] misc group stuff --- .../dialogs/BackgroundEditorDialogModel.cpp | 68 +++++++++++++++++- .../dialogs/BackgroundEditorDialogModel.h | 16 ++++- .../src/ui/dialogs/BackgroundEditorDialog.cpp | 70 ++++++++++++++++++- .../src/ui/dialogs/BackgroundEditorDialog.h | 8 +++ 4 files changed, 156 insertions(+), 6 deletions(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index 32b8e0aeb61..1094d5f1919 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -7,6 +7,7 @@ #include "nebula/neb.h" #include "nebula/neblightning.h" #include "starfield/nebula.h" +#include "lighting/lighting_profiles.h" // TODO move this to common for both FREDs. Do not pass review if this is not done const static float delta = .00001f; @@ -918,11 +919,11 @@ void BackgroundEditorDialogModel::setAmbientB(int b) refreshBackgroundPreview(); } -std::string BackgroundEditorDialogModel::getSkyboxModelName() const +SCP_string BackgroundEditorDialogModel::getSkyboxModelName() const { return The_mission.skybox_model; } -void BackgroundEditorDialogModel::setSkyboxModelName(const std::string& name) +void BackgroundEditorDialogModel::setSkyboxModelName(const SCP_string& name) { // empty string = no skybox if (std::strncmp(The_mission.skybox_model, name.c_str(), NAME_LENGTH) != 0) { @@ -1108,4 +1109,67 @@ void BackgroundEditorDialogModel::setSkyboxHeading(int deg) } } +SCP_vector BackgroundEditorDialogModel::getLightingProfileOptions() const +{ + SCP_vector out; + auto profiles = lighting_profiles::list_profiles(); // returns a vector of names + out.reserve(profiles.size()); + for (const auto& p : profiles) + out.emplace_back(p.c_str()); + return out; +} + +int BackgroundEditorDialogModel::getNumStars() const +{ + return Num_stars; +} + +void BackgroundEditorDialogModel::setNumStars(int n) +{ + CLAMP(n, getStarsLimit().first, getStarsLimit().second); + modify(Num_stars, n); + refreshBackgroundPreview(); // TODO make this actually show the stars in the background +} + +bool BackgroundEditorDialogModel::getTakesPlaceInSubspace() const +{ + return The_mission.flags[Mission::Mission_Flags::Subspace]; +} + +void BackgroundEditorDialogModel::setTakesPlaceInSubspace(bool on) +{ + auto before = The_mission.flags[Mission::Mission_Flags::Subspace]; + if (before == on) + return; + + The_mission.flags.set(Mission::Mission_Flags::Subspace, on); + + set_modified(); +} + +SCP_string BackgroundEditorDialogModel::getEnvironmentMapName() const +{ + return SCP_string(The_mission.envmap_name); +} + +void BackgroundEditorDialogModel::setEnvironmentMapName(const SCP_string& name) +{ + if (name == The_mission.envmap_name) + return; + + strcpy_s(The_mission.envmap_name, name.c_str()); + + set_modified(); +} + +SCP_string BackgroundEditorDialogModel::getLightingProfileName() const +{ + return The_mission.lighting_profile_name; +} + +void BackgroundEditorDialogModel::setLightingProfileName(const SCP_string& name) +{ + modify(The_mission.lighting_profile_name, name); +} + } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index 1b077acc162..9fe01a2bc49 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -26,6 +26,7 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { std::pair getBitmapScaleLimit() const { return {0.001f, 18.0f}; } std::pair getSunScaleLimit() const{ return {0.1f, 50.0f}; } std::pair getDivisionLimit() const { return {1, 5}; } + std::pair getStarsLimit() const { return {0, MAX_STARS}; } // bitmap group SCP_vector getAvailableBitmapNames() const; @@ -123,8 +124,8 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { void setAmbientB(int b); // skybox group - std::string getSkyboxModelName() const; - void setSkyboxModelName(const std::string& name); + SCP_string getSkyboxModelName() const; + void setSkyboxModelName(const SCP_string& name); bool getSkyboxNoLighting() const; void setSkyboxNoLighting(bool on); bool getSkyboxAllTransparent() const; @@ -144,6 +145,17 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { int getSkyboxHeading() const; void setSkyboxHeading(int deg); + // misc group + SCP_vector getLightingProfileOptions() const; + int getNumStars() const; + void setNumStars(int n); + bool getTakesPlaceInSubspace() const; + void setTakesPlaceInSubspace(bool on); + SCP_string getEnvironmentMapName() const; + void setEnvironmentMapName(const SCP_string& name); + SCP_string getLightingProfileName() const; + void setLightingProfileName(const SCP_string& name); + private: void refreshBackgroundPreview(); background_t& getActiveBackground() const; diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp index 977f7689002..b41c0f801b5 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp @@ -98,9 +98,18 @@ void BackgroundEditorDialog::initializeUi() updateAmbientLightControls(); // Skybox - updateSkyboxControls(); + // Misc + ui->numStarsSlider->setRange(_model->getStarsLimit().first, _model->getStarsLimit().second); + const auto& profiles = _model->getLightingProfileOptions(); + + for (const auto& s : profiles) { + ui->lightingProfileCombo->addItem(QString::fromStdString(s)); + } + + updateMiscControls(); + } void BackgroundEditorDialog::updateUi() @@ -343,6 +352,19 @@ void BackgroundEditorDialog::updateSkyboxControls() ui->noGlowMapsCheckBox->setChecked(_model->getSkyboxNoGlowmaps()); } +void BackgroundEditorDialog::updateMiscControls() +{ + util::SignalBlockers blockers(this); + + QString text = "Number of stars: " + QString::number(_model->getNumStars()); + ui->numStarsLabel->setText(text); + ui->numStarsSlider->setValue(_model->getNumStars()); + ui->subspaceCheckBox->setChecked(_model->getTakesPlaceInSubspace()); + ui->envMapEdit->setText(QString::fromStdString(_model->getEnvironmentMapName())); + ui->lightingProfileCombo->setCurrentIndex(ui->lightingProfileCombo->findText(QString::fromStdString(_model->getLightingProfileName()))); + +} + void BackgroundEditorDialog::on_bitmapListWidget_currentRowChanged(int row) { _model->setSelectedBitmapIndex(row); @@ -719,7 +741,7 @@ void BackgroundEditorDialog::on_skyboxButton_clicked() settings.setValue("skybox/lastDir", fi.absolutePath()); const QString baseName = fi.completeBaseName(); - _model->setSkyboxModelName(baseName.toStdString()); + _model->setSkyboxModelName(baseName.toUtf8().constData()); updateSkyboxControls(); } @@ -779,4 +801,48 @@ void BackgroundEditorDialog::on_noGlowmapsCheckBox_toggled(bool checked) _model->setSkyboxNoGlowmaps(checked); } +void BackgroundEditorDialog::on_numStarsSlider_valueChanged(int value) +{ + _model->setNumStars(value); + + QString text = "Number of stars: " + QString::number(value); + ui->numStarsLabel->setText(text); +} + +void BackgroundEditorDialog::on_subspaceCheckBox_toggled(bool checked) +{ + _model->setTakesPlaceInSubspace(checked); +} + +void BackgroundEditorDialog::on_envMapButton_clicked() +{ + QSettings settings("QtFRED", "BackgroundEditor"); + const QString lastDir = settings.value("envmap/lastDir", QDir::homePath()).toString(); + const QString path = QFileDialog::getOpenFileName(this, + tr("Select Environment Map"), + lastDir, + tr("Environment Maps (*.dds);;All Files (*)")); + if (path.isEmpty()) + return; + const QFileInfo fi(path); + settings.setValue("envmap/lastDir", fi.absolutePath()); + const QString baseName = fi.completeBaseName(); + _model->setEnvironmentMapName(baseName.toUtf8().constData()); + updateMiscControls(); +} + +void BackgroundEditorDialog::on_envMapEdit_textChanged(const QString& arg1) +{ + _model->setEnvironmentMapName(arg1.toUtf8().constData()); +} + +void BackgroundEditorDialog::on_lightingProfileCombo_currentIndexChanged(int index) +{ + if (index < 0) + return; + + const QString text = ui->lightingProfileCombo->itemText(index); + _model->setLightingProfileName(text.toUtf8().constData()); +} + } // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h index ec8a5bcbfdd..49c41c8456d 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -85,6 +85,13 @@ private slots: void on_noCullCheckBox_toggled(bool checked); void on_noGlowmapsCheckBox_toggled(bool checked); + // Misc + void on_numStarsSlider_valueChanged(int value); + void on_subspaceCheckBox_toggled(bool checked); + void on_envMapButton_clicked(); + void on_envMapEdit_textChanged(const QString& arg1); + void on_lightingProfileCombo_currentIndexChanged(int index); + private: // NOLINT(readability-redundant-access-specifiers) std::unique_ptr ui; std::unique_ptr _model; @@ -102,6 +109,7 @@ private slots: void updateAmbientLightControls(); void updateAmbientSwatch(); void updateSkyboxControls(); + void updateMiscControls(); }; } // namespace fso::fred::dialogs From 917b4547ec2bc714e9efec864449305f055d4f54 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 20:47:59 -0500 Subject: [PATCH 435/466] save swap with index --- .../dialogs/BackgroundEditorDialogModel.cpp | 198 ++++++++++++++++++ .../dialogs/BackgroundEditorDialogModel.h | 15 ++ .../src/ui/dialogs/BackgroundEditorDialog.cpp | 111 +++++++++- .../src/ui/dialogs/BackgroundEditorDialog.h | 12 ++ 4 files changed, 335 insertions(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index 1094d5f1919..3b40feee26a 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -13,6 +13,8 @@ const static float delta = .00001f; const static float default_nebula_range = 3000.0f; +extern void parse_one_background(background_t* background); + namespace fso::fred::dialogs { BackgroundEditorDialogModel::BackgroundEditorDialogModel(QObject* parent, EditorViewport* viewport) : AbstractDialogModel(parent, viewport) @@ -83,6 +85,202 @@ starfield_list_entry* BackgroundEditorDialogModel::getActiveSun() const return &list[_selectedSunIndex]; } +SCP_vector BackgroundEditorDialogModel::getBackgroundNames() const +{ + SCP_vector out; + out.reserve(Backgrounds.size()); + for (size_t i = 0; i < Backgrounds.size(); ++i) + out.emplace_back("Background " + std::to_string(i + 1)); + return out; +} + +void BackgroundEditorDialogModel::setActiveBackgroundIndex(int idx) +{ + if (!SCP_vector_inbounds(Backgrounds, idx)) + return; + + Cur_background = idx; + + // Reseed selections for the new background + _selectedBitmapIndex = Backgrounds[idx].bitmaps.empty() ? -1 : 0; + _selectedSunIndex = Backgrounds[idx].suns.empty() ? -1 : 0; + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getActiveBackgroundIndex() const +{ + return Cur_background < 0 ? 0 : Cur_background; +} + + +void BackgroundEditorDialogModel::addBackground() +{ + const int newIndex = static_cast(Backgrounds.size()); + stars_add_blank_background(/*creating_in_fred=*/true); + set_modified(); + + // select it + setActiveBackgroundIndex(newIndex); +} + +void BackgroundEditorDialogModel::removeActiveBackground() +{ + if (Backgrounds.size() <= 1 || Cur_background < 0) { + return; // keep at least one background + } + + const int oldIdx = Cur_background; + Backgrounds.erase(Backgrounds.begin() + oldIdx); + + // clamp selection to the new valid range + const int newIdx = std::min(oldIdx, static_cast(Backgrounds.size()) - 1); + set_modified(); + + setActiveBackgroundIndex(newIdx); +} + +int BackgroundEditorDialogModel::getImportableBackgroundCount(const SCP_string& fs2Path) const +{ + // Normalize the filepath to use the current platform's directory separator + SCP_string path = fs2Path; + std::replace(path.begin(), path.end(), '/', DIR_SEPARATOR_CHAR); + + try { + read_file_text(path.c_str()); + reset_parse(); + + if (!skip_to_start_of_string("#Background bitmaps")) { + return 0; // no background section + } + + // Enter the section and skip the header fields + required_string("#Background bitmaps"); + required_string("$Num stars:"); + int tmp; + stuff_int(&tmp); + required_string("$Ambient light level:"); + stuff_int(&tmp); + + // Count how many explicit "$Bitmap List:" blocks this file has + char* saved = Mp; + int count = 0; + while (skip_to_string("$Bitmap List:")) { + ++count; + } + Mp = saved; + + // Retail-style missions may have 0 "$Bitmap List:" entries but still one background. + return (count > 0) ? count : 1; + } catch (...) { + return 0; // parse error + } +} + +bool BackgroundEditorDialogModel::importBackgroundFromMission(const SCP_string& fs2Path, int whichIndex) +{ + // Replace the CURRENT background with one parsed from another mission file. + if (Cur_background < 0) + return false; + + // Normalize the filepath to use the current platform's directory separator + SCP_string path = fs2Path; + std::replace(path.begin(), path.end(), '/', DIR_SEPARATOR_CHAR); + + try { + read_file_text(path.c_str()); + reset_parse(); + + if (!skip_to_start_of_string("#Background bitmaps")) { + return false; // file has no background section + } + + required_string("#Background bitmaps"); + required_string("$Num stars:"); + int tmp; + stuff_int(&tmp); + required_string("$Ambient light level:"); + stuff_int(&tmp); + + // Count "$Bitmap List:" occurrences + char* saved = Mp; + int count = 0; + while (skip_to_string("$Bitmap List:")) { + ++count; + } + Mp = saved; + + // If multiple lists exist, skip to the requested one. + // If zero lists exist (retail), parse_one_background will handle the single background. + if (count > 0) { + const int target = std::max(0, std::min(whichIndex, count - 1)); + for (int i = 0; i < target + 1; ++i) { + skip_to_string("$Bitmap List:"); + } + } + + // Parse into the current slot + parse_one_background(&Backgrounds[Cur_background]); + } catch (...) { + return false; + } + + set_modified(); + // Rebuild instances & repaint so the import is visible immediately + stars_load_background(Cur_background); + if (_viewport) + _viewport->needsUpdate(); + return true; +} + +void BackgroundEditorDialogModel::swapBackgrounds() +{ + if (Cur_background < 0 || _swapIndex < 0 || _swapIndex >= static_cast(Backgrounds.size()) || _swapIndex == Cur_background) { + return; + } + + stars_swap_backgrounds(Cur_background, _swapIndex); + set_modified(); + + refreshBackgroundPreview(); + return; +} + +int BackgroundEditorDialogModel::getSwapWithIndex() const +{ + return _swapIndex; +} + +void BackgroundEditorDialogModel::setSwapWithIndex(int idx) +{ + if (!SCP_vector_inbounds(Backgrounds, idx)) { + return; + } + + _swapIndex = idx; +} + +bool BackgroundEditorDialogModel::getSaveAnglesCorrectFlag() const +{ + const auto& bg = getActiveBackground(); + return bg.flags[Starfield::Background_Flags::Corrected_angles_in_mission_file]; +} + +void BackgroundEditorDialogModel::setSaveAnglesCorrectFlag(bool on) +{ + auto& bg = getActiveBackground(); + const bool before = bg.flags[Starfield::Background_Flags::Corrected_angles_in_mission_file]; + if (before == on) + return; + + if (on) + bg.flags.set(Starfield::Background_Flags::Corrected_angles_in_mission_file); + else + bg.flags.remove(Starfield::Background_Flags::Corrected_angles_in_mission_file); + + set_modified(); +} + SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() const { SCP_vector out; diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index 9fe01a2bc49..32e5b3c2a65 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -28,6 +28,20 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { std::pair getDivisionLimit() const { return {1, 5}; } std::pair getStarsLimit() const { return {0, MAX_STARS}; } + // backgrounds group + SCP_vector getBackgroundNames() const; + void setActiveBackgroundIndex(int idx); + int getActiveBackgroundIndex() const; + void addBackground(); + void removeActiveBackground(); + int getImportableBackgroundCount(const SCP_string& fs2Path) const; + bool importBackgroundFromMission(const SCP_string& fs2Path, int whichIndex); + void swapBackgrounds(); + void setSwapWithIndex(int idx); + int getSwapWithIndex() const; + void setSaveAnglesCorrectFlag(bool on); + bool getSaveAnglesCorrectFlag() const; + // bitmap group SCP_vector getAvailableBitmapNames() const; SCP_vector getMissionBitmapNames() const; @@ -164,6 +178,7 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { int _selectedBitmapIndex = -1; // index into Backgrounds[Cur_background].bitmaps int _selectedSunIndex = -1; // index into Backgrounds[Cur_background].suns + int _swapIndex = 0; // index of background to swap with }; } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp index b41c0f801b5..bfebd5b45ca 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include namespace fso::fred::dialogs { @@ -28,7 +30,7 @@ void BackgroundEditorDialog::initializeUi() util::SignalBlockers blockers(this); // Backgrounds - // TODO + updateBackgroundControls(); // Bitmaps ui->bitmapPitchSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); @@ -40,6 +42,7 @@ void BackgroundEditorDialog::initializeUi() ui->bitmapDivYSpinBox->setRange(_model->getDivisionLimit().first, _model->getDivisionLimit().second); const auto& names = _model->getAvailableBitmapNames(); + for (const auto& s : names){ ui->bitmapTypeCombo->addItem(QString::fromStdString(s)); } @@ -116,8 +119,31 @@ void BackgroundEditorDialog::updateUi() { util::SignalBlockers blockers(this); + updateBackgroundControls(); refreshBitmapList(); refreshSunList(); + updateNebulaControls(); + updateOldNebulaControls(); + updateAmbientLightControls(); + updateSkyboxControls(); + updateMiscControls(); +} + +void BackgroundEditorDialog::updateBackgroundControls() +{ + util::SignalBlockers blockers(this); + + ui->backgroundSelectionCombo->clear(); + const auto names = _model->getBackgroundNames(); + for (const auto& s : names){ + ui->backgroundSelectionCombo->addItem(QString::fromStdString(s)); + ui->swapWithCombo->addItem(QString::fromStdString(s)); + } + + ui->removeButton->setEnabled(_model->getBackgroundNames().size() > 1); + ui->backgroundSelectionCombo->setCurrentIndex(_model->getActiveBackgroundIndex()); + ui->swapWithCombo->setCurrentIndex(_model->getSwapWithIndex()); + ui->useCorrectAngleFormatCheckBox->setChecked(_model->getSaveAnglesCorrectFlag()); } void BackgroundEditorDialog::refreshBitmapList() @@ -362,7 +388,90 @@ void BackgroundEditorDialog::updateMiscControls() ui->subspaceCheckBox->setChecked(_model->getTakesPlaceInSubspace()); ui->envMapEdit->setText(QString::fromStdString(_model->getEnvironmentMapName())); ui->lightingProfileCombo->setCurrentIndex(ui->lightingProfileCombo->findText(QString::fromStdString(_model->getLightingProfileName()))); +} + +int BackgroundEditorDialog::pickBackgroundIndexDialog(QWidget* parent, int count, int defaultIndex) +{ + if (count <= 0) + return -1; + + QStringList items; + items.reserve(count); + for (int i = 0; i < count; ++i) + items << QObject::tr("Background %1").arg(i + 1); + + bool ok = false; + const int start = std::clamp(defaultIndex, 0, count - 1); + const QString sel = QInputDialog::getItem(parent, + QObject::tr("Choose Background to Import"), + QObject::tr("Import which background?"), + items, + start, + false, + &ok); + if (!ok) + return -1; + return items.indexOf(sel); +} + +void BackgroundEditorDialog::on_backgroundSelectionCombo_currentIndexChanged(int index) +{ + if (index < 0) + return; + + _model->setActiveBackgroundIndex(index); + updateUi(); +} + +void BackgroundEditorDialog::on_addButton_clicked() +{ + _model->addBackground(); + updateUi(); +} +void BackgroundEditorDialog::on_removeButton_clicked() +{ + _model->removeActiveBackground(); + updateUi(); +} + +void BackgroundEditorDialog::on_importButton_clicked() +{ + const QString file = QFileDialog::getOpenFileName(this, "Import Backgrounds from File", QString(), "Freespace 2 Mission Files (*.fs2);;All Files (*)"); + if (file.isEmpty()) + return; + int count = _model->getImportableBackgroundCount(file.toUtf8().constData()); + + if (count <= 0) { + QMessageBox::information(this, "Import Background", "No backgrounds found in the specified file."); + return; + } + + int which = pickBackgroundIndexDialog(this, count); + + if (which < 0) + return; + + _model->importBackgroundFromMission(file.toUtf8().constData(), which); + + updateUi(); +} + +void BackgroundEditorDialog::on_swapWithButton_clicked() +{ + _model->swapBackgrounds(); + + updateUi(); +} + +void BackgroundEditorDialog::on_swapWithCombo_currentIndexChanged(int index) +{ + _model->setSwapWithIndex(index); +} + +void BackgroundEditorDialog::on_useCorrectAngleFormatCheckBox_toggled(bool checked) +{ + _model->setSaveAnglesCorrectFlag(checked); } void BackgroundEditorDialog::on_bitmapListWidget_currentRowChanged(int row) diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h index 49c41c8456d..323325eebdf 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -20,6 +20,15 @@ class BackgroundEditorDialog : public QDialog { private slots: + // Backgrounds + void on_backgroundSelectionCombo_currentIndexChanged(int index); + void on_addButton_clicked(); + void on_removeButton_clicked(); + void on_importButton_clicked(); + void on_swapWithButton_clicked(); + void on_swapWithCombo_currentIndexChanged(int index); + void on_useCorrectAngleFormatCheckBox_toggled(bool checked); + // Bitmaps void on_bitmapListWidget_currentRowChanged(int row); void on_bitmapTypeCombo_currentIndexChanged(int index); @@ -99,6 +108,7 @@ private slots: void initializeUi(); void updateUi(); + void updateBackgroundControls(); void refreshBitmapList(); void updateBitmapControls(); void refreshSunList(); @@ -110,6 +120,8 @@ private slots: void updateAmbientSwatch(); void updateSkyboxControls(); void updateMiscControls(); + + int pickBackgroundIndexDialog(QWidget* parent, int count, int defaultIndex = 0); }; } // namespace fso::fred::dialogs From 1d1ed1905bde58ebca8523f0be747a7765ed6c0b Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 20:48:50 -0500 Subject: [PATCH 436/466] minor fix --- qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index 3b40feee26a..9231c2a3535 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -138,6 +138,11 @@ void BackgroundEditorDialogModel::removeActiveBackground() set_modified(); setActiveBackgroundIndex(newIdx); + + // Ensure the swap index is still valid + if (!SCP_vector_inbounds(Backgrounds, _swapIndex)) { + _swapIndex = 0; + } } int BackgroundEditorDialogModel::getImportableBackgroundCount(const SCP_string& fs2Path) const From 0f73a59155259eacb0af1bd9cf4938e8aeecd95a Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 20:58:29 -0500 Subject: [PATCH 437/466] clean --- .../dialogs/BackgroundEditorDialogModel.cpp | 24 +------------------ .../dialogs/BackgroundEditorDialogModel.h | 2 -- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index 9231c2a3535..e85bc8fbf5d 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -674,28 +674,6 @@ void BackgroundEditorDialogModel::setSunPitch(int deg) refreshBackgroundPreview(); } -int BackgroundEditorDialogModel::getSunBank() const -{ - // Bank is not used for suns but this is added for consistency - /*auto* s = getActiveSun(); - if (!s) - return 0; - - return fl2ir(fl_degrees(s->ang.b) + delta);*/ -} - -void BackgroundEditorDialogModel::setSunBank(int deg) -{ - // Bank is not used for suns but this is added for consistency - /*auto* s = getActiveSun(); - if (!s) - return; - - CLAMP(deg, getOrientLimit().first, getOrientLimit().second); - modify(s->ang.b, fl_radians(deg)); - refreshBackgroundPreview();*/ -} - int BackgroundEditorDialogModel::getSunHeading() const { auto* s = getActiveSun(); @@ -859,7 +837,7 @@ void BackgroundEditorDialogModel::setSelectedPoofs(const SCP_vector& clear_all_bits(Neb2_poof_flags.get(), Poof_info.size()); for (const auto& want : names) { for (size_t i = 0; i < Poof_info.size(); ++i) { - if (!_stricmp(Poof_info[i].name, want.c_str())) { + if (!stricmp(Poof_info[i].name, want.c_str())) { set_bit(Neb2_poof_flags.get(), i); break; } diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index 32e5b3c2a65..f679354e285 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -77,8 +77,6 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { void setSunName(const SCP_string& name); int getSunPitch() const; void setSunPitch(int deg); - int getSunBank() const; // unused for suns but added for consistency - void setSunBank(int deg); // unused for suns but added for consistency int getSunHeading() const; void setSunHeading(int deg); float getSunScale() const; // uses scale_x for both x and y From ca4540a821b99ad5fd3a23e883ef4af4150f95a3 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 21:54:50 -0500 Subject: [PATCH 438/466] clang --- .../dialogs/BackgroundEditorDialogModel.cpp | 112 +++++++++++------- .../dialogs/BackgroundEditorDialogModel.h | 91 +++++++------- .../src/ui/dialogs/BackgroundEditorDialog.h | 2 +- 3 files changed, 115 insertions(+), 90 deletions(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index e85bc8fbf5d..070cf0a28ab 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -9,7 +9,7 @@ #include "starfield/nebula.h" #include "lighting/lighting_profiles.h" -// TODO move this to common for both FREDs. Do not pass review if this is not done +// TODO move this to common for both FREDs. const static float delta = .00001f; const static float default_nebula_range = 3000.0f; @@ -56,7 +56,7 @@ void BackgroundEditorDialogModel::refreshBackgroundPreview() _editor->missionChanged(); } -background_t& BackgroundEditorDialogModel::getActiveBackground() const +background_t& BackgroundEditorDialogModel::getActiveBackground() { if (!SCP_vector_inbounds(Backgrounds, Cur_background)) { // Fall back to first background if Cur_background isn’t set @@ -85,8 +85,15 @@ starfield_list_entry* BackgroundEditorDialogModel::getActiveSun() const return &list[_selectedSunIndex]; } +int BackgroundEditorDialogModel::getNumBackgrounds() const +{ + return static_cast(Backgrounds.size()); +} + SCP_vector BackgroundEditorDialogModel::getBackgroundNames() const { + Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); + SCP_vector out; out.reserve(Backgrounds.size()); for (size_t i = 0; i < Backgrounds.size(); ++i) @@ -110,6 +117,8 @@ void BackgroundEditorDialogModel::setActiveBackgroundIndex(int idx) int BackgroundEditorDialogModel::getActiveBackgroundIndex() const { + Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); + return Cur_background < 0 ? 0 : Cur_background; } @@ -147,6 +156,8 @@ void BackgroundEditorDialogModel::removeActiveBackground() int BackgroundEditorDialogModel::getImportableBackgroundCount(const SCP_string& fs2Path) const { + Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); + // Normalize the filepath to use the current platform's directory separator SCP_string path = fs2Path; std::replace(path.begin(), path.end(), '/', DIR_SEPARATOR_CHAR); @@ -248,7 +259,6 @@ void BackgroundEditorDialogModel::swapBackgrounds() set_modified(); refreshBackgroundPreview(); - return; } int BackgroundEditorDialogModel::getSwapWithIndex() const @@ -288,6 +298,8 @@ void BackgroundEditorDialogModel::setSaveAnglesCorrectFlag(bool on) SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() const { + Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); + SCP_vector out; const int count = stars_get_num_entries(/*is_a_sun=*/false, /*bitmap_count=*/true); out.reserve(count); @@ -301,6 +313,8 @@ SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() co SCP_vector BackgroundEditorDialogModel::getMissionBitmapNames() const { + Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); + SCP_vector out; const auto& vec = getActiveBackground().bitmaps; out.reserve(vec.size()); @@ -552,6 +566,8 @@ void BackgroundEditorDialogModel::setBitmapDivY(int v) SCP_vector BackgroundEditorDialogModel::getAvailableSunNames() const { + Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); + SCP_vector out; const int count = stars_get_num_entries(/*is_a_sun=*/true, /*bitmap_count=*/true); out.reserve(count); @@ -565,6 +581,8 @@ SCP_vector BackgroundEditorDialogModel::getAvailableSunNames() const SCP_vector BackgroundEditorDialogModel::getMissionSunNames() const { + Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); + SCP_vector out; const auto& vec = getActiveBackground().suns; out.reserve(vec.size()); @@ -716,6 +734,8 @@ void BackgroundEditorDialogModel::setSunScale(float v) SCP_vector BackgroundEditorDialogModel::getLightningNames() const { + Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); + SCP_vector out; out.emplace_back(""); // legacy default for (const auto& st : Storm_types) { @@ -726,6 +746,8 @@ SCP_vector BackgroundEditorDialogModel::getLightningNames() const SCP_vector BackgroundEditorDialogModel::getNebulaPatternNames() const { + Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); + SCP_vector out; out.emplace_back(""); // matches legacy combo where index 0 = none for (const auto& neb : Neb2_bitmap_filenames) { @@ -736,6 +758,8 @@ SCP_vector BackgroundEditorDialogModel::getNebulaPatternNames() cons SCP_vector BackgroundEditorDialogModel::getPoofNames() const { + Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); + SCP_vector out; out.reserve(Poof_info.size()); for (const auto& p : Poof_info) { @@ -744,7 +768,7 @@ SCP_vector BackgroundEditorDialogModel::getPoofNames() const return out; } -bool BackgroundEditorDialogModel::getFullNebulaEnabled() const +bool BackgroundEditorDialogModel::getFullNebulaEnabled() { return The_mission.flags[Mission::Mission_Flags::Fullneb]; } @@ -772,7 +796,7 @@ void BackgroundEditorDialogModel::setFullNebulaEnabled(bool enabled) set_modified(); } -float BackgroundEditorDialogModel::getFullNebulaRange() const +float BackgroundEditorDialogModel::getFullNebulaRange() { // May be -1 if full nebula is disabled return Neb2_awacs; @@ -783,7 +807,7 @@ void BackgroundEditorDialogModel::setFullNebulaRange(float range) modify(Neb2_awacs, range); } -SCP_string BackgroundEditorDialogModel::getNebulaFullPattern() const +SCP_string BackgroundEditorDialogModel::getNebulaFullPattern() { return (Neb2_texture_name[0] != '\0') ? SCP_string(Neb2_texture_name) : SCP_string(""); } @@ -799,7 +823,7 @@ void BackgroundEditorDialogModel::setNebulaFullPattern(const SCP_string& name) set_modified(); } -SCP_string BackgroundEditorDialogModel::getLightning() const +SCP_string BackgroundEditorDialogModel::getLightning() { // Return "" when engine stores "none" or empty if (Mission_parse_storm_name[0] == '\0') @@ -821,7 +845,7 @@ void BackgroundEditorDialogModel::setLightning(const SCP_string& name) set_modified(); } -SCP_vector BackgroundEditorDialogModel::getSelectedPoofs() const +SCP_vector BackgroundEditorDialogModel::getSelectedPoofs() { SCP_vector out; for (size_t i = 0; i < Poof_info.size(); ++i) { @@ -847,7 +871,7 @@ void BackgroundEditorDialogModel::setSelectedPoofs(const SCP_vector& set_modified(); } -bool BackgroundEditorDialogModel::getShipTrailsToggled() const +bool BackgroundEditorDialogModel::getShipTrailsToggled() { return The_mission.flags[Mission::Mission_Flags::Toggle_ship_trails]; } @@ -858,7 +882,7 @@ void BackgroundEditorDialogModel::setShipTrailsToggled(bool on) set_modified(); } -float BackgroundEditorDialogModel::getFogNearMultiplier() const +float BackgroundEditorDialogModel::getFogNearMultiplier() { return Neb2_fog_near_mult; } @@ -868,7 +892,7 @@ void BackgroundEditorDialogModel::setFogNearMultiplier(float v) modify(Neb2_fog_near_mult, v); } -float BackgroundEditorDialogModel::getFogFarMultiplier() const +float BackgroundEditorDialogModel::getFogFarMultiplier() { return Neb2_fog_far_mult; } @@ -878,7 +902,7 @@ void BackgroundEditorDialogModel::setFogFarMultiplier(float v) modify(Neb2_fog_far_mult, v); } -bool BackgroundEditorDialogModel::getDisplayBackgroundBitmaps() const +bool BackgroundEditorDialogModel::getDisplayBackgroundBitmaps() { return The_mission.flags[Mission::Mission_Flags::Fullneb_background_bitmaps]; } @@ -889,7 +913,7 @@ void BackgroundEditorDialogModel::setDisplayBackgroundBitmaps(bool on) set_modified(); } -bool BackgroundEditorDialogModel::getFogPaletteOverride() const +bool BackgroundEditorDialogModel::getFogPaletteOverride() { return The_mission.flags[Mission::Mission_Flags::Neb2_fog_color_override]; } @@ -900,7 +924,7 @@ void BackgroundEditorDialogModel::setFogPaletteOverride(bool on) set_modified(); } -int BackgroundEditorDialogModel::getFogR() const +int BackgroundEditorDialogModel::getFogR() { return Neb2_fog_color[0]; } @@ -908,11 +932,11 @@ int BackgroundEditorDialogModel::getFogR() const void BackgroundEditorDialogModel::setFogR(int r) { CLAMP(r, 0, 255) - const ubyte v = static_cast(r); + const auto v = static_cast(r); modify(Neb2_fog_color[0], v); } -int BackgroundEditorDialogModel::getFogG() const +int BackgroundEditorDialogModel::getFogG() { return Neb2_fog_color[1]; } @@ -920,11 +944,11 @@ int BackgroundEditorDialogModel::getFogG() const void BackgroundEditorDialogModel::setFogG(int g) { CLAMP(g, 0, 255) - const ubyte v = static_cast(g); + const auto v = static_cast(g); modify(Neb2_fog_color[1], v); } -int BackgroundEditorDialogModel::getFogB() const +int BackgroundEditorDialogModel::getFogB() { return Neb2_fog_color[2]; } @@ -932,11 +956,11 @@ int BackgroundEditorDialogModel::getFogB() const void BackgroundEditorDialogModel::setFogB(int b) { CLAMP(b, 0, 255) - const ubyte v = static_cast(b); + const auto v = static_cast(b); modify(Neb2_fog_color[2], v); } -SCP_vector BackgroundEditorDialogModel::getOldNebulaPatternOptions() const +SCP_vector BackgroundEditorDialogModel::getOldNebulaPatternOptions() { SCP_vector out; out.emplace_back(""); @@ -946,7 +970,7 @@ SCP_vector BackgroundEditorDialogModel::getOldNebulaPatternOptions() return out; } -SCP_vector BackgroundEditorDialogModel::getOldNebulaColorOptions() const +SCP_vector BackgroundEditorDialogModel::getOldNebulaColorOptions() { SCP_vector out; out.reserve(NUM_NEBULA_COLORS); @@ -956,7 +980,7 @@ SCP_vector BackgroundEditorDialogModel::getOldNebulaColorOptions() c return out; } -SCP_string BackgroundEditorDialogModel::getOldNebulaPattern() const +SCP_string BackgroundEditorDialogModel::getOldNebulaPattern() { if (Nebula_index < 0) return ""; @@ -983,7 +1007,7 @@ void BackgroundEditorDialogModel::setOldNebulaPattern(const SCP_string& name) modify(Nebula_index, newIndex); } -SCP_string BackgroundEditorDialogModel::getOldNebulaColorName() const +SCP_string BackgroundEditorDialogModel::getOldNebulaColorName() { if (Mission_palette >= 0 && Mission_palette < NUM_NEBULA_COLORS) { return Nebula_colors[Mission_palette]; @@ -1004,7 +1028,7 @@ void BackgroundEditorDialogModel::setOldNebulaColorName(const SCP_string& name) // name not found: ignore } -int BackgroundEditorDialogModel::getOldNebulaPitch() const +int BackgroundEditorDialogModel::getOldNebulaPitch() { return Nebula_pitch; } @@ -1018,7 +1042,7 @@ void BackgroundEditorDialogModel::setOldNebulaPitch(int deg) } } -int BackgroundEditorDialogModel::getOldNebulaBank() const +int BackgroundEditorDialogModel::getOldNebulaBank() { return Nebula_bank; } @@ -1032,7 +1056,7 @@ void BackgroundEditorDialogModel::setOldNebulaBank(int deg) } } -int BackgroundEditorDialogModel::getOldNebulaHeading() const +int BackgroundEditorDialogModel::getOldNebulaHeading() { return Nebula_heading; } @@ -1046,7 +1070,7 @@ void BackgroundEditorDialogModel::setOldNebulaHeading(int deg) } } -int BackgroundEditorDialogModel::getAmbientR() const +int BackgroundEditorDialogModel::getAmbientR() { return The_mission.ambient_light_level & 0xff; } @@ -1064,7 +1088,7 @@ void BackgroundEditorDialogModel::setAmbientR(int r) refreshBackgroundPreview(); } -int BackgroundEditorDialogModel::getAmbientG() const +int BackgroundEditorDialogModel::getAmbientG() { return (The_mission.ambient_light_level >> 8) & 0xff; } @@ -1082,7 +1106,7 @@ void BackgroundEditorDialogModel::setAmbientG(int g) refreshBackgroundPreview(); } -int BackgroundEditorDialogModel::getAmbientB() const +int BackgroundEditorDialogModel::getAmbientB() { return (The_mission.ambient_light_level >> 16) & 0xff; } @@ -1100,7 +1124,7 @@ void BackgroundEditorDialogModel::setAmbientB(int b) refreshBackgroundPreview(); } -SCP_string BackgroundEditorDialogModel::getSkyboxModelName() const +SCP_string BackgroundEditorDialogModel::getSkyboxModelName() { return The_mission.skybox_model; } @@ -1116,7 +1140,7 @@ void BackgroundEditorDialogModel::setSkyboxModelName(const SCP_string& name) refreshBackgroundPreview(); } -bool BackgroundEditorDialogModel::getSkyboxNoLighting() const +bool BackgroundEditorDialogModel::getSkyboxNoLighting() { return (The_mission.skybox_flags & MR_NO_LIGHTING) != 0; } @@ -1132,7 +1156,7 @@ void BackgroundEditorDialogModel::setSkyboxNoLighting(bool on) set_modified(); } -bool BackgroundEditorDialogModel::getSkyboxAllTransparent() const +bool BackgroundEditorDialogModel::getSkyboxAllTransparent() { return (The_mission.skybox_flags & MR_ALL_XPARENT) != 0; } @@ -1148,7 +1172,7 @@ void BackgroundEditorDialogModel::setSkyboxAllTransparent(bool on) set_modified(); } -bool BackgroundEditorDialogModel::getSkyboxNoZbuffer() const +bool BackgroundEditorDialogModel::getSkyboxNoZbuffer() { return (The_mission.skybox_flags & MR_NO_ZBUFFER) != 0; } @@ -1164,7 +1188,7 @@ void BackgroundEditorDialogModel::setSkyboxNoZbuffer(bool on) set_modified(); } -bool BackgroundEditorDialogModel::getSkyboxNoCull() const +bool BackgroundEditorDialogModel::getSkyboxNoCull() { return (The_mission.skybox_flags & MR_NO_CULL) != 0; } @@ -1180,7 +1204,7 @@ void BackgroundEditorDialogModel::setSkyboxNoCull(bool on) set_modified(); } -bool BackgroundEditorDialogModel::getSkyboxNoGlowmaps() const +bool BackgroundEditorDialogModel::getSkyboxNoGlowmaps() { return (The_mission.skybox_flags & MR_NO_GLOWMAPS) != 0; } @@ -1196,7 +1220,7 @@ void BackgroundEditorDialogModel::setSkyboxNoGlowmaps(bool on) set_modified(); } -bool BackgroundEditorDialogModel::getSkyboxForceClamp() const +bool BackgroundEditorDialogModel::getSkyboxForceClamp() { return (The_mission.skybox_flags & MR_FORCE_CLAMP) != 0; } @@ -1212,7 +1236,7 @@ void BackgroundEditorDialogModel::setSkyboxForceClamp(bool on) set_modified(); } -int BackgroundEditorDialogModel::getSkyboxPitch() const +int BackgroundEditorDialogModel::getSkyboxPitch() { angles a; vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); @@ -1238,7 +1262,7 @@ void BackgroundEditorDialogModel::setSkyboxPitch(int deg) } } -int BackgroundEditorDialogModel::getSkyboxBank() const +int BackgroundEditorDialogModel::getSkyboxBank() { angles a; vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); @@ -1264,7 +1288,7 @@ void BackgroundEditorDialogModel::setSkyboxBank(int deg) } } -int BackgroundEditorDialogModel::getSkyboxHeading() const +int BackgroundEditorDialogModel::getSkyboxHeading() { angles a; vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); @@ -1290,7 +1314,7 @@ void BackgroundEditorDialogModel::setSkyboxHeading(int deg) } } -SCP_vector BackgroundEditorDialogModel::getLightingProfileOptions() const +SCP_vector BackgroundEditorDialogModel::getLightingProfileOptions() { SCP_vector out; auto profiles = lighting_profiles::list_profiles(); // returns a vector of names @@ -1300,7 +1324,7 @@ SCP_vector BackgroundEditorDialogModel::getLightingProfileOptions() return out; } -int BackgroundEditorDialogModel::getNumStars() const +int BackgroundEditorDialogModel::getNumStars() { return Num_stars; } @@ -1312,7 +1336,7 @@ void BackgroundEditorDialogModel::setNumStars(int n) refreshBackgroundPreview(); // TODO make this actually show the stars in the background } -bool BackgroundEditorDialogModel::getTakesPlaceInSubspace() const +bool BackgroundEditorDialogModel::getTakesPlaceInSubspace() { return The_mission.flags[Mission::Mission_Flags::Subspace]; } @@ -1328,7 +1352,7 @@ void BackgroundEditorDialogModel::setTakesPlaceInSubspace(bool on) set_modified(); } -SCP_string BackgroundEditorDialogModel::getEnvironmentMapName() const +SCP_string BackgroundEditorDialogModel::getEnvironmentMapName() { return SCP_string(The_mission.envmap_name); } @@ -1343,7 +1367,7 @@ void BackgroundEditorDialogModel::setEnvironmentMapName(const SCP_string& name) set_modified(); } -SCP_string BackgroundEditorDialogModel::getLightingProfileName() const +SCP_string BackgroundEditorDialogModel::getLightingProfileName() { return The_mission.lighting_profile_name; } diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index f679354e285..673314c0308 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -22,11 +22,11 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { void reject() override; // limits - std::pair getOrientLimit() const { return {0, 359}; } - std::pair getBitmapScaleLimit() const { return {0.001f, 18.0f}; } - std::pair getSunScaleLimit() const{ return {0.1f, 50.0f}; } - std::pair getDivisionLimit() const { return {1, 5}; } - std::pair getStarsLimit() const { return {0, MAX_STARS}; } + static std::pair getOrientLimit() { return {0, 359}; } + static std::pair getBitmapScaleLimit() { return {0.001f, 18.0f}; } + static std::pair getSunScaleLimit() { return {0.1f, 50.0f}; } + static std::pair getDivisionLimit() { return {1, 5}; } + static std::pair getStarsLimit() { return {0, MAX_STARS}; } // backgrounds group SCP_vector getBackgroundNames() const; @@ -86,97 +86,98 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { SCP_vector getLightningNames() const; SCP_vector getNebulaPatternNames() const; SCP_vector getPoofNames() const; - bool getFullNebulaEnabled() const; + static bool getFullNebulaEnabled(); void setFullNebulaEnabled(bool enabled); - float getFullNebulaRange() const; + static float getFullNebulaRange(); void setFullNebulaRange(float range); - SCP_string getNebulaFullPattern() const; + static SCP_string getNebulaFullPattern(); void setNebulaFullPattern(const SCP_string& name); - SCP_string getLightning() const; + static SCP_string getLightning(); void setLightning(const SCP_string& name); - SCP_vector getSelectedPoofs() const; + static SCP_vector getSelectedPoofs(); void setSelectedPoofs(const SCP_vector& names); - bool getShipTrailsToggled() const; + static bool getShipTrailsToggled(); void setShipTrailsToggled(bool on); - float getFogNearMultiplier() const; + static float getFogNearMultiplier(); void setFogNearMultiplier(float v); - float getFogFarMultiplier() const; + static float getFogFarMultiplier(); void setFogFarMultiplier(float v); - bool getDisplayBackgroundBitmaps() const; + static bool getDisplayBackgroundBitmaps(); void setDisplayBackgroundBitmaps(bool on); - bool getFogPaletteOverride() const; + static bool getFogPaletteOverride(); void setFogPaletteOverride(bool on); - int getFogR() const; + static int getFogR(); void setFogR(int r); - int getFogG() const; + static int getFogG(); void setFogG(int g); - int getFogB() const; + static int getFogB(); void setFogB(int b); // old nebula group - SCP_vector getOldNebulaPatternOptions() const; - SCP_vector getOldNebulaColorOptions() const; - SCP_string getOldNebulaColorName() const; + static SCP_vector getOldNebulaPatternOptions(); + static SCP_vector getOldNebulaColorOptions(); + static SCP_string getOldNebulaColorName(); void setOldNebulaColorName(const SCP_string& name); - SCP_string getOldNebulaPattern() const; + static SCP_string getOldNebulaPattern(); void setOldNebulaPattern(const SCP_string& name); - int getOldNebulaPitch() const; + static int getOldNebulaPitch(); void setOldNebulaPitch(int deg); - int getOldNebulaBank() const; + static int getOldNebulaBank(); void setOldNebulaBank(int deg); - int getOldNebulaHeading() const; + static int getOldNebulaHeading(); void setOldNebulaHeading(int deg); // ambient light group - int getAmbientR() const; + static int getAmbientR(); void setAmbientR(int r); - int getAmbientG() const; + static int getAmbientG(); void setAmbientG(int g); - int getAmbientB() const; + static int getAmbientB(); void setAmbientB(int b); // skybox group - SCP_string getSkyboxModelName() const; + static SCP_string getSkyboxModelName(); void setSkyboxModelName(const SCP_string& name); - bool getSkyboxNoLighting() const; + static bool getSkyboxNoLighting(); void setSkyboxNoLighting(bool on); - bool getSkyboxAllTransparent() const; + static bool getSkyboxAllTransparent(); void setSkyboxAllTransparent(bool on); - bool getSkyboxNoZbuffer() const; + static bool getSkyboxNoZbuffer(); void setSkyboxNoZbuffer(bool on); - bool getSkyboxNoCull() const; + static bool getSkyboxNoCull(); void setSkyboxNoCull(bool on); - bool getSkyboxNoGlowmaps() const; + static bool getSkyboxNoGlowmaps(); void setSkyboxNoGlowmaps(bool on); - bool getSkyboxForceClamp() const; + static bool getSkyboxForceClamp(); void setSkyboxForceClamp(bool on); - int getSkyboxPitch() const; + static int getSkyboxPitch(); void setSkyboxPitch(int deg); - int getSkyboxBank() const; + static int getSkyboxBank(); void setSkyboxBank(int deg); - int getSkyboxHeading() const; + static int getSkyboxHeading(); void setSkyboxHeading(int deg); // misc group - SCP_vector getLightingProfileOptions() const; - int getNumStars() const; + static SCP_vector getLightingProfileOptions(); + static int getNumStars(); void setNumStars(int n); - bool getTakesPlaceInSubspace() const; + static bool getTakesPlaceInSubspace(); void setTakesPlaceInSubspace(bool on); - SCP_string getEnvironmentMapName() const; + static SCP_string getEnvironmentMapName(); void setEnvironmentMapName(const SCP_string& name); - SCP_string getLightingProfileName() const; + static SCP_string getLightingProfileName(); void setLightingProfileName(const SCP_string& name); private: void refreshBackgroundPreview(); - background_t& getActiveBackground() const; + static background_t& getActiveBackground(); starfield_list_entry* getActiveBitmap() const; starfield_list_entry* getActiveSun() const; + int getNumBackgrounds() const; int _selectedBitmapIndex = -1; // index into Backgrounds[Cur_background].bitmaps int _selectedSunIndex = -1; // index into Backgrounds[Cur_background].suns - int _swapIndex = 0; // index of background to swap with + int _swapIndex = 0; // index of background to swap with }; } // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h index 323325eebdf..b89eb779fbb 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -121,7 +121,7 @@ private slots: void updateSkyboxControls(); void updateMiscControls(); - int pickBackgroundIndexDialog(QWidget* parent, int count, int defaultIndex = 0); + static int pickBackgroundIndexDialog(QWidget* parent, int count, int defaultIndex = 0); }; } // namespace fso::fred::dialogs From 886b54c298dea853c63b4e298b86fce33df5f43e Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 22:12:24 -0500 Subject: [PATCH 439/466] more clang --- .../dialogs/BackgroundEditorDialogModel.cpp | 49 +++++-------------- .../dialogs/BackgroundEditorDialogModel.h | 21 ++++---- qtfred/ui/BackgroundEditor.ui | 15 ++++++ 3 files changed, 38 insertions(+), 47 deletions(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index 070cf0a28ab..ab15569b03f 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -53,6 +53,7 @@ void BackgroundEditorDialogModel::refreshBackgroundPreview() stars_load_background(Cur_background); // rebuild instances from Backgrounds[] stars_set_background_model(The_mission.skybox_model, nullptr, The_mission.skybox_flags); // rebuild skybox stars_set_background_orientation(&The_mission.skybox_orientation); + // TODO make this actually show the stars in the background _editor->missionChanged(); } @@ -85,14 +86,8 @@ starfield_list_entry* BackgroundEditorDialogModel::getActiveSun() const return &list[_selectedSunIndex]; } -int BackgroundEditorDialogModel::getNumBackgrounds() const +SCP_vector BackgroundEditorDialogModel::getBackgroundNames() { - return static_cast(Backgrounds.size()); -} - -SCP_vector BackgroundEditorDialogModel::getBackgroundNames() const -{ - Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); SCP_vector out; out.reserve(Backgrounds.size()); @@ -115,10 +110,8 @@ void BackgroundEditorDialogModel::setActiveBackgroundIndex(int idx) refreshBackgroundPreview(); } -int BackgroundEditorDialogModel::getActiveBackgroundIndex() const +int BackgroundEditorDialogModel::getActiveBackgroundIndex() { - Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); - return Cur_background < 0 ? 0 : Cur_background; } @@ -154,10 +147,8 @@ void BackgroundEditorDialogModel::removeActiveBackground() } } -int BackgroundEditorDialogModel::getImportableBackgroundCount(const SCP_string& fs2Path) const +int BackgroundEditorDialogModel::getImportableBackgroundCount(const SCP_string& fs2Path) { - Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); - // Normalize the filepath to use the current platform's directory separator SCP_string path = fs2Path; std::replace(path.begin(), path.end(), '/', DIR_SEPARATOR_CHAR); @@ -275,7 +266,7 @@ void BackgroundEditorDialogModel::setSwapWithIndex(int idx) _swapIndex = idx; } -bool BackgroundEditorDialogModel::getSaveAnglesCorrectFlag() const +bool BackgroundEditorDialogModel::getSaveAnglesCorrectFlag() { const auto& bg = getActiveBackground(); return bg.flags[Starfield::Background_Flags::Corrected_angles_in_mission_file]; @@ -296,10 +287,8 @@ void BackgroundEditorDialogModel::setSaveAnglesCorrectFlag(bool on) set_modified(); } -SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() const +SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() { - Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); - SCP_vector out; const int count = stars_get_num_entries(/*is_a_sun=*/false, /*bitmap_count=*/true); out.reserve(count); @@ -313,8 +302,6 @@ SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() co SCP_vector BackgroundEditorDialogModel::getMissionBitmapNames() const { - Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); - SCP_vector out; const auto& vec = getActiveBackground().bitmaps; out.reserve(vec.size()); @@ -564,10 +551,8 @@ void BackgroundEditorDialogModel::setBitmapDivY(int v) refreshBackgroundPreview(); } -SCP_vector BackgroundEditorDialogModel::getAvailableSunNames() const +SCP_vector BackgroundEditorDialogModel::getAvailableSunNames() { - Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); - SCP_vector out; const int count = stars_get_num_entries(/*is_a_sun=*/true, /*bitmap_count=*/true); out.reserve(count); @@ -579,10 +564,8 @@ SCP_vector BackgroundEditorDialogModel::getAvailableSunNames() const return out; } -SCP_vector BackgroundEditorDialogModel::getMissionSunNames() const +SCP_vector BackgroundEditorDialogModel::getMissionSunNames() { - Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); - SCP_vector out; const auto& vec = getActiveBackground().suns; out.reserve(vec.size()); @@ -732,10 +715,8 @@ void BackgroundEditorDialogModel::setSunScale(float v) refreshBackgroundPreview(); } -SCP_vector BackgroundEditorDialogModel::getLightningNames() const +SCP_vector BackgroundEditorDialogModel::getLightningNames() { - Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); - SCP_vector out; out.emplace_back(""); // legacy default for (const auto& st : Storm_types) { @@ -744,10 +725,8 @@ SCP_vector BackgroundEditorDialogModel::getLightningNames() const return out; } -SCP_vector BackgroundEditorDialogModel::getNebulaPatternNames() const +SCP_vector BackgroundEditorDialogModel::getNebulaPatternNames() { - Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); - SCP_vector out; out.emplace_back(""); // matches legacy combo where index 0 = none for (const auto& neb : Neb2_bitmap_filenames) { @@ -756,10 +735,8 @@ SCP_vector BackgroundEditorDialogModel::getNebulaPatternNames() cons return out; } -SCP_vector BackgroundEditorDialogModel::getPoofNames() const +SCP_vector BackgroundEditorDialogModel::getPoofNames() { - Assertion(getNumBackgrounds() > 0, "There should always be at least one background"); - SCP_vector out; out.reserve(Poof_info.size()); for (const auto& p : Poof_info) { @@ -1333,7 +1310,7 @@ void BackgroundEditorDialogModel::setNumStars(int n) { CLAMP(n, getStarsLimit().first, getStarsLimit().second); modify(Num_stars, n); - refreshBackgroundPreview(); // TODO make this actually show the stars in the background + refreshBackgroundPreview(); } bool BackgroundEditorDialogModel::getTakesPlaceInSubspace() @@ -1354,7 +1331,7 @@ void BackgroundEditorDialogModel::setTakesPlaceInSubspace(bool on) SCP_string BackgroundEditorDialogModel::getEnvironmentMapName() { - return SCP_string(The_mission.envmap_name); + return {The_mission.envmap_name}; } void BackgroundEditorDialogModel::setEnvironmentMapName(const SCP_string& name) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index 673314c0308..401129a6849 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -29,21 +29,21 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { static std::pair getStarsLimit() { return {0, MAX_STARS}; } // backgrounds group - SCP_vector getBackgroundNames() const; + static SCP_vector getBackgroundNames(); void setActiveBackgroundIndex(int idx); - int getActiveBackgroundIndex() const; + static int getActiveBackgroundIndex(); void addBackground(); void removeActiveBackground(); - int getImportableBackgroundCount(const SCP_string& fs2Path) const; + static int getImportableBackgroundCount(const SCP_string& fs2Path); bool importBackgroundFromMission(const SCP_string& fs2Path, int whichIndex); void swapBackgrounds(); void setSwapWithIndex(int idx); int getSwapWithIndex() const; void setSaveAnglesCorrectFlag(bool on); - bool getSaveAnglesCorrectFlag() const; + static bool getSaveAnglesCorrectFlag(); // bitmap group - SCP_vector getAvailableBitmapNames() const; + static SCP_vector getAvailableBitmapNames(); SCP_vector getMissionBitmapNames() const; void setSelectedBitmapIndex(int index); int getSelectedBitmapIndex() const; @@ -67,8 +67,8 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { void setBitmapDivY(int v); // sun group - SCP_vector getAvailableSunNames() const; - SCP_vector getMissionSunNames() const; + static SCP_vector getAvailableSunNames(); + static SCP_vector getMissionSunNames(); void setSelectedSunIndex(int index); int getSelectedSunIndex() const; void addMissionSunByName(const SCP_string& name); @@ -83,9 +83,9 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { void setSunScale(float v); // nebula group - SCP_vector getLightningNames() const; - SCP_vector getNebulaPatternNames() const; - SCP_vector getPoofNames() const; + static SCP_vector getLightningNames(); + static SCP_vector getNebulaPatternNames(); + static SCP_vector getPoofNames(); static bool getFullNebulaEnabled(); void setFullNebulaEnabled(bool enabled); static float getFullNebulaRange(); @@ -173,7 +173,6 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { static background_t& getActiveBackground(); starfield_list_entry* getActiveBitmap() const; starfield_list_entry* getActiveSun() const; - int getNumBackgrounds() const; int _selectedBitmapIndex = -1; // index into Backgrounds[Cur_background].bitmaps int _selectedSunIndex = -1; // index into Backgrounds[Cur_background].suns diff --git a/qtfred/ui/BackgroundEditor.ui b/qtfred/ui/BackgroundEditor.ui index 6800a49dfe4..7303acef3d6 100644 --- a/qtfred/ui/BackgroundEditor.ui +++ b/qtfred/ui/BackgroundEditor.ui @@ -541,6 +541,9 @@ + + QFrame::Box + @@ -781,9 +784,18 @@ + + + 0 + 0 + + R: 0 + + false + @@ -801,6 +813,9 @@ + + QFrame::Box + From b9229cd713fa4a3309f10f0704e3c317752447df Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 25 Aug 2025 22:25:29 -0500 Subject: [PATCH 440/466] more static --- qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp | 2 +- qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index ab15569b03f..d75f4c3e075 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -300,7 +300,7 @@ SCP_vector BackgroundEditorDialogModel::getAvailableBitmapNames() return out; } -SCP_vector BackgroundEditorDialogModel::getMissionBitmapNames() const +SCP_vector BackgroundEditorDialogModel::getMissionBitmapNames() { SCP_vector out; const auto& vec = getActiveBackground().bitmaps; diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h index 401129a6849..8d450ae654b 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -44,7 +44,7 @@ class BackgroundEditorDialogModel : public AbstractDialogModel { // bitmap group static SCP_vector getAvailableBitmapNames(); - SCP_vector getMissionBitmapNames() const; + static SCP_vector getMissionBitmapNames(); void setSelectedBitmapIndex(int index); int getSelectedBitmapIndex() const; void addMissionBitmapByName(const SCP_string& name); From 2b2320ffe76b4bb12f2774d60a3aa6a3f2a54129 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 26 Aug 2025 13:08:39 -0500 Subject: [PATCH 441/466] volumetric nebula dialog --- code/nebula/volumetrics.h | 7 +- qtfred/source_groups.cmake | 5 + .../dialogs/MissionGoalsDialogModel.cpp | 2 +- .../dialogs/VolumetricNebulaDialogModel.cpp | 446 +++++++++++ .../dialogs/VolumetricNebulaDialogModel.h | 139 ++++ qtfred/src/ui/FredView.cpp | 7 + qtfred/src/ui/FredView.h | 1 + .../src/ui/dialogs/VolumetricNebulaDialog.cpp | 365 +++++++++ .../src/ui/dialogs/VolumetricNebulaDialog.h | 101 +++ qtfred/ui/FredView.ui | 8 +- qtfred/ui/VolumetricNebulaDialog.ui | 727 ++++++++++++++++++ 11 files changed, 1805 insertions(+), 3 deletions(-) create mode 100644 qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.h create mode 100644 qtfred/src/ui/dialogs/VolumetricNebulaDialog.cpp create mode 100644 qtfred/src/ui/dialogs/VolumetricNebulaDialog.h create mode 100644 qtfred/ui/VolumetricNebulaDialog.ui diff --git a/code/nebula/volumetrics.h b/code/nebula/volumetrics.h index d80900935bd..a03b846fd70 100644 --- a/code/nebula/volumetrics.h +++ b/code/nebula/volumetrics.h @@ -9,6 +9,9 @@ namespace fso { namespace fred { class CFred_mission_save; + namespace dialogs { + class VolumetricNebulaDialogModel; + } } } @@ -87,7 +90,9 @@ class volumetric_nebula { friend class CFred_mission_save; //FRED friend class volumetrics_dlg; //FRED friend class fso::fred::CFred_mission_save; //QtFRED -public: + friend class fso::fred::dialogs::VolumetricNebulaDialogModel; // QtFRED + + public: volumetric_nebula(); ~volumetric_nebula(); diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index a25c2303b29..f0620155661 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -78,6 +78,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/VariableDialogModel.h src/mission/dialogs/VoiceActingManagerModel.h src/mission/dialogs/VoiceActingManagerModel.cpp + src/mission/dialogs/VolumetricNebulaDialogModel.cpp + src/mission/dialogs/VolumetricNebulaDialogModel.h src/mission/dialogs/WaypointEditorDialogModel.cpp src/mission/dialogs/WaypointEditorDialogModel.h src/mission/dialogs/WingEditorDialogModel.cpp @@ -176,6 +178,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/VariableDialog.h src/ui/dialogs/VoiceActingManager.h src/ui/dialogs/VoiceActingManager.cpp + src/ui/dialogs/VolumetricNebulaDialog.h + src/ui/dialogs/VolumetricNebulaDialog.cpp src/ui/dialogs/WaypointEditorDialog.cpp src/ui/dialogs/WaypointEditorDialog.h src/ui/dialogs/WingEditorDialog.cpp @@ -284,6 +288,7 @@ add_file_folder("UI" ui/ShieldSystemDialog.ui ui/SoundEnvironmentDialog.ui ui/VoiceActingManager.ui + ui/VolumetricNebulaDialog.ui ui/WaypointEditorDialog.ui ui/ShipEditorDialog.ui ui/ShipInitialStatus.ui diff --git a/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp index 4f1f8c0db13..ed9d5d605c0 100644 --- a/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp @@ -77,7 +77,7 @@ void MissionGoalsDialogModel::initializeData() { m_sig.clear(); for (size_t i=0; i(i)); if (m_goals[i].name.empty()) m_goals[i].name = ""; diff --git a/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp new file mode 100644 index 00000000000..c96ab4e7e74 --- /dev/null +++ b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp @@ -0,0 +1,446 @@ +#include "mission/dialogs/VolumetricNebulaDialogModel.h" + +namespace fso::fred::dialogs { + +VolumetricNebulaDialogModel::VolumetricNebulaDialogModel(QObject* parent, EditorViewport* viewport) : + AbstractDialogModel(parent, viewport), + _bypass_errors(false) +{ + initializeData(); +} + +bool VolumetricNebulaDialogModel::apply() +{ + if (!VolumetricNebulaDialogModel::validate_data()) { + return false; + } + if (!_volumetrics.enabled) { + The_mission.volumetrics.reset(); + } else { + if (!The_mission.volumetrics) { + The_mission.volumetrics.emplace(); + } + makeVolumetricsCopy(*The_mission.volumetrics, _volumetrics); + } + return true; +} + +void VolumetricNebulaDialogModel::reject() +{ + //do nothing - only here because parent class reject() function is virtual +} + +void VolumetricNebulaDialogModel::initializeData() +{ + if (The_mission.volumetrics) { + // Copy authoring fields into our working copy + makeVolumetricsCopy(_volumetrics, *The_mission.volumetrics); + } else { + // Start from engine defaults + makeVolumetricsCopy(_volumetrics, volumetric_nebula{}); + _volumetrics.enabled = false; + } +} + +bool VolumetricNebulaDialogModel::validate_data() +{ + if (!_volumetrics.enabled) { + return true; + } + else { + // be helpful to the FREDer; try to advise precisely what the problem is + // more general checks 1st, followed by more specific ones + _bypass_errors = false; + + if (_volumetrics.hullPof.empty()) { + showErrorDialogNoCancel("You must select a hull model for the volumetric nebula."); + return false; + } + + } + + return true; +} + +void VolumetricNebulaDialogModel::showErrorDialogNoCancel(const SCP_string& message) +{ + if (_bypass_errors) { + return; + } + + _bypass_errors = true; + _viewport->dialogProvider->showButtonDialog(DialogType::Error, + "Error", + message, + { DialogButton::Ok }); +} + +void VolumetricNebulaDialogModel::makeVolumetricsCopy(volumetric_nebula& dest, const volumetric_nebula& src) const +{ + // Instance / placement / look + dest.hullPof = src.hullPof; + dest.pos = src.pos; + dest.nebulaColor = src.nebulaColor; + + // Quality + dest.doEdgeSmoothing = src.doEdgeSmoothing; + dest.steps = src.steps; + dest.globalLightSteps = src.globalLightSteps; + dest.resolution = src.resolution; + dest.oversampling = src.oversampling; + dest.smoothing = src.smoothing; + dest.noiseResolution = src.noiseResolution; + + // Visibility + dest.opacityDistance = src.opacityDistance; + dest.alphaLim = src.alphaLim; + + // Emissive + dest.emissiveSpread = src.emissiveSpread; + dest.emissiveIntensity = src.emissiveIntensity; + dest.emissiveFalloff = src.emissiveFalloff; + + // Lighting (sun/global) + dest.henyeyGreensteinCoeff = src.henyeyGreensteinCoeff; + dest.globalLightDistanceFactor = src.globalLightDistanceFactor; + + // Noise authoring + dest.noiseActive = src.noiseActive; + dest.noiseScale = src.noiseScale; + dest.noiseColorFunc1 = src.noiseColorFunc1; + dest.noiseColorFunc2 = src.noiseColorFunc2; + dest.noiseColor = src.noiseColor; + dest.noiseColorIntensity = src.noiseColorIntensity; + + // Enabled flag + dest.enabled = src.enabled; +} + +bool VolumetricNebulaDialogModel::getEnabled() const +{ + return _volumetrics.enabled; +} + +void VolumetricNebulaDialogModel::setEnabled(bool e) +{ + modify(_volumetrics.enabled, e); +} + +const SCP_string& VolumetricNebulaDialogModel::getHullPof() const +{ + return _volumetrics.hullPof; +} + +void VolumetricNebulaDialogModel::setHullPof(const SCP_string& pofPath) +{ + modify(_volumetrics.hullPof, pofPath); +} + +float VolumetricNebulaDialogModel::getPosX() const +{ + return _volumetrics.pos.xyz.x; +} + +void VolumetricNebulaDialogModel::setPosX(float x) +{ + modify(_volumetrics.pos.xyz.x, x); +} + +float VolumetricNebulaDialogModel::getPosY() const +{ + return _volumetrics.pos.xyz.y; +} + +void VolumetricNebulaDialogModel::setPosY(float y) +{ + modify(_volumetrics.pos.xyz.y, y); +} + +float VolumetricNebulaDialogModel::getPosZ() const +{ + return _volumetrics.pos.xyz.z; +} + +void VolumetricNebulaDialogModel::setPosZ(float z) +{ + modify(_volumetrics.pos.xyz.z, z); +} + +int VolumetricNebulaDialogModel::getColorR() const +{ + const auto& c = _volumetrics.nebulaColor; + const int r = static_cast(std::get<0>(c) * 255.0f + 0.5f); + return std::clamp(r, 0, 255); +} + +void VolumetricNebulaDialogModel::setColorR(int r) +{ + CLAMP(r, 0 , 255); + auto t = _volumetrics.nebulaColor; + std::get<0>(t) = r / 255.0f; + modify(_volumetrics.nebulaColor, t); +} + +int VolumetricNebulaDialogModel::getColorG() const +{ + const auto& c = _volumetrics.nebulaColor; + const int g = static_cast(std::get<1>(c) * 255.0f + 0.5f); + return std::clamp(g, 0, 255); +} + +void VolumetricNebulaDialogModel::setColorG(int g) +{ + CLAMP(g, 0, 255); + auto t = _volumetrics.nebulaColor; + std::get<1>(t) = g / 255.0f; + modify(_volumetrics.nebulaColor, t); +} + +int VolumetricNebulaDialogModel::getColorB() const +{ + const auto& c = _volumetrics.nebulaColor; + const int b = static_cast(std::get<2>(c) * 255.0f + 0.5f); + return std::clamp(b, 0, 255); +} + +void VolumetricNebulaDialogModel::setColorB(int b) +{ + CLAMP(b, 0, 255); + auto t = _volumetrics.nebulaColor; + std::get<2>(t) = b / 255.0f; + modify(_volumetrics.nebulaColor, t); +} + +float VolumetricNebulaDialogModel::getOpacity() const +{ + return _volumetrics.alphaLim; +} + +void VolumetricNebulaDialogModel::setOpacity(float v) +{ + CLAMP(v, getOpacityLimit().first, getOpacityLimit().second); + modify(_volumetrics.alphaLim, v); +} + +float VolumetricNebulaDialogModel::getOpacityDistance() const +{ + return _volumetrics.opacityDistance; +} + +void VolumetricNebulaDialogModel::setOpacityDistance(float v) +{ + CLAMP(v, getOpacityDistanceLimit().first, getOpacityDistanceLimit().second); + modify(_volumetrics.opacityDistance, v); +} + +int VolumetricNebulaDialogModel::getSteps() const +{ + return _volumetrics.steps; +} + +void VolumetricNebulaDialogModel::setSteps(int v) +{ + CLAMP(v, getStepsLimit().first, getStepsLimit().second); + modify(_volumetrics.steps, v); +} + +int VolumetricNebulaDialogModel::getResolution() const +{ + return _volumetrics.resolution; +} + +void VolumetricNebulaDialogModel::setResolution(int v) +{ + CLAMP(v, getResolutionLimit().first, getResolutionLimit().second); + modify(_volumetrics.resolution, v); +} + +int VolumetricNebulaDialogModel::getOversampling() const +{ + return _volumetrics.oversampling; +} + +void VolumetricNebulaDialogModel::setOversampling(int v) +{ + CLAMP(v, getOversamplingLimit().first, getOversamplingLimit().second); + modify(_volumetrics.oversampling, v); +} + +float VolumetricNebulaDialogModel::getSmoothing() const +{ + return _volumetrics.smoothing; +} + +void VolumetricNebulaDialogModel::setSmoothing(float v) +{ + CLAMP(v, getSmoothingLimit().first, getSmoothingLimit().second); + modify(_volumetrics.smoothing, v); +} + +float VolumetricNebulaDialogModel::getHenyeyGreenstein() const +{ + return _volumetrics.henyeyGreensteinCoeff; +} + +void VolumetricNebulaDialogModel::setHenyeyGreenstein(float v) +{ + CLAMP(v, getHenyeyGreensteinLimit().first, getHenyeyGreensteinLimit().second); + modify(_volumetrics.henyeyGreensteinCoeff, v); +} + +float VolumetricNebulaDialogModel::getSunFalloffFactor() const +{ + return _volumetrics.globalLightDistanceFactor; +} + +void VolumetricNebulaDialogModel::setSunFalloffFactor(float v) +{ + CLAMP(v, getSunFalloffFactorLimit().first, getSunFalloffFactorLimit().second); + modify(_volumetrics.globalLightDistanceFactor, v); +} + +int VolumetricNebulaDialogModel::getSunSteps() const +{ + return _volumetrics.globalLightSteps; +} + +void VolumetricNebulaDialogModel::setSunSteps(int v) +{ + CLAMP(v, getSunStepsLimit().first, getSunStepsLimit().second); + modify(_volumetrics.globalLightSteps, v); +} + +float VolumetricNebulaDialogModel::getEmissiveSpread() const +{ + return _volumetrics.emissiveSpread; +} + +void VolumetricNebulaDialogModel::setEmissiveSpread(float v) +{ + CLAMP(v, getEmissiveSpreadLimit().first, getEmissiveSpreadLimit().second); + modify(_volumetrics.emissiveSpread, v); +} + +float VolumetricNebulaDialogModel::getEmissiveIntensity() const +{ + return _volumetrics.emissiveIntensity; +} + +void VolumetricNebulaDialogModel::setEmissiveIntensity(float v) +{ + CLAMP(v, getEmissiveIntensityLimit().first, getEmissiveIntensityLimit().second); + modify(_volumetrics.emissiveIntensity, v); +} + +float VolumetricNebulaDialogModel::getEmissiveFalloff() const +{ + return _volumetrics.emissiveFalloff; +} + +void VolumetricNebulaDialogModel::setEmissiveFalloff(float v) +{ + CLAMP(v, getEmissiveFalloffLimit().first, getEmissiveFalloffLimit().second); + modify(_volumetrics.emissiveFalloff, v); +} + +bool VolumetricNebulaDialogModel::getNoiseEnabled() const +{ + return _volumetrics.noiseActive; +} + +void VolumetricNebulaDialogModel::setNoiseEnabled(bool on) +{ + modify(_volumetrics.noiseActive, on); +} + +int VolumetricNebulaDialogModel::getNoiseColorR() const +{ + const auto& c = _volumetrics.noiseColor; + const int r = static_cast(std::get<0>(c) * 255.0f + 0.5f); + return std::clamp(r, 0, 255); +} +void VolumetricNebulaDialogModel::setNoiseColorR(int r) +{ + CLAMP(r, 0, 255); + auto t = _volumetrics.noiseColor; + std::get<0>(t) = r / 255.0f; + modify(_volumetrics.noiseColor, t); +} + +int VolumetricNebulaDialogModel::getNoiseColorG() const +{ + const auto& c = _volumetrics.noiseColor; + const int g = static_cast(std::get<1>(c) * 255.0f + 0.5f); + return std::clamp(g, 0, 255); +} +void VolumetricNebulaDialogModel::setNoiseColorG(int g) +{ + CLAMP(g, 0, 255); + auto t = _volumetrics.noiseColor; + std::get<1>(t) = g / 255.0f; + modify(_volumetrics.noiseColor, t); +} + +int VolumetricNebulaDialogModel::getNoiseColorB() const +{ + const auto& c = _volumetrics.noiseColor; + const int b = static_cast(std::get<2>(c) * 255.0f + 0.5f); + return std::clamp(b, 0, 255); +} +void VolumetricNebulaDialogModel::setNoiseColorB(int b) +{ + CLAMP(b, 0, 255); + auto t = _volumetrics.noiseColor; + std::get<2>(t) = b / 255.0f; + modify(_volumetrics.noiseColor, t); +} + +float VolumetricNebulaDialogModel::getNoiseScaleBase() const +{ + return std::get<0>(_volumetrics.noiseScale); +} + +void VolumetricNebulaDialogModel::setNoiseScaleBase(float v) +{ + CLAMP(v, getNoiseScaleBaseLimit().first, getNoiseScaleBaseLimit().second); + auto t = _volumetrics.noiseScale; + std::get<0>(t) = v; + modify(_volumetrics.noiseScale, t); +} + +float VolumetricNebulaDialogModel::getNoiseScaleSub() const +{ + return std::get<1>(_volumetrics.noiseScale); +} + +void VolumetricNebulaDialogModel::setNoiseScaleSub(float v) +{ + CLAMP(v, getNoiseScaleSubLimit().first, getNoiseScaleSubLimit().second); + auto t = _volumetrics.noiseScale; + std::get<1>(t) = v; + modify(_volumetrics.noiseScale, t); +} + +float VolumetricNebulaDialogModel::getNoiseIntensity() const +{ + return _volumetrics.noiseColorIntensity; +} + +void VolumetricNebulaDialogModel::setNoiseIntensity(float v) +{ + CLAMP(v, getNoiseIntensityLimit().first, getNoiseIntensityLimit().second); + modify(_volumetrics.noiseColorIntensity, v); +} + +int VolumetricNebulaDialogModel::getNoiseResolution() const +{ + return _volumetrics.noiseResolution; +} + +void VolumetricNebulaDialogModel::setNoiseResolution(int v) +{ + CLAMP(v, getNoiseResolutionLimit().first, getNoiseResolutionLimit().second); + modify(_volumetrics.noiseResolution, v); +} + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.h b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.h new file mode 100644 index 00000000000..893dcf85cca --- /dev/null +++ b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.h @@ -0,0 +1,139 @@ +#pragma once + +#include "mission/dialogs/AbstractDialogModel.h" + +#include "mission/missionparse.h" +#include "nebula/volumetrics.h" + +#include +#include +#include + +namespace fso::fred::dialogs { + +class VolumetricNebulaDialogModel : public AbstractDialogModel { + Q_OBJECT + +public: + VolumetricNebulaDialogModel(QObject* parent, EditorViewport* viewport); + + // overrides + bool apply() override; + void reject() override; + + // limits + static std::pair getOpacityLimit() { return {0.0001f, 1.0f}; } + static std::pair getOpacityDistanceLimit() { return {0.1f, 16777215.0f}; } // Qt max + static std::pair getStepsLimit() { return {1, 100}; } + static std::pair getResolutionLimit() { return {5, 8}; } + static std::pair getOversamplingLimit() { return {1, 3}; } + static std::pair getSmoothingLimit() { return {0.0f, 0.5f}; } + static std::pair getHenyeyGreensteinLimit() { return {-1.0f, 1.0f}; } + static std::pair getSunFalloffFactorLimit() { return {0.001f, 100.0f}; } + static std::pair getSunStepsLimit() { return {2, 16}; } + static std::pair getEmissiveSpreadLimit() { return {0.0f, 5.0f}; } + static std::pair getEmissiveIntensityLimit() { return {0.0f, 100.0f}; } + static std::pair getEmissiveFalloffLimit() { return {0.01f, 10.0f}; } + static std::pair getNoiseScaleBaseLimit() { return {0.01f, 1000.0f}; } + static std::pair getNoiseScaleSubLimit() { return {0.01f, 1000.0f}; } + static std::pair getNoiseIntensityLimit() { return {0.1f, 100.0f}; } + static std::pair getNoiseResolutionLimit() { return {5, 8}; } + + bool getEnabled() const; + void setEnabled(bool e); + + // Basic + const SCP_string& getHullPof() const; + void setHullPof(const SCP_string& pofPath); + + float getPosX() const; + void setPosX(float x); + float getPosY() const; + void setPosY(float y); + float getPosZ() const; + void setPosZ(float z); + + // Color + int getColorR() const; + void setColorR(int r); + int getColorG() const; + void setColorG(int g); + int getColorB() const; + void setColorB(int b); + + // Visibility + float getOpacity() const; + void setOpacity(float v); + + float getOpacityDistance() const; + void setOpacityDistance(float v); + + // Quality + int getSteps() const; + void setSteps(int v); + + int getResolution() const; + void setResolution(int v); + + int getOversampling() const; + void setOversampling(int v); + + float getSmoothing() const; + void setSmoothing(float v); + + // Lighting + float getHenyeyGreenstein() const; + void setHenyeyGreenstein(float v); + + float getSunFalloffFactor() const; + void setSunFalloffFactor(float v); + + int getSunSteps() const; + void setSunSteps(int v); + + // Emissive + float getEmissiveSpread() const; + void setEmissiveSpread(float v); + + float getEmissiveIntensity() const; + void setEmissiveIntensity(float v); + + float getEmissiveFalloff() const; + void setEmissiveFalloff(float v); + + // Noise + bool getNoiseEnabled() const; + void setNoiseEnabled(bool on); + + int getNoiseColorR() const; + void setNoiseColorR(int r); + int getNoiseColorG() const; + void setNoiseColorG(int g); + int getNoiseColorB() const; + void setNoiseColorB(int b); + + float getNoiseScaleBase() const; + void setNoiseScaleBase(float v); + float getNoiseScaleSub() const; + void setNoiseScaleSub(float v); + + float getNoiseIntensity() const; + void setNoiseIntensity(float v); + + int getNoiseResolution() const; + void setNoiseResolution(int v); + +private: + void initializeData(); + bool validate_data(); + void showErrorDialogNoCancel(const SCP_string& message); + + void makeVolumetricsCopy(volumetric_nebula& dest, const volumetric_nebula& src) const; + + // boilerplate + bool _bypass_errors; + + volumetric_nebula _volumetrics; +}; + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 0e316bc4a77..bd8b4c1dd0e 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -738,6 +739,12 @@ void FredView::on_actionAsteroid_Field_triggered(bool) { asteroidFieldEditor->setAttribute(Qt::WA_DeleteOnClose); asteroidFieldEditor->show(); } +void FredView::on_actionVolumetric_Nebula_triggered(bool) +{ + auto volumetricNebulaEditor = new dialogs::VolumetricNebulaDialog(this, _viewport); + volumetricNebulaEditor->setAttribute(Qt::WA_DeleteOnClose); + volumetricNebulaEditor->show(); +} void FredView::on_actionBriefing_triggered(bool) { auto eventEditor = new dialogs::BriefingEditorDialog(this); eventEditor->setAttribute(Qt::WA_DeleteOnClose); diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index 98b194f681c..a2c2743ae02 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -94,6 +94,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionMission_Events_triggered(bool); void on_actionMission_Cutscenes_triggered(bool); void on_actionAsteroid_Field_triggered(bool); + void on_actionVolumetric_Nebula_triggered(bool); void on_actionBriefing_triggered(bool); void on_actionMission_Specs_triggered(bool); void on_actionWaypoint_Paths_triggered(bool); diff --git a/qtfred/src/ui/dialogs/VolumetricNebulaDialog.cpp b/qtfred/src/ui/dialogs/VolumetricNebulaDialog.cpp new file mode 100644 index 00000000000..bddd931f4ea --- /dev/null +++ b/qtfred/src/ui/dialogs/VolumetricNebulaDialog.cpp @@ -0,0 +1,365 @@ +#include "ui/dialogs/VolumetricNebulaDialog.h" +#include "ui/util/SignalBlockers.h" + +#include "ui_VolumetricNebulaDialog.h" +#include + +#include +#include + +namespace fso::fred::dialogs { + +VolumetricNebulaDialog::VolumetricNebulaDialog(FredView* parent, EditorViewport* viewport) : + QDialog(parent), _viewport(viewport), ui(new Ui::VolumetricNebulaDialog()), + _model(new VolumetricNebulaDialogModel(this, viewport)) +{ + this->setFocus(); + ui->setupUi(this); + + // set our internal values, update the UI + initializeUi(); + updateUi(); +} + +VolumetricNebulaDialog::~VolumetricNebulaDialog() = default; + +void VolumetricNebulaDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void VolumetricNebulaDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close +} + +void VolumetricNebulaDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window +} + +void VolumetricNebulaDialog::initializeUi() +{ + util::SignalBlockers blockers(this); // block signals while we set up the UI + + // Set ranges + ui->opacityDoubleSpinBox->setRange(_model->getOpacityLimit().first, _model->getOpacityLimit().second); + ui->opacityDistanceDoubleSpinBox->setRange(_model->getOpacityDistanceLimit().first, _model->getOpacityDistanceLimit().second); + ui->renderQualityStepsSpinBox->setRange(_model->getStepsLimit().first, _model->getStepsLimit().second); + ui->resolutionSpinBox->setRange(_model->getResolutionLimit().first, _model->getResolutionLimit().second); + ui->oversamplingSpinBox->setRange(_model->getOversamplingLimit().first, _model->getOversamplingLimit().second); + ui->smoothingDoubleSpinBox->setRange(_model->getSmoothingLimit().first, _model->getSmoothingLimit().second); + ui->henyeyGreensteinCoeffDoubleSpinBox->setRange(_model->getHenyeyGreensteinLimit().first, _model->getHenyeyGreensteinLimit().second); + ui->sunFalloffFactorDoubleSpinBox->setRange(_model->getSunFalloffFactorLimit().first, _model->getSunFalloffFactorLimit().second); + ui->sunQualityStepsSpinBox->setRange(_model->getSunStepsLimit().first, _model->getSunStepsLimit().second); + ui->emissiveLightDoubleSpinBox->setRange(_model->getEmissiveSpreadLimit().first, _model->getEmissiveSpreadLimit().second); + ui->emissiveLightIntensityDoubleSpinBox->setRange(_model->getEmissiveIntensityLimit().first, _model->getEmissiveIntensityLimit().second); + ui->emissiveLightFalloffDoubleSpinBox->setRange(_model->getEmissiveFalloffLimit().first, _model->getEmissiveFalloffLimit().second); + ui->noiseScaleBaseDoubleSpinBox->setRange(_model->getNoiseScaleBaseLimit().first, _model->getNoiseScaleBaseLimit().second); + ui->noiseScaleSubDoubleSpinBox->setRange(_model->getNoiseScaleSubLimit().first, _model->getNoiseScaleSubLimit().second); + ui->noiseIntensityDoubleSpinBox->setRange(_model->getNoiseIntensityLimit().first, _model->getNoiseIntensityLimit().second); + ui->noiseResolutionSpinBox->setRange(_model->getNoiseResolutionLimit().first, _model->getNoiseResolutionLimit().second); +} + +void VolumetricNebulaDialog::updateUi() +{ + util::SignalBlockers blockers(this); // block signals while we update the UI + + enableDisableControls(); + + ui->enabled->setChecked(_model->getEnabled()); + + ui->setModelLineEdit->setText(QString::fromStdString(_model->getHullPof())); + ui->positionXSpinBox->setValue(_model->getPosX()); + ui->positionYSpinBox->setValue(_model->getPosY()); + ui->positionZSpinBox->setValue(_model->getPosZ()); + ui->colorRSpinBox->setValue(_model->getColorR()); + ui->colorGSpinBox->setValue(_model->getColorG()); + ui->colorBSpinBox->setValue(_model->getColorB()); + + ui->opacityDoubleSpinBox->setValue(_model->getOpacity()); + ui->opacityDistanceDoubleSpinBox->setValue(_model->getOpacityDistance()); + ui->renderQualityStepsSpinBox->setValue(_model->getSteps()); + ui->resolutionSpinBox->setValue(_model->getResolution()); + ui->oversamplingSpinBox->setValue(_model->getOversampling()); + ui->smoothingDoubleSpinBox->setValue(_model->getSmoothing()); + ui->henyeyGreensteinCoeffDoubleSpinBox->setValue(_model->getHenyeyGreenstein()); + ui->sunFalloffFactorDoubleSpinBox->setValue(_model->getSunFalloffFactor()); + ui->sunQualityStepsSpinBox->setValue(_model->getSunSteps()); + + ui->emissiveLightDoubleSpinBox->setValue(_model->getEmissiveSpread()); + ui->emissiveLightIntensityDoubleSpinBox->setValue(_model->getEmissiveIntensity()); + ui->emissiveLightFalloffDoubleSpinBox->setValue(_model->getEmissiveFalloff()); + + ui->enableNoiseCheckBox->setChecked(_model->getNoiseEnabled()); + ui->noiseColorRSpinBox->setValue(_model->getNoiseColorR()); + ui->noiseColorGSpinBox->setValue(_model->getNoiseColorG()); + ui->noiseColorBSpinBox->setValue(_model->getNoiseColorB()); + ui->noiseScaleBaseDoubleSpinBox->setValue(_model->getNoiseScaleBase()); + ui->noiseScaleSubDoubleSpinBox->setValue(_model->getNoiseScaleSub()); + ui->noiseIntensityDoubleSpinBox->setValue(_model->getNoiseIntensity()); + ui->noiseResolutionSpinBox->setValue(_model->getNoiseResolution()); + + updateColorSwatch(); + updateNoiseColorSwatch(); +} + +void VolumetricNebulaDialog::enableDisableControls() +{ + bool enabled = _model->getEnabled(); + + ui->setModelButton->setEnabled(enabled); + ui->setModelLineEdit->setEnabled(enabled); + ui->positionXSpinBox->setEnabled(enabled); + ui->positionYSpinBox->setEnabled(enabled); + ui->positionZSpinBox->setEnabled(enabled); + ui->colorRSpinBox->setEnabled(enabled); + ui->colorGSpinBox->setEnabled(enabled); + ui->colorBSpinBox->setEnabled(enabled); + ui->opacityDoubleSpinBox->setEnabled(enabled); + ui->opacityDistanceDoubleSpinBox->setEnabled(enabled); + ui->renderQualityStepsSpinBox->setEnabled(enabled); + ui->resolutionSpinBox->setEnabled(enabled); + ui->oversamplingSpinBox->setEnabled(enabled); + ui->smoothingDoubleSpinBox->setEnabled(enabled); + ui->henyeyGreensteinCoeffDoubleSpinBox->setEnabled(enabled); + ui->sunFalloffFactorDoubleSpinBox->setEnabled(enabled); + ui->sunQualityStepsSpinBox->setEnabled(enabled); + ui->emissiveLightDoubleSpinBox->setEnabled(enabled); + ui->emissiveLightIntensityDoubleSpinBox->setEnabled(enabled); + ui->emissiveLightFalloffDoubleSpinBox->setEnabled(enabled); + + ui->enableNoiseCheckBox->setEnabled(enabled); + + bool noiseEnabled = enabled && _model->getNoiseEnabled(); + + ui->noiseColorRSpinBox->setEnabled(noiseEnabled); + ui->noiseColorGSpinBox->setEnabled(noiseEnabled); + ui->noiseColorBSpinBox->setEnabled(noiseEnabled); + ui->noiseScaleBaseDoubleSpinBox->setEnabled(noiseEnabled); + ui->noiseScaleSubDoubleSpinBox->setEnabled(noiseEnabled); + ui->noiseIntensityDoubleSpinBox->setEnabled(noiseEnabled); + ui->noiseResolutionSpinBox->setEnabled(noiseEnabled); + ui->setBaseNoiseFunctionButton->setEnabled(noiseEnabled); + ui->setSubNoiseFunctionButton->setEnabled(noiseEnabled); + +} + +void VolumetricNebulaDialog::updateColorSwatch() +{ + const int r = _model->getColorR(); + const int g = _model->getColorG(); + const int b = _model->getColorB(); + ui->colorPreview->setStyleSheet(QString("background: rgb(%1,%2,%3);" + "border: 1px solid #444; border-radius: 3px;") + .arg(r) + .arg(g) + .arg(b)); +} + +void VolumetricNebulaDialog::updateNoiseColorSwatch() +{ + const int r = _model->getNoiseColorR(); + const int g = _model->getNoiseColorG(); + const int b = _model->getNoiseColorB(); + ui->noiseColorPreview->setStyleSheet(QString("background: rgb(%1,%2,%3);" + "border: 1px solid #444; border-radius: 3px;") + .arg(r) + .arg(g) + .arg(b)); +} + +void VolumetricNebulaDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void VolumetricNebulaDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void VolumetricNebulaDialog::on_enabled_toggled(bool checked) +{ + _model->setEnabled(checked); + enableDisableControls(); +} + +void VolumetricNebulaDialog::on_setModelButton_clicked() +{ + const QString path = QFileDialog::getOpenFileName(this, + "Select POF File", + QString(), + "Freespace 2 Model Files (*.pof);;All Files (*)"); + if (path.isEmpty()) + return; + + const QString filename = QFileInfo(path).fileName(); + _model->setHullPof(filename.toUtf8().constData()); + updateUi(); +} + +void VolumetricNebulaDialog::on_setModelLineEdit_textChanged(const QString& text) +{ + _model->setHullPof(text.toUtf8().constData()); +} + +void VolumetricNebulaDialog::on_positionXSpinBox_valueChanged(int v) +{ + _model->setPosX(static_cast(v)); +} + +void VolumetricNebulaDialog::on_positionYSpinBox_valueChanged(int v) +{ + _model->setPosY(static_cast(v)); +} + +void VolumetricNebulaDialog::on_positionZSpinBox_valueChanged(int v) +{ + _model->setPosZ(static_cast(v)); +} + +void VolumetricNebulaDialog::on_colorRSpinBox_valueChanged(int v) +{ + _model->setColorR(v); + updateColorSwatch(); +} + +void VolumetricNebulaDialog::on_colorGSpinBox_valueChanged(int v) +{ + _model->setColorG(v); + updateColorSwatch(); +} + +void VolumetricNebulaDialog::on_colorBSpinBox_valueChanged(int v) +{ + _model->setColorB(v); + updateColorSwatch(); +} + +void VolumetricNebulaDialog::on_opacityDoubleSpinBox_valueChanged(double v) +{ + _model->setOpacity(static_cast(v)); +} + +void VolumetricNebulaDialog::on_opacityDistanceDoubleSpinBox_valueChanged(double v) +{ + _model->setOpacityDistance(static_cast(v)); +} + +void VolumetricNebulaDialog::on_renderQualityStepsSpinBox_valueChanged(int v) +{ + _model->setSteps(v); +} + +void VolumetricNebulaDialog::on_resolutionSpinBox_valueChanged(int v) +{ + _model->setResolution(v); +} + +void VolumetricNebulaDialog::on_oversamplingSpinBox_valueChanged(int v) +{ + _model->setOversampling(v); +} + +void VolumetricNebulaDialog::on_smoothingDoubleSpinBox_valueChanged(double v) +{ + _model->setSmoothing(static_cast(v)); +} + +void VolumetricNebulaDialog::on_henyeyGreensteinCoeffDoubleSpinBox_valueChanged(double v) +{ + _model->setHenyeyGreenstein(static_cast(v)); +} + +void VolumetricNebulaDialog::on_sunFalloffFactorDoubleSpinBox_valueChanged(double v) +{ + _model->setSunFalloffFactor(static_cast(v)); +} + +void VolumetricNebulaDialog::on_sunQualityStepsSpinBox_valueChanged(int v) +{ + _model->setSunSteps(v); +} + +void VolumetricNebulaDialog::on_emissiveLightDoubleSpinBox_valueChanged(double v) +{ + _model->setEmissiveSpread(static_cast(v)); +} + +void VolumetricNebulaDialog::on_emissiveLightIntensityDoubleSpinBox_valueChanged(double v) +{ + _model->setEmissiveIntensity(static_cast(v)); +} + +void VolumetricNebulaDialog::on_emissiveLightFalloffDoubleSpinBox_valueChanged(double v) +{ + _model->setEmissiveFalloff(static_cast(v)); +} + +void VolumetricNebulaDialog::on_enableNoiseCheckBox_toggled(bool enabled) +{ + _model->setNoiseEnabled(enabled); + enableDisableControls(); +} + +void VolumetricNebulaDialog::on_noiseColorRSpinBox_valueChanged(int v) +{ + _model->setNoiseColorR(v); + updateNoiseColorSwatch(); +} + +void VolumetricNebulaDialog::on_noiseColorGSpinBox_valueChanged(int v) +{ + _model->setNoiseColorG(v); + updateNoiseColorSwatch(); +} + +void VolumetricNebulaDialog::on_noiseColorBSpinBox_valueChanged(int v) +{ + _model->setNoiseColorB(v); + updateNoiseColorSwatch(); +} + +void VolumetricNebulaDialog::on_noiseScaleBaseDoubleSpinBox_valueChanged(double v) +{ + _model->setNoiseScaleBase(static_cast(v)); +} + +void VolumetricNebulaDialog::on_noiseScaleSubDoubleSpinBox_valueChanged(double v) +{ + _model->setNoiseScaleSub(static_cast(v)); +} + +void VolumetricNebulaDialog::on_noiseIntensityDoubleSpinBox_valueChanged(double v) +{ + _model->setNoiseIntensity(static_cast(v)); +} + +void VolumetricNebulaDialog::on_noiseResolutionSpinBox_valueChanged(int v) +{ + _model->setNoiseResolution(v); +} + +void VolumetricNebulaDialog::on_setBaseNoiseFunctionButton_clicked() +{ + QMessageBox::information(this, "Not Implemented", "Setting the base noise function is not implemented yet."); +} + +void VolumetricNebulaDialog::on_setSubNoiseFunctionButton_clicked() +{ + QMessageBox::information(this, "Not Implemented", "Setting the sub noise function is not implemented yet."); +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/ui/dialogs/VolumetricNebulaDialog.h b/qtfred/src/ui/dialogs/VolumetricNebulaDialog.h new file mode 100644 index 00000000000..b488ab77b90 --- /dev/null +++ b/qtfred/src/ui/dialogs/VolumetricNebulaDialog.h @@ -0,0 +1,101 @@ +#pragma once + +#include + +#include +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class VolumetricNebulaDialog; +} + +class VolumetricNebulaDialog : public QDialog { + Q_OBJECT +public: + VolumetricNebulaDialog(FredView* parent, EditorViewport* viewport); + ~VolumetricNebulaDialog() override; + + void accept() override; + void reject() override; + +protected: + void closeEvent(QCloseEvent* e) override; // funnel all Window X presses through reject() + + +private slots: + // dialog controls + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + // Master toggle + void on_enabled_toggled(bool enabled); + + // Model + void on_setModelButton_clicked(); + void on_setModelLineEdit_textChanged(const QString& text); + + // Position + void on_positionXSpinBox_valueChanged(int v); + void on_positionYSpinBox_valueChanged(int v); + void on_positionZSpinBox_valueChanged(int v); + + // Color + void on_colorRSpinBox_valueChanged(int v); + void on_colorGSpinBox_valueChanged(int v); + void on_colorBSpinBox_valueChanged(int v); + + // Visibility + void on_opacityDoubleSpinBox_valueChanged(double v); + void on_opacityDistanceDoubleSpinBox_valueChanged(double v); + + // Quality + void on_renderQualityStepsSpinBox_valueChanged(int v); + void on_resolutionSpinBox_valueChanged(int v); + void on_oversamplingSpinBox_valueChanged(int v); + void on_smoothingDoubleSpinBox_valueChanged(double v); + + // Lighting + void on_henyeyGreensteinCoeffDoubleSpinBox_valueChanged(double v); + void on_sunFalloffFactorDoubleSpinBox_valueChanged(double v); + void on_sunQualityStepsSpinBox_valueChanged(int v); + + // Emissive + void on_emissiveLightDoubleSpinBox_valueChanged(double v); + void on_emissiveLightIntensityDoubleSpinBox_valueChanged(double v); + void on_emissiveLightFalloffDoubleSpinBox_valueChanged(double v); + + // Noise toggle + void on_enableNoiseCheckBox_toggled(bool enabled); + + // Noise params + void on_noiseColorRSpinBox_valueChanged(int v); + void on_noiseColorGSpinBox_valueChanged(int v); + void on_noiseColorBSpinBox_valueChanged(int v); + void on_noiseScaleBaseDoubleSpinBox_valueChanged(double v); + void on_noiseScaleSubDoubleSpinBox_valueChanged(double v); + void on_noiseIntensityDoubleSpinBox_valueChanged(double v); + void on_noiseResolutionSpinBox_valueChanged(int v); + + // Noise functions + void on_setBaseNoiseFunctionButton_clicked(); + void on_setSubNoiseFunctionButton_clicked(); + + +private: // NOLINT(readability-redundant-access-specifiers) + void initializeUi(); + void updateUi(); + void enableDisableControls(); + + void updateColorSwatch(); + void updateNoiseColorSwatch(); + + // Boilerplate + EditorViewport* _viewport = nullptr; + std::unique_ptr ui; + std::unique_ptr _model; +}; + + +} // namespace fso::fred::dialogs diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index 71e95e6a198..0348e11c87c 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -20,7 +20,7 @@ 0 0 926 - 22 + 26 @@ -185,6 +185,7 @@ + @@ -1546,6 +1547,11 @@ Music Player + + + Volumetric Nebula + + diff --git a/qtfred/ui/VolumetricNebulaDialog.ui b/qtfred/ui/VolumetricNebulaDialog.ui new file mode 100644 index 00000000000..03361a59b22 --- /dev/null +++ b/qtfred/ui/VolumetricNebulaDialog.ui @@ -0,0 +1,727 @@ + + + fso::fred::dialogs::VolumetricNebulaDialog + + + Qt::WindowModal + + + + 0 + 0 + 957 + 463 + + + + + 0 + 0 + + + + + 0 + 0 + + + + Volumetric Nebula Editor + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Set Volumetric Nebula Model + + + + + + + + + + Position + + + + + + + + + X + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 16777215 + + + + + + + Y + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 16777215 + + + + + + + Z + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 16777215 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + R + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 255 + + + + + + + G + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 255 + + + + + + + B + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 255 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Color + + + + + + + + 30 + 0 + + + + QFrame::Box + + + + + + + + + + + + Opacity + + + + + + + 4 + + + 0.000100000000000 + + + 1.000000000000000 + + + 0.010000000000000 + + + + + + + Opacity Distance + + + + + + + Render Quality Steps + + + + + + + 1 + + + 100 + + + + + + + Resolution + + + + + + + 5 + + + 8 + + + + + + + Oversampling + + + + + + + 1 + + + 3 + + + + + + + Smoothing + + + + + + + Henyey-Greenstein Coeff. + + + + + + + -1.000000000000000 + + + 1.000000000000000 + + + 0.100000000000000 + + + + + + + Sun Falloff Factor + + + + + + + Sun Quality Steps + + + + + + + 2 + + + 16 + + + + + + + 2 + + + 0.100000000000000 + + + 16777215.000000000000000 + + + + + + + 0.500000000000000 + + + 0.010000000000000 + + + + + + + 3 + + + 0.001000000000000 + + + 100.000000000000000 + + + 1.000000000000000 + + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Emissive Light + + + + + + + 5.000000000000000 + + + 0.100000000000000 + + + + + + + Emissive Light Intensity + + + + + + + 100.000000000000000 + + + 0.100000000000000 + + + + + + + Emissive Light Falloff + + + + + + + 0.010000000000000 + + + 10.000000000000000 + + + 0.100000000000000 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Noise Settings + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Enable Noise + + + + + + + + + Color + + + + + + + + 30 + 0 + + + + QFrame::Box + + + + + + + + + + + + + + R + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 255 + + + + + + + G + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 255 + + + + + + + B + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 255 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Scale + + + + + + + QLayout::SetDefaultConstraint + + + + + Base + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 0.010000000000000 + + + 1000.000000000000000 + + + + + + + Sub + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + 0.010000000000000 + + + 1000.000000000000000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Intensity + + + + + + + Resolution + + + + + + + 5 + + + 8 + + + + + + + + + Set Base Noise Function + + + + + + + Set Sub Noise Function + + + + + + + + + 0.100000000000000 + + + 100.000000000000000 + + + + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Enabled + + + + + + + + From 772920b727f00dfd56b0f4d11dd9495056cee8b2 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 26 Aug 2025 13:30:15 -0500 Subject: [PATCH 442/466] static method --- qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp index c96ab4e7e74..6e336321380 100644 --- a/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp +++ b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp @@ -75,7 +75,7 @@ void VolumetricNebulaDialogModel::showErrorDialogNoCancel(const SCP_string& mess { DialogButton::Ok }); } -void VolumetricNebulaDialogModel::makeVolumetricsCopy(volumetric_nebula& dest, const volumetric_nebula& src) const +void VolumetricNebulaDialogModel::makeVolumetricsCopy(volumetric_nebula& dest, const volumetric_nebula& src) { // Instance / placement / look dest.hullPof = src.hullPof; From 689d184869e6b7081fcc4370cfd7eae528188ab7 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Tue, 26 Aug 2025 13:41:01 -0500 Subject: [PATCH 443/466] commit error --- qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.h b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.h index 893dcf85cca..6515e5d9852 100644 --- a/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.h +++ b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.h @@ -128,7 +128,7 @@ class VolumetricNebulaDialogModel : public AbstractDialogModel { bool validate_data(); void showErrorDialogNoCancel(const SCP_string& message); - void makeVolumetricsCopy(volumetric_nebula& dest, const volumetric_nebula& src) const; + static void makeVolumetricsCopy(volumetric_nebula& dest, const volumetric_nebula& src); // boilerplate bool _bypass_errors; From b3db62cf256717be106743d2827fbf97ba804480 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 29 Aug 2025 08:05:11 -0500 Subject: [PATCH 444/466] QtFRED Mission Events Editor (#6961) * Match file framework of mission events to the rest * first pass at splitting up the model and ui layers * fix up message side of event editor * fix event selection * fix lots of event bugs * Get event annotations working * type to search for operators * control annotations * Add event dragging and fix event deletion * cleanup * Event and Msg order manipulation * implement some remaining message todos * minor cleanup * prep for browse ani dialog picker * basic file browser for now * cleanup * unused var * more unused * clang * static member access * rebase error * fix rebase error * address feedback * missing parenthesis --- code/mission/missionmessage.h | 2 + fred2/eventeditor.h | 2 - qtfred/resources/images/arrow_down.png | Bin 0 -> 2144 bytes qtfred/resources/images/arrow_up.png | Bin 0 -> 2149 bytes qtfred/resources/images/comment.png | Bin 235 -> 1962 bytes qtfred/resources/resources.qrc | 3 + qtfred/source_groups.cmake | 8 +- .../dialogs/MissionEventsDialogModel.cpp | 1498 +++++++++++++++++ .../dialogs/MissionEventsDialogModel.h | 206 +++ qtfred/src/ui/FredView.cpp | 4 +- qtfred/src/ui/dialogs/EventEditorDialog.cpp | 1129 ------------- qtfred/src/ui/dialogs/EventEditorDialog.h | 111 -- qtfred/src/ui/dialogs/MissionEventsDialog.cpp | 1025 +++++++++++ qtfred/src/ui/dialogs/MissionEventsDialog.h | 129 ++ qtfred/src/ui/widgets/sexp_tree.cpp | 680 +++++++- qtfred/src/ui/widgets/sexp_tree.h | 43 +- ...EditorDialog.ui => MissionEventsDialog.ui} | 473 +++--- 17 files changed, 3793 insertions(+), 1520 deletions(-) create mode 100644 qtfred/resources/images/arrow_down.png create mode 100644 qtfred/resources/images/arrow_up.png create mode 100644 qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/MissionEventsDialogModel.h delete mode 100644 qtfred/src/ui/dialogs/EventEditorDialog.cpp delete mode 100644 qtfred/src/ui/dialogs/EventEditorDialog.h create mode 100644 qtfred/src/ui/dialogs/MissionEventsDialog.cpp create mode 100644 qtfred/src/ui/dialogs/MissionEventsDialog.h rename qtfred/ui/{EventEditorDialog.ui => MissionEventsDialog.ui} (72%) diff --git a/code/mission/missionmessage.h b/code/mission/missionmessage.h index f8f84e851aa..6d2834305ab 100644 --- a/code/mission/missionmessage.h +++ b/code/mission/missionmessage.h @@ -56,6 +56,8 @@ extern SCP_vector Message_waves; #define DEFAULT_COMMAND "Command" #define DEFAULT_HASHCOMMAND "#" DEFAULT_COMMAND +#define MAX_SEARCH_MESSAGE_DEPTH 5 // maximum search number of event nodes with message text + extern SCP_vector Builtin_moods; extern int Current_mission_mood; extern float Command_announces_enemy_arrival_chance; diff --git a/fred2/eventeditor.h b/fred2/eventeditor.h index d5654fec223..20cf0697dea 100644 --- a/fred2/eventeditor.h +++ b/fred2/eventeditor.h @@ -16,8 +16,6 @@ #include "mission/missiongoals.h" #include "mission/missionmessage.h" -#define MAX_SEARCH_MESSAGE_DEPTH 5 // maximum search number of event nodes with message text - class event_sexp_tree : public sexp_tree { diff --git a/qtfred/resources/images/arrow_down.png b/qtfred/resources/images/arrow_down.png new file mode 100644 index 0000000000000000000000000000000000000000..98ab8f990229dcd2055ccb383cef77735de12e76 GIT binary patch literal 2144 zcmbVO3rrJt96lul;Ga+NVG`LnCV+cG3k&8{pQbiD6W66e7 zMK=!{)db4oI0fsCYNO7GGX{MkOyZ;#!zm2cp@fkpNE)mMe+ZCsb9Oq%yeudN?o3FL zEK4+odA(k>mr{%FLX0pPjTla1B#8n9>hZf|#)rB*^ClR~yoYrQk}QZWSYc#rVu@@* zfYtFBoKlF^ zXHbWhpAZ1KwOB$LPxaz-h9o?4S}6!4n2@KUJ$ZhK$8va&SmI{+v{GPmo)V2jr@DDY z7TtNG=$NQf_Jn1a)Txc|f_%ZniC)jb$2Rb0M&?Zjs2Yjl29(g}5frV}(gcO#S{ld4 zp%#%7?EVl`t3XLSj{pdg))G%ZLCa43y*s^}2i zKp42A##~q|bcV|#GcK0TFq;q%vsw^1+Gev8q)x9z8Jmqnb-0E@4U`6W!f`#JH!v(o zF%$dEB3q(VR@pz^<($X@j(=4(IFGX=VMiH)(x5hjhC&Uj-iA`7&Oqq2dXk~Y3APn( z0la98<8fBS6$cn~oYBAmB2){^pc)FeGHN*uAT!uB8cs)%xLxVyu?C^j1rI2lKlpy+ z@*f1R90EMn6Ewpry>3ESrN}&o1Ro1e$;V462lso6ctHA`;*YC)M7!)|-2Bo)kh>Q) z7xpak9;Wp9)?coa(F7dK zIOQ@lUBOLgKJzX>?FQ5LM)k^_5H#78VP2Y-9>b1|j;4e)<6#$KMjsAi;o;T!`T2$E znVGJfRjbxl@7PsWr%R&gsHNEj`3Nov8TVvZOl%D zd!*W?FZb_H$jr=)s~Q{}tRPxd2WG`m$J^T*_O-OcF@~*SoqoSRdcL(`Q%@LC6*lj6 zha}6IzJl7OsL06d7)-4WzY5RoNKCg_x{3{Wm*(gG{)+YMR<91%h1p|&m1?(bKD%Sz z;lq)R4+UZ2>57O~5?WhZZ{4Kk)W0=*^Y`qb=4RC$6;o^6YuGaTy_G8mjx;xa*LVKB zW6_k|_4Qe*?y{3rRo&Y1s<8VV!s5kE8|stedM{sId+qvlmrDQl!{MRw4k>BdCD?W> z5V)~t(AR)8(TXPA3?iM zb$55iCzp@FTUVr5PcxAh11Vx>PtULlz29!O&g5ps=C+mI{YCn%?%2lB4e_=I(=>Aq zwlp?2PUc2BE2DN+|CHJ`&@sh+qm|z_xAv0x$Q#ht{YVWyIjLXy2FOUWnvcFy@XJ$PC@v-Md@4yS?@n?tn&3 zO7Vqn%%re~8bgW>KAJ?7@X7psR4lb)q zVDrjM7P_p6CKxzvB9!VU0fCDZDah|~y1k@d4+nTja4kJXVJH9*i}Y~1BnTB4bD<1g zU?D<|BQ%C7AuS;%Gz3QJwBsQKrck4p62&wKPLQ~Q1na>c4CDmHMw$&%f@0uK4?9HB zL!zk9=ac)Ca$c~bI6)97ra%=61RxM^savG{h})ak&tPD^w7_{pj(0;6BW2}_MLi6x z_Ql}xglOH~V4gt6P(S5CaXBW%6aX@G2);@4bW+hHh_MlyqU@&XVB z?vw!+MkAT+_KK97X0r`?7{n~+IEEx>O0BfgDul%p3_{^54AEJY1VX4(R-H}DGFm3I z-@wzwQe~z6eO=D*G~oDGRb$09O2&pERvnHbI+Y3slxh`XQxaNQt5eu;ovPo>G=T#z z8s)s7RdU4uMkR$Q)tFX|SXDHGU^bN!A#j>TRJ2A**mPa71PmmNX^|~IWr6RKo9DK|@Bp*+u9Nh17uz>V2#qU%1@;1>&32eF@h zg+9u>mnwO@^`|RkpQn)ZM>XUBu^vnd?V#Lt7EDeQmV!a05z|)`^uP5Atlc+``r%;4 zNtdDN3T{I4nRNqd0ZiXH^QMQ%WZ{#u4e6Gw#k<$0eQthj_`w75Ut|}>6y-(_c+(JV zvFx(79M09|MZ9?L!mOA@g%uS?HvTmIW3{^bPhp6`5tX=np=anzubfI6n$lT&!gsNI zf8E}{FR5?;(N$C4+;QuKzJu6N*zohbmelF>!%&@VVr_>*vr&GkS+1J0qsd&?Ie$pQ z**Pm3a;iFhzt)==lL)Wsw%4C6OBr32U)9ljr#XFm?7dBbVeGoOBcES%<8EwuZq=Q* zxH#XsSGy{aE#238+t;*qYG&Ooywwxg#O?hd?NW5!kM0}9i1N!T>8YmUDNzw;ZXQ~j zlXSJ_NW|*($Ge)M@Xa%}4SeC-^Q!pBcJ|P&1>axZbM?ZguYC>Q?L${(ZCSrAQL(|< zwjDD4`pcD@RWs%HN|wF<$?1#DIa^P^QPXB`+;Gmi^X=X^{BW1BKY7Da&0jO+Hzs|0 zvI&o0Zm(>at!tdIZR?!fz2j;t6Sl0dHQri#WYmmp*Nf{c3cmBCBWBQh;hk4YkJ%bm znzmA^_;g*u+?M40I?J?*7AR(EW>ReWw9!M)->o*lVXe&S(xXl4kyaP+vX;Y95OKB%1+^3%ZQbOmzP(bvsT|8 zR$G(sl^x8Aq&3{QS@QK0W~Ys*PyVdBJ-c=PooMrzo@2JMrb5)8`q9hR3uorHqCIkW UNxXrPJ|bkkfxuuUs7$GKQ7bQcYx*glu< z|Nrm5ob#WZ7(X=7_iP_UQ3Lt0+$8x9Mo&+IykA%gA^G&WW5<1p+OaEox~TIPc2iX1 zj8&WprV6iW2DX#X#5zjWZI_@aYX5NEg~lujfR4(RlcpPAT%mzwrs-o+fh)KpsA7#R zdg$olc+ps#HB^%xJ^=RDHKM>q0R(ls>iAkcO*i#wWE?HCG-yJC*)*Mv41%e`1Q@{{ z0&0?H42~1QkeXCwm4nLAJ|J*{#Bw6b$qcV*yr7Z&_Kzm!JhP-t=3Z$VBeOJJ2?AGR z*;=iZtcgkNm04a@RhAQ2L0|}i@fVx`))~j&)6tMazTsJJU||PD8ljHof;3I2wtTSN zSgqr?<3u9H)}hPtNiOoK2{eru=gxW6rg76?Q5D(934DU(W2{@j0ro5S0jlHeO$JD8 z3x(LmmRxK*HsJ@O^TdsIK(<8t#RV6!lgP(&o`FW^3C%r`H?B6~AsAq^){VCa(D}Z4f{?7@#yAWsTv;UoP?~Ev0CjLZ%wWwV(xTTBU`JP=yh=BFQc< zYeH%hl+=s~1Nca=X=o+v*^oGF*|3aQ*D2E=4x=`LtJouk2|KaLqEOKCjvqkBK>1vn zCO#)E%hb%0Dl5F8G6sYkqm)t#151X)5EiPc42H$agBlS-CajGW7mHH&q+hH{-&?zHwSF-ef zaOSZp@!xsqIJ4<2{eRA|(GA(EF!rCt*WBB1o03E}m&g?z4ddoOCUJW~4vCXTn#aE7 z)jvq%S~vFa X@BL)W?fy3E##DZEJa_Tn^jm)e1(<;- delta 199 zcmZ3*|C(`vL_7;K0|Ud`yN`l^ltF+`h-(9o0R#;U4Gka)NCOEVIB?*=f1tpBhW}s+ zNCR0wu$_N(I#8UkB*-tA!Qt7BG!Q4r+uensgH_gJqM~>#1AB?5uPgggMkZzrxg82` ztbi&^JY5_^IIbrrI5ZR~XfPPQ0-AK6FZqjjgQVAgR@Q|*|9dSY{&!AjGSaeYU^pVc d!n{M8;ikIDtQ809M1lGlJYD@<);T3K0RT;XM85z4 diff --git a/qtfred/resources/resources.qrc b/qtfred/resources/resources.qrc index c7a35e37666..632b40f9a9c 100644 --- a/qtfred/resources/resources.qrc +++ b/qtfred/resources/resources.qrc @@ -1,5 +1,7 @@ + images/arrow_down.png + images/arrow_up.png images/next.bmp images/play.bmp images/prev.bmp @@ -9,6 +11,7 @@ images/bmp00001.png images/chained.png images/chained_directive.png + images/comment.png images/constx.png images/constxy.png images/constxz.png diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index a25c2303b29..b737258ca29 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -58,6 +58,8 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/LoadoutEditorDialogModel.h src/mission/dialogs/MissionCutscenesDialogModel.cpp src/mission/dialogs/MissionCutscenesDialogModel.h + src/mission/dialogs/MissionEventsDialogModel.cpp + src/mission/dialogs/MissionEventsDialogModel.h src/mission/dialogs/MissionGoalsDialogModel.cpp src/mission/dialogs/MissionGoalsDialogModel.h src/mission/dialogs/MissionSpecDialogModel.cpp @@ -142,8 +144,6 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/CampaignEditorDialog.cpp src/ui/dialogs/CommandBriefingDialog.cpp src/ui/dialogs/CommandBriefingDialog.h - src/ui/dialogs/EventEditorDialog.cpp - src/ui/dialogs/EventEditorDialog.h src/ui/dialogs/FictionViewerDialog.cpp src/ui/dialogs/FictionViewerDialog.h src/ui/dialogs/FormWingDialog.cpp @@ -156,6 +156,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/LoadoutDialog.h src/ui/dialogs/MissionCutscenesDialog.cpp src/ui/dialogs/MissionCutscenesDialog.h + src/ui/dialogs/MissionEventsDialog.cpp + src/ui/dialogs/MissionEventsDialog.h src/ui/dialogs/MissionGoalsDialog.cpp src/ui/dialogs/MissionGoalsDialog.h src/ui/dialogs/MissionSpecDialog.cpp @@ -267,7 +269,6 @@ add_file_folder("UI" ui/CustomDataDialog.ui ui/CustomStringsDialog.ui ui/CustomWingNamesDialog.ui - ui/EventEditorDialog.ui ui/FictionViewerDialog.ui ui/FormWingDialog.ui ui/FredView.ui @@ -275,6 +276,7 @@ add_file_folder("UI" ui/JumpNodeEditorDialog.ui ui/LoadoutDialog.ui ui/MissionCutscenesDialog.ui + ui/MissionEventsDialog.ui ui/MissionGoalsDialog.ui ui/MissionSpecDialog.ui ui/MusicPlayerDialog.ui diff --git a/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp new file mode 100644 index 00000000000..3b22f04af11 --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp @@ -0,0 +1,1498 @@ +#include "MissionEventsDialogModel.h" + +#include +#include + +namespace fso::fred::dialogs { + +MissionEventsDialogModel::MissionEventsDialogModel(QObject* parent, fso::fred::EditorViewport* viewport, IEventTreeOps& tree_ops) + : AbstractDialogModel(parent, viewport), m_event_tree_ops(tree_ops) +{ + initializeData(); +} + +bool MissionEventsDialogModel::apply() +{ + SCP_vector> names; + + audiostream_close_file(m_wave_id, false); + m_wave_id = -1; + + for (auto& event : Mission_events) { + free_sexp2(event.formula); + event.result = 0; // use this as a processed flag + } + + // rename all sexp references to old events + for (int i = 0; i < static_cast(m_events.size()); i++) { + if (m_sig[i] >= 0) { + names.emplace_back(Mission_events[m_sig[i]].name, m_events[i].name); + Mission_events[m_sig[i]].result = 1; + } + } + + // invalidate all sexp references to deleted events. + for (const auto& event : Mission_events) { + if (!event.result) { + SCP_string buf = "<" + event.name + ">"; + + // force it to not be too long + if (SCP_truncate(buf, NAME_LENGTH - 1)) + buf.back() = '>'; + + names.emplace_back(event.name, buf); + } + } + + // copy all dialog events to the mission + Mission_events.clear(); + for (const auto& dialog_event : m_events) { + Mission_events.push_back(dialog_event); + Mission_events.back().formula = m_event_tree_ops.save_tree(dialog_event.formula); + } + + // now update all sexp references + for (const auto& name_pair : names) + update_sexp_references(name_pair.first.c_str(), name_pair.second.c_str(), OPF_EVENT_NAME); + + for (int i = Num_builtin_messages; i < Num_messages; i++) { + if (Messages[i].avi_info.name) + free(Messages[i].avi_info.name); + + if (Messages[i].wave_info.name) + free(Messages[i].wave_info.name); + } + + Num_messages = static_cast(m_messages.size()) + Num_builtin_messages; + Messages.resize(Num_messages); + for (int i = 0; i < static_cast(m_messages.size()); i++) + Messages[i + Num_builtin_messages] = m_messages[i]; + + applyAnnotations(); + + // Only fire the signal after the changes have been applied to make sure the other parts of the code see the updated + // state + if (query_modified()) { + _editor->missionChanged(); + } + return true; +} + +void MissionEventsDialogModel::reject() +{ + // Nothing to do here +} + +void MissionEventsDialogModel::initializeData() +{ + initializeMessages(); + initializeHeadAniList(); + initializeWaveList(); + initializePersonaList(); + + initializeTeamList(); + initializeEvents(); +} + +void MissionEventsDialogModel::initializeEvents() +{ + m_events.clear(); + m_sig.clear(); + m_cur_event = -1; + for (auto i = 0; i < static_cast(Mission_events.size()); i++) { + m_events.push_back(Mission_events[i]); + m_sig.push_back(i); + + if (m_events[i].name.empty()) { + m_events[i].name = ""; + } + + m_events[i].formula = m_event_tree_ops.load_sub_tree(Mission_events[i].formula, false, "do-nothing"); + + // we must check for the case of the repeat count being 0. This would happen if the repeat + // count is not specified in a mission + if (m_events[i].repeat_count <= 0) { + m_events[i].repeat_count = 1; + } + } + + m_event_tree_ops.post_load(); + + m_event_tree_ops.clear(); + for (auto& event : m_events) { + // set the proper bitmap + NodeImage image; + if (event.chain_delay >= 0) { + image = NodeImage::CHAIN; + if (!event.objective_text.empty()) { + image = NodeImage::CHAIN_DIRECTIVE; + } + } else { + image = NodeImage::ROOT; + if (!event.objective_text.empty()) { + image = NodeImage::ROOT_DIRECTIVE; + } + } + + m_event_tree_ops.add_sub_tree(event.name, image, event.formula); + } + + initializeEventAnnotations(); +} + +int MissionEventsDialogModel::findFormulaByOriginalEventIndex(int orig) const +{ + for (int cur = 0; cur < static_cast(m_sig.size()); ++cur) + if (m_sig[cur] == orig) + return m_events[cur].formula; + return -1; +} + +void MissionEventsDialogModel::initializeEventAnnotations() +{ + m_event_annotations = Event_annotations; // copy + + for (auto& ea : m_event_annotations) { + ea.handle = nullptr; + if (ea.path.empty()) + continue; + + const int origIdx = ea.path.front(); + const int formula = findFormulaByOriginalEventIndex(origIdx); + if (formula < 0) + continue; + + IEventTreeOps::Handle h = m_event_tree_ops.get_root_by_formula(formula); + if (!h) + continue; + + // walk children + auto it = ea.path.begin(); + ++it; // skip event index + for (; it != ea.path.end() && h; ++it) { + const int child = *it; + if (child < 0 || child >= m_event_tree_ops.child_count(h)) { + h = nullptr; + break; + } + h = m_event_tree_ops.child_at(h, child); + } + + ea.handle = h; + if (h) { + const bool hasColor = (ea.r != 255) || (ea.g != 255) || (ea.b != 255); + m_event_tree_ops.set_node_note(h, ea.comment); + m_event_tree_ops.set_node_bg_color(h, ea.r, ea.g, ea.b, hasColor); + } + } +} + +// Build the path for a handle (root formula -> orig index; then child indices) +SCP_list MissionEventsDialogModel::buildPathForHandle(IEventTreeOps::Handle h) const +{ + SCP_list path; + if (!h) + return path; + + const int rootFormula = m_event_tree_ops.root_formula_of(h); + if (rootFormula < 0) + return path; + + // Find the *current* index of this root in m_events + int curIdx = -1; + for (int i = 0; i < static_cast(m_events.size()); ++i) { + if (m_events[i].formula == rootFormula) { + curIdx = i; + break; + } + } + if (curIdx < 0) + return path; + + // persist the current index + path.push_back(curIdx); + + // Collect child indices from node up to root, then reverse + std::vector rev; + IEventTreeOps::Handle cur = h; + for (;;) { + IEventTreeOps::Handle parent = m_event_tree_ops.parent_of(cur); + if (!parent) + break; + rev.push_back(m_event_tree_ops.index_in_parent(cur)); + cur = parent; + } + for (auto it = rev.rbegin(); it != rev.rend(); ++it) + path.push_back(*it); + + return path; +} + +bool MissionEventsDialogModel::isDefaultAnnotation(const event_annotation& ea) +{ + const bool noNote = ea.comment.empty(); + const bool noColor = (ea.r == 255 && ea.g == 255 && ea.b == 255); + return noNote && noColor; +} + +IEventTreeOps::Handle MissionEventsDialogModel::resolveHandleFromPath(const SCP_list& path) const +{ + if (path.empty()) + return nullptr; + const int origEvt = path.front(); + const int formula = findFormulaByOriginalEventIndex(origEvt); + if (formula < 0) + return nullptr; + + auto h = m_event_tree_ops.get_root_by_formula(formula); + auto it = path.begin(); + ++it; // skip event index + for (; it != path.end() && h; ++it) { + const int childIdx = *it; + if (childIdx < 0 || childIdx >= m_event_tree_ops.child_count(h)) + return nullptr; + h = m_event_tree_ops.child_at(h, childIdx); + } + return h; +} + +event_annotation& MissionEventsDialogModel::ensureAnnotationByPath(const SCP_list& path) +{ + for (auto& ea : m_event_annotations) + if (ea.path == path) + return ea; + event_annotation ea{}; + ea.path = path; + m_event_annotations.push_back(ea); + return m_event_annotations.back(); +} + +void MissionEventsDialogModel::initializeTeamList() +{ + m_team_list.clear(); + m_team_list.emplace_back("", -1); + for (auto& team : Mission_event_teams_tvt) { + m_team_list.emplace_back(team.first, team.second); + } +} + +mission_event MissionEventsDialogModel::makeDefaultEvent() +{ + mission_event e{}; + e.name = "Event name"; + e.formula = -1; + // Seems like most initializers are handled by the sexp_tree widget... This is so messy + + return e; +} + +void MissionEventsDialogModel::applyAnnotations() +{ + // Recompute paths from whatever we currently have + for (auto& ea : m_event_annotations) { + // Prefer live handle if still valid + if (ea.handle && m_event_tree_ops.is_handle_valid(ea.handle)) { + ea.path = buildPathForHandle(ea.handle); + } else { + // If we lost the handle, try to resolve from the old path + auto h = resolveHandleFromPath(ea.path); + if (h) { + ea.handle = h; // refresh cache for the rest of the session + ea.path = buildPathForHandle(h); // normalize + } else { + // Node is gone; mark default so we prune + ea.comment.clear(); + ea.r = ea.g = ea.b = 255; + ea.handle = nullptr; + ea.path.clear(); + } + } + // Drop the handle + ea.handle = nullptr; + } + + // Prune defaults + m_event_annotations.erase(std::remove_if(m_event_annotations.begin(), m_event_annotations.end(), [](const event_annotation& ea) { return isDefaultAnnotation(ea); }), m_event_annotations.end()); + + // Apply + Event_annotations = m_event_annotations; +} + + +void MissionEventsDialogModel::initializeMessages() +{ + int num_messages = Num_messages - Num_builtin_messages; + m_messages.clear(); + m_messages.reserve(num_messages); + for (auto i = 0; i < num_messages; i++) { + auto msg = Messages[i + Num_builtin_messages]; + m_messages.push_back(msg); + if (m_messages[i].avi_info.name) { + m_messages[i].avi_info.name = strdup(m_messages[i].avi_info.name); + } + if (m_messages[i].wave_info.name) { + m_messages[i].wave_info.name = strdup(m_messages[i].wave_info.name); + } + } + + if (Num_messages > Num_builtin_messages) { + setCurrentlySelectedMessage(0); + } else { + setCurrentlySelectedMessage(-1); + } +} + +void MissionEventsDialogModel::initializeHeadAniList() +{ + m_head_ani_list.clear(); + m_head_ani_list.emplace_back(""); + + if (!Disable_hc_message_ani) { + m_head_ani_list.emplace_back("Head-TP2"); + m_head_ani_list.emplace_back("Head-TP3"); + m_head_ani_list.emplace_back("Head-TP4"); + m_head_ani_list.emplace_back("Head-TP5"); + m_head_ani_list.emplace_back("Head-TP6"); + m_head_ani_list.emplace_back("Head-TP7"); + m_head_ani_list.emplace_back("Head-TP8"); + m_head_ani_list.emplace_back("Head-VP1"); + m_head_ani_list.emplace_back("Head-VP2"); + m_head_ani_list.emplace_back("Head-CM1"); + m_head_ani_list.emplace_back("Head-CM2"); + m_head_ani_list.emplace_back("Head-CM3"); + m_head_ani_list.emplace_back("Head-CM4"); + m_head_ani_list.emplace_back("Head-CM5"); + m_head_ani_list.emplace_back("Head-VC"); + m_head_ani_list.emplace_back("Head-VC2"); + m_head_ani_list.emplace_back("Head-BSH"); + } + + for (auto& thisHead : Custom_head_anis) { + m_head_ani_list.emplace_back(thisHead); + } + + for (auto& msg : m_messages) { + if (msg.avi_info.name) { + auto it = std::find(m_head_ani_list.begin(), m_head_ani_list.end(), msg.avi_info.name); + if (it == m_head_ani_list.end()) { + m_head_ani_list.emplace_back(msg.avi_info.name); + } + } + } +} + +void MissionEventsDialogModel::initializeWaveList() +{ + m_wave_list.clear(); + m_wave_list.emplace_back(""); + + // Use the main Message vector so we also get the builtins? + for (auto i = 0; i < Num_messages; i++) { + if (Messages[i].wave_info.name) { + auto it = std::find(m_wave_list.begin(), m_wave_list.end(), Messages[i].wave_info.name); + if (it == m_wave_list.end()) { + m_wave_list.emplace_back(Messages[i].wave_info.name); + } + } + } +} + +void MissionEventsDialogModel::initializePersonaList() +{ + m_persona_list.clear(); + m_persona_list.emplace_back("", -1); + for (int i = 0; i < static_cast(Personas.size()); ++i) { + auto& persona = Personas[i]; + m_persona_list.emplace_back(persona.name, i); + } +} + +bool MissionEventsDialogModel::checkMessageNameConflict(const SCP_string& name) +{ + // Validate against builtin messages + for (auto i = 0; i < Num_builtin_messages; i++) { + if (!stricmp(name.c_str(), Messages[i].name)) { + _viewport->dialogProvider->showButtonDialog(DialogType::Warning, + "Invalid Message Name", + "Message name cannot be the same as a builtin message name!", + {DialogButton::Ok}); + return true; + break; + } + } + + // Validate against existing messages + for (auto i = 0; i < static_cast(m_messages.size()); i++) { + if ((i != m_cur_msg) && (!stricmp(name.c_str(), m_messages[i].name))) { + _viewport->dialogProvider->showButtonDialog(DialogType::Warning, + "Invalid Message Name", + "Message name cannot be the same another message!", + {DialogButton::Ok}); + return true; + break; + } + } + + return false; +} + +SCP_string MissionEventsDialogModel::makeUniqueMessageName(const SCP_string& base) const +{ + const int maxLen = NAME_LENGTH - 1; + + auto exists = [&](const SCP_string& cand) -> bool { + for (const auto& m : m_messages) { + if (m.name[0] != '\0' && stricmp(m.name, cand.c_str()) == 0) { + return true; + } + } + return false; + }; + + // Try base, then base + " 1", base + " 2", ... + for (int n = 0;; ++n) { + SCP_string suffix = (n == 0) ? "" : (" " + std::to_string(n)); + const size_t avail = (maxLen > static_cast(suffix.size())) + ? static_cast(maxLen - static_cast(suffix.size())) + : 0u; + SCP_string head = base.substr(0, avail); + SCP_string cand = head + suffix; + if (!exists(cand)) + return cand; + } +} + +bool MissionEventsDialogModel::eventIsValid() const +{ + return SCP_vector_inbounds(m_events, m_cur_event); +} + +bool MissionEventsDialogModel::messageIsValid() const +{ + return SCP_vector_inbounds(m_messages, m_cur_msg); +} + +void MissionEventsDialogModel::setCurrentlySelectedEvent(int event) +{ + m_cur_event = event; +} + +void MissionEventsDialogModel::setCurrentlySelectedEventByFormula(int formula) +{ + for (auto i = 0; i < static_cast(m_events.size()); i++) { + if (m_events[i].formula == formula) { + setCurrentlySelectedEvent(i); + return; + } + } +} + +int MissionEventsDialogModel::getCurrentlySelectedEvent() const +{ + return m_cur_event; +} + +SCP_vector& MissionEventsDialogModel::getEventList() +{ + return m_events; +} + +void MissionEventsDialogModel::deleteRootNode(int node) +{ + int i; + for (i = 0; i < static_cast(m_events.size()); i++) { + if (m_events[i].formula == node) { + break; + } + } + + Assertion(i < static_cast(m_events.size()), "Attempt to delete an invalid event!"); + m_events.erase(m_events.begin() + i); + m_sig.erase(m_sig.begin() + i); + + if (i >= static_cast(m_events.size())) // if we have deleted the last event, + i--; // i will be set to -1 which is what we want + + setCurrentlySelectedEvent(i); + set_modified(); +} + +void MissionEventsDialogModel::renameRootNode(int node, const SCP_string& name) +{ + int i; + for (i = 0; i < static_cast(m_events.size()); i++) { + if (m_events[i].formula == node) { + break; + } + } + Assertion(i < static_cast(m_events.size()), "Attempt to rename an invalid event!"); + m_events[i].name = name; + set_modified(); +} + +void MissionEventsDialogModel::changeRootNodeFormula(int old, int node) +{ + int i; + for (i = 0; i < static_cast(m_events.size()); i++) { + if (m_events[i].formula == old) { + break; + } + } + + Assertion(i < static_cast(m_events.size()), "Attempt to modify invalid event!"); + m_events[i].formula = node; + set_modified(); +} + +void MissionEventsDialogModel::reorderByRootFormulaOrder(const SCP_vector& newOrderedFormulas) +{ + // Basic sanity: must be a 1:1 permutation of current roots + if (newOrderedFormulas.size() != m_events.size()) + return; + + // Build the permuted arrays (O(n^2) is fine for event counts) + SCP_vector newEvents; + SCP_vector newSig; + newEvents.reserve(m_events.size()); + newSig.reserve(m_sig.size()); + + for (int formula : newOrderedFormulas) { + int oldIdx = -1; + for (int i = 0; i < static_cast(m_events.size()); ++i) { + if (m_events[i].formula == formula) { + oldIdx = i; + break; + } + } + if (oldIdx < 0) { + // Unknown formula; bail without mutating state + return; + } + newEvents.push_back(m_events[oldIdx]); + if (SCP_vector_inbounds(m_sig, oldIdx)) { + newSig.push_back(m_sig[oldIdx]); + } + } + + // Swap in the new order + m_events.swap(newEvents); + m_sig.swap(newSig); + + // Keep selection reasonable (select the first event after reorder) + setCurrentlySelectedEvent(m_events.empty() ? -1 : 0); + + // Rebuild applied annotations against new handles/order if needed + initializeEventAnnotations(); + + set_modified(); +} + +void MissionEventsDialogModel::setCurrentlySelectedMessage(int msg) +{ + m_cur_msg = msg; + + audiostream_close_file(m_wave_id, false); + m_wave_id = -1; +} + +int MissionEventsDialogModel::getCurrentlySelectedMessage() const +{ + return m_cur_msg; +} + +const SCP_vector& MissionEventsDialogModel::getHeadAniList() +{ + return m_head_ani_list; +} + +const SCP_vector& MissionEventsDialogModel::getWaveList() +{ + return m_wave_list; +} + +const SCP_vector>& MissionEventsDialogModel::getPersonaList() +{ + return m_persona_list; +} + +const SCP_vector>& MissionEventsDialogModel::getTeamList() +{ + return m_team_list; +} + +void MissionEventsDialogModel::createEvent() +{ + m_events.emplace_back(makeDefaultEvent()); + m_sig.push_back(-1); + + auto& event = m_events.back(); + + const int after = event.formula; + event.formula = m_event_tree_ops.build_default_root(event.name, after); + m_event_tree_ops.select_root(event.formula); + + setCurrentlySelectedEventByFormula(event.formula); + set_modified(); +} + +void MissionEventsDialogModel::insertEvent() +{ + if (m_cur_event < 0 || m_events.empty()) { + createEvent(); + return; + } + + const int pos = m_cur_event; // Can shift during tree ops so save our position now + m_events.insert(m_events.begin() + pos, makeDefaultEvent()); + m_sig.insert(m_sig.begin() + pos, -1); + auto& event = m_events[pos]; + + // Place after the previous root if it exists and is valid; otherwise we’ll fix index explicitly + int after = (pos > 0 && m_events[pos - 1].formula >= 0) ? m_events[pos - 1].formula : -1; + + event.formula = m_event_tree_ops.build_default_root(event.name, after); + + if (pos == 0) { + m_event_tree_ops.ensure_top_level_index(event.formula, 0); + } + m_event_tree_ops.select_root(event.formula); + + setCurrentlySelectedEventByFormula(event.formula); + set_modified(); +} + +void MissionEventsDialogModel::deleteEvent() +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + + m_event_tree_ops.delete_event(); +} + +void MissionEventsDialogModel::renameEvent(int id, const SCP_string& name) +{ + // Find by formula id first; fallback to treating id as an index if you want. + int idx = -1; + for (int i = 0; i < static_cast(m_events.size()); ++i) { + if (m_events[i].formula == id) { + idx = i; + break; + } + } + if (idx == -1 && id >= 0 && id < static_cast(m_events.size())) + idx = id; + if (idx < 0) + return; + + // Normalize to engine expectations + SCP_string normalized = name.empty() ? SCP_string("") : name; + SCP_truncate(normalized, NAME_LENGTH - 1); + + modify(m_events[idx].name, normalized); +} + +int MissionEventsDialogModel::getFormula() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return -1; + } + return m_events[m_cur_event].formula; +} + +void MissionEventsDialogModel::setFormula(int node) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + + auto& event = m_events[m_cur_event]; + modify(event.formula, node); +} + +int MissionEventsDialogModel::getRepeatCount() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return 1; + } + return m_events[m_cur_event].repeat_count; +} + +void MissionEventsDialogModel::setRepeatCount(int count) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (count < -1) { + count = -1; + } + modify(event.repeat_count, count); +} + +int MissionEventsDialogModel::getTriggerCount() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return 0; + } + return m_events[m_cur_event].trigger_count; +} + +void MissionEventsDialogModel::setTriggerCount(int count) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (count < -1) { + count = -1; + } + modify(event.trigger_count, count); +} + +int MissionEventsDialogModel::getIntervalTime() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return 0; + } + return m_events[m_cur_event].interval; +} + +void MissionEventsDialogModel::setIntervalTime(int time) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (time < 0) { + time = 0; + } + modify(event.interval, time); +} + +bool MissionEventsDialogModel::getChained() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].chain_delay >= 0); +} + +void MissionEventsDialogModel::setChained(bool chained) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (chained) { + modify(event.chain_delay, 0); + } else { + modify(event.chain_delay, -1); + } +} + +int MissionEventsDialogModel::getChainDelay() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return -1; + } + return m_events[m_cur_event].chain_delay; +} + +void MissionEventsDialogModel::setChainDelay(int delay) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (delay < 0) { + delay = 0; + } + modify(event.chain_delay, delay); +} + +int MissionEventsDialogModel::getEventScore() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return 0; + } + return m_events[m_cur_event].score; +} + +void MissionEventsDialogModel::setEventScore(int score) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + modify(event.score, score); +} + +int MissionEventsDialogModel::getEventTeam() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return -1; + } + return m_events[m_cur_event].team; +} + +void MissionEventsDialogModel::setEventTeam(int team) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + + auto& event = m_events[m_cur_event]; + + if (team < -1 || team >= MAX_TVT_TEAMS) { + team = -1; + } + modify(event.team, team); +} + +SCP_string MissionEventsDialogModel::getEventDirectiveText() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return ""; + } + return m_events[m_cur_event].objective_text; +} + +void MissionEventsDialogModel::setEventDirectiveText(const SCP_string& text) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + modify(event.objective_text, text); +} + +SCP_string MissionEventsDialogModel::getEventDirectiveKeyText() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return ""; + } + return m_events[m_cur_event].objective_key_text; +} + +void MissionEventsDialogModel::setEventDirectiveKeyText(const SCP_string& text) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + modify(event.objective_key_text, text); +} + +bool MissionEventsDialogModel::getLogTrue() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_SEXP_TRUE) != 0; +} + +void MissionEventsDialogModel::setLogTrue(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_SEXP_TRUE; + } else { + event.mission_log_flags &= ~MLF_SEXP_TRUE; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogFalse() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_SEXP_FALSE) != 0; +} + +void MissionEventsDialogModel::setLogFalse(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_SEXP_FALSE; + } else { + event.mission_log_flags &= ~MLF_SEXP_FALSE; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogLogPrevious() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_STATE_CHANGE) != 0; +} + +void MissionEventsDialogModel::setLogLogPrevious(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_STATE_CHANGE; + } else { + event.mission_log_flags &= ~MLF_STATE_CHANGE; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogAlwaysFalse() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_SEXP_KNOWN_FALSE) != 0; +} + +void MissionEventsDialogModel::setLogAlwaysFalse(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_SEXP_KNOWN_FALSE; + } else { + event.mission_log_flags &= ~MLF_SEXP_KNOWN_FALSE; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogFirstRepeat() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_FIRST_REPEAT_ONLY) != 0; +} + +void MissionEventsDialogModel::setLogFirstRepeat(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_FIRST_REPEAT_ONLY; + } else { + event.mission_log_flags &= ~MLF_FIRST_REPEAT_ONLY; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogLastRepeat() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_LAST_REPEAT_ONLY) != 0; +} + +void MissionEventsDialogModel::setLogLastRepeat(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_LAST_REPEAT_ONLY; + } else { + event.mission_log_flags &= ~MLF_LAST_REPEAT_ONLY; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogFirstTrigger() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_FIRST_TRIGGER_ONLY) != 0; +} + +void MissionEventsDialogModel::setLogFirstTrigger(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_FIRST_TRIGGER_ONLY; + } else { + event.mission_log_flags &= ~MLF_FIRST_TRIGGER_ONLY; + } + set_modified(); +} + +bool MissionEventsDialogModel::getLogLastTrigger() const +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return false; + } + return (m_events[m_cur_event].mission_log_flags & MLF_LAST_TRIGGER_ONLY) != 0; +} + +void MissionEventsDialogModel::setLogLastTrigger(bool log) +{ + if (!SCP_vector_inbounds(m_events, m_cur_event)) { + return; + } + auto& event = m_events[m_cur_event]; + if (log) { + event.mission_log_flags |= MLF_LAST_TRIGGER_ONLY; + } else { + event.mission_log_flags &= ~MLF_LAST_TRIGGER_ONLY; + } + set_modified(); +} + +void MissionEventsDialogModel::setNodeAnnotation(IEventTreeOps::Handle h, const SCP_string& note) +{ + auto path = buildPathForHandle(h); + auto& ea = ensureAnnotationByPath(path); + ea.handle = h; + ea.comment = note; + m_event_tree_ops.set_node_note(h, note); + set_modified(); +} + +void MissionEventsDialogModel::setNodeBgColor(IEventTreeOps::Handle h, int r, int g, int b, bool has_color) +{ + auto path = buildPathForHandle(h); + auto& ea = ensureAnnotationByPath(path); + ea.handle = h; + if (has_color) { + ea.r = (ubyte)r; + ea.g = (ubyte)g; + ea.b = (ubyte)b; + } else { + ea.r = ea.g = ea.b = 255; + } + m_event_tree_ops.set_node_bg_color(h, r, g, b, has_color); + set_modified(); +} + +void MissionEventsDialogModel::createMessage() +{ + MMessage msg; + + const SCP_string base = ""; + const SCP_string unique = makeUniqueMessageName(base); + + strcpy_s(msg.name, unique.c_str()); + strcpy_s(msg.message, ""); + msg.avi_info.name = nullptr; + msg.wave_info.name = nullptr; + msg.persona_index = -1; + msg.multi_team = -1; + m_messages.push_back(msg); + auto id = static_cast(m_messages.size()) - 1; + + setCurrentlySelectedMessage(id); + + set_modified(); +} + +void MissionEventsDialogModel::insertMessage() +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + createMessage(); + return; + } + + MMessage msg; + const SCP_string base = ""; + const SCP_string unique = makeUniqueMessageName(base); + + strcpy_s(msg.name, unique.c_str()); + strcpy_s(msg.message, ""); + msg.avi_info.name = nullptr; + msg.wave_info.name = nullptr; + msg.persona_index = -1; + msg.multi_team = -1; + + // Insert at requested position + m_messages.insert(m_messages.begin() + m_cur_msg, msg); + + // Select the new message + setCurrentlySelectedMessage(m_cur_msg); + + set_modified(); +} + +void MissionEventsDialogModel::deleteMessage() +{ + // handle this case somewhat gracefully + Assertion(SCP_vector_inbounds(m_messages, m_cur_msg), + "Unexpected m_cur_msg value (%d); expected either -1, or between 0-%d. Get a coder!\n", + m_cur_msg, + static_cast(m_messages.size()) - 1); + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + audiostream_close_file(m_wave_id, false); + m_wave_id = -1; + + if (m_messages[m_cur_msg].avi_info.name) { + free(m_messages[m_cur_msg].avi_info.name); + m_messages[m_cur_msg].avi_info.name = nullptr; + } + if (m_messages[m_cur_msg].wave_info.name) { + free(m_messages[m_cur_msg].wave_info.name); + m_messages[m_cur_msg].wave_info.name = nullptr; + } + + SCP_string buf = "<" + SCP_string(m_messages[m_cur_msg].name) + ">"; + update_sexp_references(m_messages[m_cur_msg].name, buf.c_str(), OPF_MESSAGE); + update_sexp_references(m_messages[m_cur_msg].name, buf.c_str(), OPF_MESSAGE_OR_STRING); + + m_messages.erase(m_messages.begin() + m_cur_msg); + + if (m_cur_msg >= static_cast(m_messages.size())) { + m_cur_msg = static_cast(m_messages.size()) - 1; + } + + setCurrentlySelectedMessage(m_cur_msg); + + set_modified(); +} + +void MissionEventsDialogModel::moveMessageUp() +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg) || m_cur_msg == 0) + return; + + std::swap(m_messages[m_cur_msg - 1], m_messages[m_cur_msg]); + setCurrentlySelectedMessage(m_cur_msg - 1); + set_modified(); +} + +void MissionEventsDialogModel::moveMessageDown() +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg) || m_cur_msg >= static_cast(m_messages.size()) - 1) + return; + + std::swap(m_messages[m_cur_msg + 1], m_messages[m_cur_msg]); + setCurrentlySelectedMessage(m_cur_msg + 1); + set_modified(); +} + + +SCP_string MissionEventsDialogModel::getMessageName() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return ""; + } + return m_messages[m_cur_msg].name; +} + +void MissionEventsDialogModel::setMessageName(const SCP_string& name) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + + if (!checkMessageNameConflict(name)) { + strncpy(msg.name, name.c_str(), NAME_LENGTH - 1); + set_modified(); + } +} + +SCP_string MissionEventsDialogModel::getMessageText() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return ""; + } + return m_messages[m_cur_msg].message; +} + +void MissionEventsDialogModel::setMessageText(const SCP_string& text) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + + strncpy(msg.message, text.c_str(), MESSAGE_LENGTH - 1); + lcl_fred_replace_stuff(msg.message, MESSAGE_LENGTH - 1); + + set_modified(); +} + +SCP_string MissionEventsDialogModel::getMessageNote() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return ""; + } + return m_messages[m_cur_msg].note; +} + +void MissionEventsDialogModel::setMessageNote(const SCP_string& note) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + + modify(msg.note, note); +} + +SCP_string MissionEventsDialogModel::getMessageAni() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return ""; + } + auto& msg = m_messages[m_cur_msg]; + return msg.avi_info.name ? SCP_string(msg.avi_info.name) : SCP_string(""); +} + +void MissionEventsDialogModel::setMessageAni(const SCP_string& ani) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + const char* cur = msg.avi_info.name; + const SCP_string curStr = cur ? cur : ""; + + // Treat empty, "none", "" as no avi and store nullptr + const bool isNone = ani.empty() || lcase_equal(ani, "") || lcase_equal(ani, "none"); + if (isNone) { + if (cur != nullptr) { // only do work if changing something + free(msg.avi_info.name); + msg.avi_info.name = nullptr; + set_modified(); + } + return; + } + + // No change? bail + if (cur && curStr == ani) { + return; + } + + // Replace value + if (cur) + free(msg.avi_info.name); + msg.avi_info.name = strdup(ani.c_str()); + set_modified(); + + // Possibly add to list of known anis + auto it = std::find_if(m_head_ani_list.begin(), m_head_ani_list.end(), [&](const SCP_string& s) { + return lcase_equal(s, ani); + }); + if (it == m_head_ani_list.end()) { + m_head_ani_list.emplace_back(ani); + } +} + +SCP_string MissionEventsDialogModel::getMessageWave() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return ""; + } + auto& msg = m_messages[m_cur_msg]; + return msg.wave_info.name ? SCP_string(msg.wave_info.name) : SCP_string(""); +} + +void MissionEventsDialogModel::setMessageWave(const SCP_string& wave) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + audiostream_close_file(m_wave_id, false); + m_wave_id = -1; + + auto& msg = m_messages[m_cur_msg]; + const char* cur = msg.wave_info.name; + const SCP_string curStr = cur ? cur : ""; + + // Treat empty, "none", "" as no avi and store nullptr + const bool isNone = wave.empty() || lcase_equal(wave, "") || lcase_equal(wave, "none"); + if (isNone) { + if (cur != nullptr) { // only do work if changing something + free(msg.wave_info.name); + msg.wave_info.name = nullptr; + set_modified(); + } + return; + } + + // No change? bail + if (cur && curStr == wave) { + return; + } + + // Replace value + if (cur) + free(msg.wave_info.name); + msg.wave_info.name = strdup(wave.c_str()); + set_modified(); + + autoSelectPersona(); +} + +int MissionEventsDialogModel::getMessagePersona() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return -1; + } + auto& msg = m_messages[m_cur_msg]; + if (SCP_vector_inbounds(Personas, msg.persona_index)) { + return msg.persona_index; + } + return -1; +} + +void MissionEventsDialogModel::setMessagePersona(int persona) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + + msg.persona_index = persona; + set_modified(); +} + +int MissionEventsDialogModel::getMessageTeam() const +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return -1; + } + auto& msg = m_messages[m_cur_msg]; + if (msg.multi_team < 0 || msg.multi_team >= MAX_TVT_TEAMS) { + return -1; + } + return msg.multi_team; +} + +void MissionEventsDialogModel::setMessageTeam(int team) +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + auto& msg = m_messages[m_cur_msg]; + + if (team >= MAX_TVT_TEAMS) { + msg.multi_team = -1; + } else { + msg.multi_team = team; + } + set_modified(); +} + +void MissionEventsDialogModel::autoSelectPersona() +{ + // I hate everything about this function outside of retail but someone will complain + // if I omit this "feature"... + + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + SCP_string wave_name = m_messages[m_cur_msg].wave_info.name ? m_messages[m_cur_msg].wave_info.name : ""; + SCP_string avi_name = m_messages[m_cur_msg].avi_info.name ? m_messages[m_cur_msg].avi_info.name : ""; + + if ((wave_name[0] >= '1') && (wave_name[0] <= '9') && (wave_name[1] == '_')) { + auto i = wave_name[0] - '1'; + if ((i < static_cast(Personas.size())) && (Personas[i].flags & PERSONA_FLAG_WINGMAN)) { + modify(m_messages[m_cur_msg].persona_index, i); + if (i == 0 || i == 1) { + avi_name = "HEAD-TP1"; + } else if (i == 2 || i == 3) { + avi_name = "HEAD-TP2"; + } else if (i == 4) { + avi_name = "HEAD-TP3"; + } else if (i == 5) { + avi_name = "HEAD-VP1"; + } + } + } else { + auto mask = 0; + if (!strnicmp(wave_name.c_str(), "S_", 2)) { + mask = PERSONA_FLAG_SUPPORT; + avi_name = "HEAD-CM1"; + } else if (!strnicmp(wave_name.c_str(), "L_", 2)) { + mask = PERSONA_FLAG_LARGE; + avi_name = "HEAD-CM1"; + } else if (!strnicmp(wave_name.c_str(), "TC_", 3)) { + mask = PERSONA_FLAG_COMMAND; + avi_name = "HEAD-CM1"; + } + + for (auto i = 0; i < (static_cast(Personas.size())); i++) { + if (Personas[i].flags & mask) { + modify(m_messages[m_cur_msg].persona_index, i); + } + } + } + + SCP_string original_avi_name = avi_name; + if (m_messages[m_cur_msg].avi_info.name) { + free(m_messages[m_cur_msg].avi_info.name); + m_messages[m_cur_msg].avi_info.name = nullptr; + } + m_messages[m_cur_msg].avi_info.name = strdup(avi_name.c_str()); + + if (original_avi_name != avi_name) { + set_modified(); + } +} + +void MissionEventsDialogModel::playMessageWave() +{ + if (!SCP_vector_inbounds(m_messages, m_cur_msg)) { + return; + } + + //audiostream_close_file(m_wave_id, false); + + auto& msg = m_messages[m_cur_msg]; + + if (msg.wave_info.name) { + m_wave_id = audiostream_open(msg.wave_info.name, ASF_VOICE); + if (m_wave_id >= 0) { + audiostream_play(m_wave_id, 1.0f, 0); + } + } +} + +const SCP_vector& MissionEventsDialogModel::getMessageList() const +{ + return m_messages; +} + +bool MissionEventsDialogModel::getMissionIsMultiTeam() +{ + return The_mission.game_type & MISSION_TYPE_MULTI_TEAMS; +} + +void MissionEventsDialogModel::setModified() { + set_modified(); +} + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/MissionEventsDialogModel.h b/qtfred/src/mission/dialogs/MissionEventsDialogModel.h new file mode 100644 index 00000000000..ca010dc077f --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionEventsDialogModel.h @@ -0,0 +1,206 @@ +#pragma once + +#include "AbstractDialogModel.h" + +#include "ui/widgets/sexp_tree.h" + +#include +#include +#include + +namespace fso::fred::dialogs { + + struct IEventTreeOps { + using Handle = void*; + + virtual ~IEventTreeOps() = default; + + // Called after the tree is loaded, to allow for any post-load operations. + virtual int load_sub_tree(int formula, bool allow_empty = false, const char* default_body = "do-nothing") = 0; + + // deselects all nodes + virtual void post_load() = 0; + + // adds the tree and sets the image + virtual void add_sub_tree(const SCP_string& name, NodeImage image, int formula) = 0; + + // Insert a new top-level root with the given name and the default body: + // when -> true -> do-nothing + // If after_root >= 0, place visually after that root; otherwise append. + // Returns the new root formula id (stored on the root item). + virtual int build_default_root(const SCP_string& name, int after_root) = 0; + + // Serialize root back into compact SEXP form and return root id. + virtual int save_tree(int root_formula) = 0; + + // Used for the "insert at index 0" special case. + virtual void ensure_top_level_index(int root_formula, int desired_index) = 0; + + // Optional: select/highlight the root in the UI. + virtual void select_root(int root_formula) = 0; + + // Clear the tree + virtual void clear() = 0; + + // Delete the selected event + virtual void delete_event() = 0; + + // Navigation + virtual Handle parent_of(Handle node) = 0; // nullptr if root + virtual int index_in_parent(Handle node) = 0; // 0..N-1, or -1 if no parent + virtual int root_formula_of(Handle node) = 0; + + // Discovery + virtual bool is_handle_valid(Handle node) = 0; + virtual Handle get_root_by_formula(int formula) = 0; + virtual int child_count(Handle node) = 0; + virtual Handle child_at(Handle node, int idx) = 0; + + // Annotations + virtual void set_node_note(Handle node, const SCP_string& note) = 0; + virtual void set_node_bg_color(Handle node, int r, int g, int b, bool has_color) = 0; + }; + +class MissionEventsDialogModel : public AbstractDialogModel { + public: + MissionEventsDialogModel(QObject* parent, EditorViewport* viewport, IEventTreeOps& tree_ops); + + bool apply() override; + void reject() override; + + bool eventIsValid() const; + bool messageIsValid() const; + + void setCurrentlySelectedEvent(int event); + void setCurrentlySelectedEventByFormula(int formula); + int getCurrentlySelectedEvent() const; + SCP_vector& getEventList(); + void deleteRootNode(int node); + void renameRootNode(int node, const SCP_string& name); + void changeRootNodeFormula(int old, int node); + void reorderByRootFormulaOrder(const SCP_vector& newOrderedFormulas); + + void setCurrentlySelectedMessage(int msg); + int getCurrentlySelectedMessage() const; + const SCP_vector& getHeadAniList(); + const SCP_vector& getWaveList(); + const SCP_vector>& getPersonaList(); + const SCP_vector>& getTeamList(); + + // Event Management + void createEvent(); + void insertEvent(); + void deleteEvent(); + void renameEvent(int id, const SCP_string& name); + int getFormula() const; + void setFormula(int node); + int getRepeatCount() const; + void setRepeatCount(int count); + int getTriggerCount() const; + void setTriggerCount(int count); + int getIntervalTime() const; + void setIntervalTime(int time); + bool getChained() const; + void setChained(bool chained); + int getChainDelay() const; + void setChainDelay(int delay); + int getEventScore() const; + void setEventScore(int score); + int getEventTeam() const; + void setEventTeam(int team); + SCP_string getEventDirectiveText() const; + void setEventDirectiveText(const SCP_string& text); + SCP_string getEventDirectiveKeyText() const; + void setEventDirectiveKeyText(const SCP_string& text); + + // Event Logging + bool getLogTrue() const; + void setLogTrue(bool log); + bool getLogFalse() const; + void setLogFalse(bool log); + bool getLogLogPrevious() const; + void setLogLogPrevious(bool log); + bool getLogAlwaysFalse() const; + void setLogAlwaysFalse(bool log); + bool getLogFirstRepeat() const; + void setLogFirstRepeat(bool log); + bool getLogLastRepeat() const; + void setLogLastRepeat(bool log); + bool getLogFirstTrigger() const; + void setLogFirstTrigger(bool log); + bool getLogLastTrigger() const; + void setLogLastTrigger(bool log); + + // Event Annotations + void setNodeAnnotation(IEventTreeOps::Handle h, const SCP_string& note); + void setNodeBgColor(IEventTreeOps::Handle h, int r, int g, int b, bool has_color); + + // Message Management + void createMessage(); + void insertMessage(); + void deleteMessage(); + void moveMessageUp(); + void moveMessageDown(); + SCP_string getMessageName() const; + void setMessageName(const SCP_string& name); + SCP_string getMessageText() const; + void setMessageText(const SCP_string& text); + SCP_string getMessageNote() const; + void setMessageNote(const SCP_string& note); + SCP_string getMessageAni() const; + void setMessageAni(const SCP_string& ani); + SCP_string getMessageWave() const; + void setMessageWave(const SCP_string& wave); + int getMessagePersona() const; + void setMessagePersona(int persona); + int getMessageTeam() const; + void setMessageTeam(int team); + + void autoSelectPersona(); + void playMessageWave(); + const SCP_vector& getMessageList() const; + static bool getMissionIsMultiTeam(); + + void setModified(); + + private: + void initializeData(); + + void initializeEvents(); + int findFormulaByOriginalEventIndex(int orig) const; + void initializeEventAnnotations(); + SCP_list buildPathForHandle(IEventTreeOps::Handle h) const; + static bool isDefaultAnnotation(const event_annotation& ea); + IEventTreeOps::Handle resolveHandleFromPath(const SCP_list& path) const; + event_annotation& ensureAnnotationByPath(const SCP_list& path); + void initializeTeamList(); + static mission_event makeDefaultEvent(); + + void applyAnnotations(); + + void initializeMessages(); + void initializeHeadAniList(); + void initializeWaveList(); + void initializePersonaList(); + + bool checkMessageNameConflict(const SCP_string& name); + SCP_string makeUniqueMessageName(const SCP_string& name) const; + + IEventTreeOps& m_event_tree_ops; + + SCP_vector m_events; + SCP_vector m_event_annotations; + SCP_vector m_sig; + int m_cur_event = -1; + + SCP_vector m_messages; + int m_cur_msg = -1; + int m_wave_id = -1; + + SCP_vector m_head_ani_list; + SCP_vector m_wave_list; + SCP_vector> m_persona_list; + SCP_vector> m_team_list; +}; + +} // namespace fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 0e316bc4a77..280446bfc2d 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -13,7 +13,7 @@ #include #include #include -#include +#include #include #include #include @@ -711,7 +711,7 @@ void FredView::keyReleaseEvent(QKeyEvent* event) { _inKeyReleaseHandler = false; } void FredView::on_actionMission_Events_triggered(bool) { - auto eventEditor = new dialogs::EventEditorDialog(this, _viewport); + auto eventEditor = new dialogs::MissionEventsDialog(this, _viewport); eventEditor->setAttribute(Qt::WA_DeleteOnClose); eventEditor->show(); } diff --git a/qtfred/src/ui/dialogs/EventEditorDialog.cpp b/qtfred/src/ui/dialogs/EventEditorDialog.cpp deleted file mode 100644 index b0900640bce..00000000000 --- a/qtfred/src/ui/dialogs/EventEditorDialog.cpp +++ /dev/null @@ -1,1129 +0,0 @@ -// -// - -#include "EventEditorDialog.h" -#include "ui_EventEditorDialog.h" -#include "ui/util/SignalBlockers.h" - -#include -#include - -#include -#include -#include -#include -#include - -namespace fso { -namespace fred { -namespace dialogs { - -namespace { -void maybe_add_head(QComboBox* box, const char* name) { - auto id = box->findText(QString::fromUtf8(name)); - if (id < 0) { - box->addItem(name); - } -} -int safe_stricmp(const char* one, const char* two) { - if (!one && !two) { - return 0; - } - - if (!one) { - return -1; - } - - if (!two) { - return 1; - } - - return stricmp(one, two); -} - -} - -EventEditorDialog::EventEditorDialog(QWidget* parent, EditorViewport* viewport) : - QDialog(parent), - SexpTreeEditorInterface({ TreeFlags::LabeledRoot, TreeFlags::RootDeletable, TreeFlags::RootEditable }), - ui(new Ui::EventEditorDialog()), - _editor(viewport->editor) { - ui->setupUi(this); - - ui->eventTree->initializeEditor(viewport->editor, this); - - connect(this, &QDialog::accepted, this, &EventEditorDialog::applyChanges); - connect(this, &QDialog::rejected, this, &EventEditorDialog::rejectChanges); - - initMessageWidgets(); - - initEventWidgets(); -} -void EventEditorDialog::initEventWidgets() { - initEventTree(); - - ui->miniHelpBox->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); - ui->helpBox->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); - ui->triggerCountBox->setMinimum(-1); - ui->repeatCountBox->setMinimum(-1); - - connect(ui->eventTree, &sexp_tree::modified, this, [this]() { modified = true; }); - connect(ui->eventTree, &sexp_tree::rootNodeDeleted, this, &EventEditorDialog::rootNodeDeleted); - connect(ui->eventTree, &sexp_tree::rootNodeRenamed, this, &EventEditorDialog::rootNodeRenamed); - connect(ui->eventTree, &sexp_tree::rootNodeFormulaChanged, this, &EventEditorDialog::rootNodeFormulaChanged); - connect(ui->eventTree, - &sexp_tree::miniHelpChanged, - this, - [this](const QString& help) { ui->miniHelpBox->setText(help); }); - connect(ui->eventTree, - &sexp_tree::helpChanged, - this, - [this](const QString& help) { ui->helpBox->setPlainText(help); }); - connect(ui->eventTree, &sexp_tree::selectedRootChanged, this, [this](int formula) { - for (auto i = 0; i < (int)m_events.size(); i++) { - if (m_events[i].formula == formula) { - set_current_event(i); - return; - } - } - set_current_event(-1); - }); - connect(ui->repeatCountBox, QOverload::of(&QSpinBox::valueChanged), this, [this](int value) { - if (cur_event < 0) { - return; - } - m_events[cur_event].repeat_count = value; - }); - connect(ui->triggerCountBox, QOverload::of(&QSpinBox::valueChanged), this, [this](int value) { - if (cur_event < 0) { - return; - } - m_events[cur_event].trigger_count = value; - }); - connect(ui->intervalTimeBox, QOverload::of(&QSpinBox::valueChanged), this, [this](int value) { - if (cur_event < 0) { - return; - } - m_events[cur_event].interval = value; - }); - connect(ui->scoreBox, QOverload::of(&QSpinBox::valueChanged), this, [this](int value) { - if (cur_event < 0) { - return; - } - m_events[cur_event].score = value; - }); - connect(ui->chainedCheckBox, &QCheckBox::stateChanged, this, [this](int value) { - if (cur_event < 0) { - return; - } - - if (value != Qt::Checked) { - m_events[cur_event].chain_delay = -1; - } else { - m_events[cur_event].chain_delay = ui->chainDelayBox->value(); - } - - updateEventBitmap(); - }); - connect(ui->editDirectiveText, &QLineEdit::textChanged, this, [this](const QString& value) { - if (cur_event < 0) { - return; - } - m_events[cur_event].objective_text = value.toUtf8().constData(); - lcl_fred_replace_stuff(m_events[cur_event].objective_text); - - updateEventBitmap(); - }); - connect(ui->editDirectiveKeypressText, &QLineEdit::textChanged, this, [this](const QString& value) { - if (cur_event < 0) { - return; - } - m_events[cur_event].objective_key_text = value.toUtf8().constData(); - lcl_fred_replace_stuff(m_events[cur_event].objective_key_text); - }); - connectLogState(ui->checkLogTrue, MLF_SEXP_TRUE); - connectLogState(ui->checkLogFalse, MLF_SEXP_FALSE); - connectLogState(ui->checkLogAlwaysFalse, MLF_SEXP_KNOWN_FALSE); - connectLogState(ui->checkLogFirstRepeat, MLF_FIRST_REPEAT_ONLY); - connectLogState(ui->checkLogLastRepeat, MLF_LAST_REPEAT_ONLY); - connectLogState(ui->checkLogFirstTrigger, MLF_FIRST_TRIGGER_ONLY); - connectLogState(ui->checkLogLastTrigger, MLF_LAST_TRIGGER_ONLY); - connectLogState(ui->checkLogPrevious, MLF_STATE_CHANGE); - - connect(ui->btnNewEvent, &QPushButton::clicked, this, &EventEditorDialog::newEventHandler); - connect(ui->btnInsertEvent, &QPushButton::clicked, this, &EventEditorDialog::insertEventHandler); - connect(ui->btnDeleteEvent, &QPushButton::clicked, this, &EventEditorDialog::deleteEventHandler); - - set_current_event(-1); -} -void EventEditorDialog::initMessageWidgets() { - initMessageList(); - - initHeadCombo(); - - initWaveFilenames(); - - initPersonas(); - - ui->messageName->setMaxLength(NAME_LENGTH - 1); - - connect(ui->aniCombo, - QOverload::of(&QComboBox::currentTextChanged), - this, - [this](const QString& text) { - if (m_cur_msg < 0) { - return; - } - - if (m_messages[m_cur_msg].avi_info.name) { - free(m_messages[m_cur_msg].avi_info.name); - m_messages[m_cur_msg].avi_info.name = nullptr; - } - - auto ptr = text.toUtf8(); - if (ptr.isEmpty() || !VALID_FNAME(ptr)) { - m_messages[m_cur_msg].avi_info.name = NULL; - } else { - m_messages[m_cur_msg].avi_info.name = strdup(ptr); - } - }); - connect(ui->waveCombo, - QOverload::of(&QComboBox::currentTextChanged), - this, - [this](const QString& text) { - if (m_cur_msg < 0) { - return; - } - - if (m_messages[m_cur_msg].wave_info.name) { - free(m_messages[m_cur_msg].wave_info.name); - m_messages[m_cur_msg].wave_info.name = nullptr; - } - - auto ptr = text.toUtf8(); - if (ptr.isEmpty() || !VALID_FNAME(ptr)) { - m_messages[m_cur_msg].wave_info.name = NULL; - } else { - m_messages[m_cur_msg].wave_info.name = strdup(ptr); - } - updatePersona(); - set_current_message(m_cur_msg); - }); - connect(ui->messageName, QOverload::of(&QLineEdit::textChanged), this, [this](const QString& text) { - if (m_cur_msg < 0) { - return; - } - - auto conflict = false; - auto ptr = text.toUtf8(); - for (auto i = 0; i < Num_builtin_messages; i++) { - if (!stricmp(ptr, Messages[i].name)) { - conflict = true; - break; - } - } - - for (auto i = 0; i < (int)m_messages.size(); i++) { - if ((i != m_cur_msg) && (!stricmp(ptr, m_messages[i].name))) { - conflict = true; - break; - } - } - - if (!conflict) { // update name if no conflicts, otherwise keep old name - strncpy(m_messages[m_cur_msg].name, text.toUtf8().constData(), NAME_LENGTH - 1); - - auto item = ui->messageList->item(m_cur_msg); - item->setText(text); - } - }); - connect(ui->messageContent, &QPlainTextEdit::textChanged, this, [this]() { - if (m_cur_msg < 0) { - return; - } - - auto msg = ui->messageContent->toPlainText(); - - strncpy(m_messages[m_cur_msg].message, msg.toUtf8().constData(), MESSAGE_LENGTH - 1); - lcl_fred_replace_stuff(m_messages[m_cur_msg].message, MESSAGE_LENGTH - 1); - }); - connect(ui->messageTeamCombo, QOverload::of(&QComboBox::activated), this, [this](int id) { - if (m_cur_msg < 0) { - return; - } - - if (id >= MAX_TVT_TEAMS) { - m_messages[m_cur_msg].multi_team = -1; - } else { - m_messages[m_cur_msg].multi_team = id; - } - }); - connect(ui->personaCombo, QOverload::of(&QComboBox::activated), this, [this](int id) { - if (m_cur_msg < 0) { - return; - } - - // update the persona to the message. We subtract 1 for the "None" at the beginning of the combo - // box list. - m_messages[m_cur_msg].persona_index = id - 1; - }); - - connect(ui->messageList, &QListWidget::currentRowChanged, this, [this](int row) { set_current_message(row); }); - connect(ui->messageList, &QListWidget::itemDoubleClicked, this, &EventEditorDialog::messageDoubleClicked); - - connect(ui->btnNewMsg, &QPushButton::clicked, this, [this](bool) { createNewMessage(); }); - connect(ui->btnDeleteMsg, &QPushButton::clicked, this, [this](bool) { deleteMessage(); }); - - connect(ui->btnAniBrowse, &QPushButton::clicked, this, [this](bool) { browseAni(); }); - connect(ui->btnBrowseWave, &QPushButton::clicked, this, [this](bool) { browseWave(); }); - - connect(ui->btnWavePlay, &QPushButton::clicked, this, [this](bool){ playWave(); }); - - connect(ui->btnUpdateStuff, &QPushButton::clicked, this, [this](bool) { updateStuff(); }); -} -EventEditorDialog::~EventEditorDialog() = default; -void EventEditorDialog::initEventTree() { - load_tree(); - - create_tree(); - -} -void EventEditorDialog::load_tree() { - ui->eventTree->clear_tree(); - m_events.clear(); - m_sig.clear(); - for (auto i = 0; i < (int)Mission_events.size(); i++) { - m_events.push_back(Mission_events[i]); - m_sig.push_back(i); - - if (m_events[i].name.empty()) { - m_events[i].name = ""; - } - - m_events[i].formula = ui->eventTree->load_sub_tree(Mission_events[i].formula, false, "do-nothing"); - - // we must check for the case of the repeat count being 0. This would happen if the repeat - // count is not specified in a mission - if (m_events[i].repeat_count <= 0) { - m_events[i].repeat_count = 1; - } - } - - ui->eventTree->post_load(); - cur_event = -1; -} -void EventEditorDialog::create_tree() { - ui->eventTree->clear(); - for (auto i = 0; i < (int)m_events.size(); i++) { - // set the proper bitmap - NodeImage image; - if (m_events[i].chain_delay >= 0) { - image = NodeImage::CHAIN; - if (!m_events[i].objective_text.empty()) { - image = NodeImage::CHAIN_DIRECTIVE; - } - } else { - image = NodeImage::ROOT; - if (!m_events[i].objective_text.empty()) { - image = NodeImage::ROOT_DIRECTIVE; - } - } - - auto h = ui->eventTree->insert(m_events[i].name.c_str(), image); - h->setData(0, sexp_tree::FormulaDataRole, m_events[i].formula); - ui->eventTree->add_sub_tree(m_events[i].formula, h); - } - - cur_event = -1; -} -void EventEditorDialog::rootNodeDeleted(int node) { - int i; - for (i = 0; i < (int)m_events.size(); i++) { - if (m_events[i].formula == node) { - break; - } - } - - Assert(i < (int)m_events.size()); - m_events.erase(m_events.begin() + i); - m_sig.erase(m_sig.begin() + i); - - if (i >= (int)m_events.size()) // if we have deleted the last event, - i--; // i will be set to -1 which is what we want - - set_current_event(i); -} -void EventEditorDialog::rootNodeRenamed(int /*node*/) { -} -void EventEditorDialog::rootNodeFormulaChanged(int old, int node) { - int i; - - for (i = 0; i < (int)m_events.size(); i++) { - if (m_events[i].formula == old) { - break; - } - } - - Assert(i < (int)m_events.size()); - m_events[i].formula = node; -} -void EventEditorDialog::initMessageList() { - int num_messages = Num_messages - Num_builtin_messages; - m_messages.clear(); - m_messages.reserve(num_messages); - for (auto i = 0; i < num_messages; i++) { - auto msg = Messages[i + Num_builtin_messages]; - m_messages.push_back(msg); - if (m_messages[i].avi_info.name) { - m_messages[i].avi_info.name = strdup(m_messages[i].avi_info.name); - } - if (m_messages[i].wave_info.name) { - m_messages[i].wave_info.name = strdup(m_messages[i].wave_info.name); - } - } - - rebuildMessageList(); - - if (Num_messages > Num_builtin_messages) { - set_current_message(0); - } else { - set_current_message(-1); - } -} -void EventEditorDialog::rebuildMessageList() { - // Block signals so that the current item index isn't overwritten by this - QSignalBlocker blocker(ui->messageList); - - ui->messageList->clear(); - for (auto& msg : m_messages) { - auto item = new QListWidgetItem(msg.name, ui->messageList); - ui->messageList->addItem(item); - } -} -void EventEditorDialog::set_current_event(int evt) { - util::SignalBlockers blockers(this); - - cur_event = evt; - - if (cur_event < 0) { - ui->repeatCountBox->setValue(1); - ui->triggerCountBox->setValue(1); - ui->intervalTimeBox->setValue(1); - ui->chainDelayBox->setValue(0); - ui->teamCombo->setCurrentIndex(MAX_TVT_TEAMS); - ui->editDirectiveText->setText(""); - ui->editDirectiveKeypressText->setText(""); - - ui->repeatCountBox->setEnabled(false); - ui->triggerCountBox->setEnabled(false); - ui->intervalTimeBox->setEnabled(false); - ui->chainDelayBox->setEnabled(false); - ui->teamCombo->setEnabled(false); - ui->editDirectiveText->setEnabled(false); - ui->editDirectiveKeypressText->setEnabled(false); - return; - } - - if (m_events[cur_event].team < 0) { - ui->teamCombo->setCurrentIndex(MAX_TVT_TEAMS); - } else { - ui->teamCombo->setCurrentIndex(m_events[cur_event].team); - } - - ui->repeatCountBox->setValue(m_events[cur_event].repeat_count); - ui->triggerCountBox->setValue(m_events[cur_event].trigger_count); - ui->intervalTimeBox->setValue(m_events[cur_event].interval); - ui->scoreBox->setValue(m_events[cur_event].score); - if (m_events[cur_event].chain_delay >= 0) { - ui->chainedCheckBox->setChecked(true); - ui->chainDelayBox->setValue(m_events[cur_event].chain_delay); - ui->chainDelayBox->setEnabled(true); - } else { - ui->chainedCheckBox->setChecked(false); - ui->chainDelayBox->setValue(0); - ui->chainDelayBox->setEnabled(false); - } - - ui->editDirectiveText->setText(QString::fromUtf8(m_events[cur_event].objective_text.c_str())); - ui->editDirectiveKeypressText->setText(QString::fromUtf8(m_events[cur_event].objective_key_text.c_str())); - - ui->repeatCountBox->setEnabled(true); - ui->triggerCountBox->setEnabled(true); - - if ((m_events[cur_event].repeat_count > 1) || (m_events[cur_event].repeat_count < 0) || - (m_events[cur_event].trigger_count > 1) || (m_events[cur_event].trigger_count < 0)) { - ui->intervalTimeBox->setEnabled(true); - } else { - ui->intervalTimeBox->setValue(1); - ui->intervalTimeBox->setEnabled(false); - } - - ui->scoreBox->setEnabled(true); - ui->chainedCheckBox->setEnabled(true); - ui->editDirectiveText->setEnabled(true); - ui->editDirectiveKeypressText->setEnabled(true); - ui->teamCombo->setEnabled(false); - if ( The_mission.game_type & MISSION_TYPE_MULTI_TEAMS ){ - ui->teamCombo->setEnabled(true); - } - - // handle event log flags - ui->checkLogTrue->setChecked((m_events[cur_event].mission_log_flags & MLF_SEXP_TRUE) != 0); - ui->checkLogFalse->setChecked((m_events[cur_event].mission_log_flags & MLF_SEXP_FALSE) != 0); - ui->checkLogAlwaysFalse->setChecked((m_events[cur_event].mission_log_flags & MLF_SEXP_KNOWN_FALSE) != 0); - ui->checkLogFirstRepeat->setChecked((m_events[cur_event].mission_log_flags & MLF_FIRST_REPEAT_ONLY) != 0); - ui->checkLogLastRepeat->setChecked((m_events[cur_event].mission_log_flags & MLF_LAST_REPEAT_ONLY) != 0); - ui->checkLogFirstTrigger->setChecked((m_events[cur_event].mission_log_flags & MLF_FIRST_TRIGGER_ONLY) != 0); - ui->checkLogLastTrigger->setChecked((m_events[cur_event].mission_log_flags & MLF_LAST_TRIGGER_ONLY) != 0); - ui->checkLogPrevious->setChecked((m_events[cur_event].mission_log_flags & MLF_STATE_CHANGE) != 0); -} -void EventEditorDialog::initHeadCombo() { - auto box = ui->aniCombo; - box->clear(); - box->addItem(""); - for (auto i = 0; i < Num_messages; i++) { - if (Messages[i].avi_info.name) { - maybe_add_head(box, Messages[i].avi_info.name); - } - } - - if (!Disable_hc_message_ani) { - maybe_add_head(box, "Head-TP2"); - maybe_add_head(box, "Head-VC2"); - maybe_add_head(box, "Head-TP4"); - maybe_add_head(box, "Head-TP5"); - maybe_add_head(box, "Head-TP6"); - maybe_add_head(box, "Head-TP7"); - maybe_add_head(box, "Head-TP8"); - maybe_add_head(box, "Head-VP2"); - maybe_add_head(box, "Head-VP2"); - maybe_add_head(box, "Head-CM2"); - maybe_add_head(box, "Head-CM3"); - maybe_add_head(box, "Head-CM4"); - maybe_add_head(box, "Head-CM5"); - maybe_add_head(box, "Head-BSH"); - } -} -void EventEditorDialog::initWaveFilenames() { - auto box = ui->waveCombo; - box->clear(); - box->addItem(""); - for (auto i = 0; i < Num_messages; i++) { - if (Messages[i].wave_info.name) { - auto id = box->findText(Messages[i].wave_info.name); - if (id < 0) { - box->addItem(Messages[i].wave_info.name); - } - } - } -} -void EventEditorDialog::initPersonas() { - // add the persona names into the combo box - auto box = ui->personaCombo; - box->clear(); - box->addItem(""); - for (const auto &persona: Personas) { - box->addItem(persona.name); - } -} -void EventEditorDialog::set_current_message(int msg) { - ui->messageList->setCurrentItem(ui->messageList->item(msg)); - m_cur_msg = msg; - - auto enable = true; - - audiostream_close_file(m_wave_id, false); - m_wave_id = -1; - - if (m_cur_msg < 0) { - enable = false; - - ui->messageName->setText(""); - ui->messageContent->setPlainText(""); - ui->aniCombo->setEditText(""); - ui->personaCombo->setCurrentIndex(0); - ui->waveCombo->setEditText(""); - ui->teamCombo->setCurrentIndex(-1); - ui->messageTeamCombo->setCurrentIndex(-1); - } else { - auto& message = m_messages[m_cur_msg]; - - ui->messageName->setText(message.name); - ui->messageContent->setPlainText(message.message); - ui->aniCombo->setEditText(message.avi_info.name); - ui->personaCombo->setCurrentIndex( - message.persona_index + 1); // add one for the "none" at the beginning of the list - ui->waveCombo->setEditText(message.wave_info.name); - - // m_message_team == -1 maps to 2 - if (m_messages[m_cur_msg].multi_team == -1) { - ui->messageTeamCombo->setCurrentIndex(MAX_TVT_TEAMS); - } else { - ui->messageTeamCombo->setCurrentIndex(m_messages[m_cur_msg].multi_team); - } - } - - ui->messageName->setEnabled(enable); - ui->messageContent->setEnabled(enable); - ui->aniCombo->setEnabled(enable); - ui->btnAniBrowse->setEnabled(enable); - ui->btnBrowseWave->setEnabled(enable); - ui->waveCombo->setEnabled(enable); - ui->btnDeleteMsg->setEnabled(enable); - ui->personaCombo->setEnabled(enable); - ui->teamCombo->setEnabled(enable); -} -void EventEditorDialog::applyChanges() -{ - SCP_vector> names; - - audiostream_close_file(m_wave_id, 0); - m_wave_id = -1; - - auto changes_detected = query_modified(); - - for (auto &event: Mission_events) { - free_sexp2(event.formula); - event.result = 0; // use this as a processed flag - } - - // rename all sexp references to old events - for (int i = 0; i < (int)m_events.size(); i++) { - if (m_sig[i] >= 0) { - names.emplace_back(Mission_events[m_sig[i]].name, m_events[i].name); - Mission_events[m_sig[i]].result = 1; - } - } - - // invalidate all sexp references to deleted events. - for (const auto &event: Mission_events) { - if (!event.result) { - SCP_string buf = "<" + event.name + ">"; - - // force it to not be too long - if (SCP_truncate(buf, NAME_LENGTH - 1)) - buf.back() = '>'; - - names.emplace_back(event.name, buf); - } - } - - // copy all dialog events to the mission - Mission_events.clear(); - for (const auto &dialog_event: m_events) { - Mission_events.push_back(dialog_event); - Mission_events.back().formula = ui->eventTree->save_tree(dialog_event.formula); - } - - // now update all sexp references - for (const auto &name_pair: names) - update_sexp_references(name_pair.first.c_str(), name_pair.second.c_str(), OPF_EVENT_NAME); - - for (int i = Num_builtin_messages; i < Num_messages; i++) { - if (Messages[i].avi_info.name) - free(Messages[i].avi_info.name); - - if (Messages[i].wave_info.name) - free(Messages[i].wave_info.name); - } - - Num_messages = (int)m_messages.size() + Num_builtin_messages; - Messages.resize(Num_messages); - for (int i = 0; i < (int)m_messages.size(); i++) - Messages[i + Num_builtin_messages] = m_messages[i]; - - // Only fire the signal after the changes have been applied to make sure the other parts of the code see the updated - // state - if (changes_detected) { - _editor->missionChanged(); - } -} -void EventEditorDialog::closeEvent(QCloseEvent* event) { - audiostream_close_file(m_wave_id, false); - m_wave_id = -1; - - if (query_modified()) { - auto result = QMessageBox::question(this, - "Close", - "Do you want to keep your changes?", - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, - QMessageBox::Cancel); - - if (result == QMessageBox::Cancel) { - event->ignore(); - return; - } - - if (result == QMessageBox::Yes) { - applyChanges(); - event->accept(); - return; - } - } -} -void EventEditorDialog::rejectChanges() { - audiostream_close_file(m_wave_id, false); - m_wave_id = -1; - - // Nothing else to do here -} -bool EventEditorDialog::query_modified() { - if (modified) { - return true; - } - - if (Mission_events.size() != m_events.size()) { - return true; - } - - for (size_t i = 0; i < m_events.size(); ++i) { - if (!lcase_equal(m_events[i].name, Mission_events[i].name)) { - return true; - } - if (m_events[i].repeat_count != Mission_events[i].repeat_count) { - return true; - } - if (m_events[i].trigger_count != Mission_events[i].trigger_count) { - return true; - } - if (m_events[i].interval != Mission_events[i].interval) { - return true; - } - if (m_events[i].score != Mission_events[i].score) { - return true; - } - if (m_events[i].chain_delay != Mission_events[i].chain_delay) { - return true; - } - if (!lcase_equal(m_events[i].objective_text, Mission_events[i].objective_text)) { - return true; - } - if (!lcase_equal(m_events[i].objective_key_text, Mission_events[i].objective_key_text)) { - return true; - } - if (m_events[i].flags != Mission_events[i].flags) { - return true; - } - if (m_events[i].mission_log_flags != Mission_events[i].mission_log_flags) { - return true; - } - } - - if (static_cast(m_messages.size()) != Num_messages - Num_builtin_messages) { - return true; - } - - for (size_t i = 0; i < m_messages.size(); ++i) { - auto& local = m_messages[i]; - auto& ref = Messages[Num_builtin_messages + i]; - - if (stricmp(local.name, ref.name) != 0) { - return true; - } - if (stricmp(local.message, ref.message) != 0) { - return true; - } - if (!lcase_equal(local.note, ref.note)) { - return true; - } - if (local.persona_index != ref.persona_index) { - return true; - } - if (local.multi_team != ref.multi_team) { - return true; - } - if (safe_stricmp(local.avi_info.name, ref.avi_info.name) != 0) { - return true; - } - if (safe_stricmp(local.wave_info.name, ref.avi_info.name) != 0) { - return true; - } - } - - return false; -} -bool EventEditorDialog::hasDefaultMessageParamter() { - return !m_messages.empty(); -} -SCP_vector EventEditorDialog::getMessages() { - SCP_vector messages; - messages.reserve(m_messages.size()); - - for (const auto &msg: m_messages) { - messages.push_back(msg.name); - } - - return messages; -} -int EventEditorDialog::getRootReturnType() const { - return OPR_NULL; -} -void EventEditorDialog::messageDoubleClicked(QListWidgetItem* item) { - auto message_name = item->text(); - - int message_nodes[MAX_SEARCH_MESSAGE_DEPTH]; - auto num_messages = - ui->eventTree->find_text(message_name.toUtf8().constData(), message_nodes, MAX_SEARCH_MESSAGE_DEPTH); - - if (num_messages == 0) { - QString message = tr("No events using message '%1'").arg(message_name); - QMessageBox::information(this, "Error", message); - } else { - // find last message_node - if (m_last_message_node == -1) { - m_last_message_node = message_nodes[0]; - } else { - - if (num_messages == 1) { - // only 1 message - m_last_message_node = message_nodes[0]; - } else { - // find which message and go to next message - int found_pos = -1; - for (int i = 0; i < num_messages; i++) { - if (message_nodes[i] == m_last_message_node) { - found_pos = i; - break; - } - } - - if (found_pos == -1) { - // no previous message - m_last_message_node = message_nodes[0]; - } else if (found_pos == num_messages - 1) { - // cycle back to start - m_last_message_node = message_nodes[0]; - } else { - // go to next - m_last_message_node = message_nodes[found_pos + 1]; - } - } - } - - // highlight next - ui->eventTree->hilite_item(m_last_message_node); - } -} -void EventEditorDialog::createNewMessage() { - MMessage msg; - - strcpy_s(msg.name, ""); - - strcpy_s(msg.message, ""); - msg.avi_info.name = NULL; - msg.wave_info.name = NULL; - msg.persona_index = -1; - msg.multi_team = -1; - m_messages.push_back(msg); - auto id = (int)m_messages.size() - 1; - - modified = true; - - ui->messageList->addItem(QString::fromUtf8(msg.name)); - set_current_message(id); -} -void EventEditorDialog::deleteMessage() { - // handle this case somewhat gracefully - Assertion((m_cur_msg >= -1) && (m_cur_msg < (int)m_messages.size()), - "Unexpected m_cur_msg value (%d); expected either -1, or between 0-%d. Get a coder!\n", - m_cur_msg, - (int)m_messages.size() - 1); - if ((m_cur_msg < 0) || (m_cur_msg >= (int)m_messages.size())) { - return; - } - - if (m_messages[m_cur_msg].avi_info.name) { - free(m_messages[m_cur_msg].avi_info.name); - m_messages[m_cur_msg].avi_info.name = nullptr; - } - if (m_messages[m_cur_msg].wave_info.name) { - free(m_messages[m_cur_msg].wave_info.name); - m_messages[m_cur_msg].wave_info.name = nullptr; - } - - SCP_string buf; - sprintf(buf, "<%s>", m_messages[m_cur_msg].name); - update_sexp_references(m_messages[m_cur_msg].name, buf.c_str(), OPF_MESSAGE); - update_sexp_references(m_messages[m_cur_msg].name, buf.c_str(), OPF_MESSAGE_OR_STRING); - - m_messages.erase(m_messages.begin() + m_cur_msg); - - if (m_cur_msg >= (int)m_messages.size()) { - m_cur_msg = (int)m_messages.size() - 1; - } - - rebuildMessageList(); - set_current_message(m_cur_msg); - - ui->btnNewMsg->setEnabled(true); - modified = true; - - // The list loses focus when the current image is removed so we fix that here - ui->messageList->setFocus(); -} -void EventEditorDialog::browseAni() { - if (m_cur_msg < 0 || m_cur_msg >= (int)m_messages.size()) { - return; - } - - auto z = cfile_push_chdir(CF_TYPE_INTERFACE); - auto interface_path = QDir::currentPath(); - if (!z) { - cfile_pop_dir(); - } - - auto name = QFileDialog::getOpenFileName(this, - tr("Select message animation"), - interface_path, - "APNG Files (*.png);;Ani Files (*.ani);;Eff Files (*.eff);;" - "All Anims (*.ani, *.eff, *.png)"); - - if (name.isEmpty()) { - // Nothing was selected - return; - } - - QFileInfo info(name); - - if (m_messages[m_cur_msg].avi_info.name) { - free(m_messages[m_cur_msg].avi_info.name); - m_messages[m_cur_msg].avi_info.name = nullptr; - } - m_messages[m_cur_msg].avi_info.name = strdup(info.fileName().toUtf8().constData()); - set_current_message(m_cur_msg); - - modified = true; -} -void EventEditorDialog::browseWave() { - if (m_cur_msg < 0 || m_cur_msg >= (int)m_messages.size()) { - return; - } - - int z; - if (The_mission.game_type & MISSION_TYPE_TRAINING) { - z = cfile_push_chdir(CF_TYPE_VOICE_TRAINING); - } else { - z = cfile_push_chdir(CF_TYPE_VOICE_SPECIAL); - } - auto interface_path = QDir::currentPath(); - if (!z) { - cfile_pop_dir(); - } - - auto name = QFileDialog::getOpenFileName(this, - tr("Select message animation"), - interface_path, - "Voice Files (*.ogg, *.wav);;Ogg Vorbis Files (*.ogg);;" - "Wave Files (*.wav)"); - - if (name.isEmpty()) { - // Nothing was selected - return; - } - - QFileInfo info(name); - - if (m_messages[m_cur_msg].wave_info.name) { - free(m_messages[m_cur_msg].wave_info.name); - m_messages[m_cur_msg].wave_info.name = nullptr; - } - m_messages[m_cur_msg].wave_info.name = strdup(info.fileName().toUtf8().constData()); - updatePersona(); - - set_current_message(m_cur_msg); - - modified = true; -} -void EventEditorDialog::updatePersona() { - if (m_cur_msg < 0 || m_cur_msg >= (int)m_messages.size()) { - return; - } - - SCP_string wave_name = m_messages[m_cur_msg].wave_info.name; - SCP_string avi_name = m_messages[m_cur_msg].avi_info.name; - - if ((wave_name[0] >= '1') && (wave_name[0] <= '9') && (wave_name[1] == '_')) { - auto i = wave_name[0] - '1'; - if ((i < (int)Personas.size()) && (Personas[i].flags & PERSONA_FLAG_WINGMAN)) { - m_messages[m_cur_msg].persona_index = i; - if (i == 0 || i == 1) { - avi_name = "HEAD-TP1"; - } else if (i == 2 || i == 3) { - avi_name = "HEAD-TP2"; - } else if (i == 4) { - avi_name = "HEAD-TP3"; - } else if (i == 5) { - avi_name = "HEAD-VP1"; - } - } - } else { - auto mask = 0; - if (!strnicmp(wave_name.c_str(), "S_", 2)) { - mask = PERSONA_FLAG_SUPPORT; - avi_name = "HEAD-CM1"; - } else if (!strnicmp(wave_name.c_str(), "L_", 2)) { - mask = PERSONA_FLAG_LARGE; - avi_name = "HEAD-CM1"; - } else if (!strnicmp(wave_name.c_str(), "TC_", 3)) { - mask = PERSONA_FLAG_COMMAND; - avi_name = "HEAD-CM1"; - } - - for (auto i = 0; i < (int)Personas.size(); i++) { - if (Personas[i].flags & mask) { - m_messages[m_cur_msg].persona_index = i; - } - } - } - - if (m_messages[m_cur_msg].avi_info.name) { - free(m_messages[m_cur_msg].avi_info.name); - m_messages[m_cur_msg].avi_info.name = nullptr; - } - m_messages[m_cur_msg].avi_info.name = strdup(avi_name.c_str()); - - modified = true; -} -void EventEditorDialog::playWave() { - if (m_wave_id >= 0) { - audiostream_close_file(m_wave_id, false); - m_wave_id = -1; - return; - } - - // we use ASF_EVENTMUSIC here so that it will keep the extension in place - m_wave_id = audiostream_open(m_messages[m_cur_msg].wave_info.name, ASF_EVENTMUSIC); - - if (m_wave_id >= 0) { - audiostream_play(m_wave_id, 1.0f, 0); - } -} -void EventEditorDialog::updateStuff() { - updatePersona(); - set_current_message(m_cur_msg); -} -void EventEditorDialog::updateEventBitmap() { - auto chained = m_events[cur_event].chain_delay != -1; - auto hasObjectiveText = !m_events[cur_event].objective_text.empty(); - - NodeImage bitmap; - if (chained) { - if (!hasObjectiveText) { - bitmap = NodeImage::CHAIN; - } else { - bitmap = NodeImage::CHAIN_DIRECTIVE; - } - } else { - if (!hasObjectiveText) { - bitmap = NodeImage::ROOT; - } else { - bitmap = NodeImage::ROOT_DIRECTIVE; - } - } - for (int i = 0; i < ui->eventTree->topLevelItemCount(); ++i) { - auto item = ui->eventTree->topLevelItem(i); - - if (item->data(0, sexp_tree::FormulaDataRole).toInt() == m_events[cur_event].formula) { - item->setIcon(0, sexp_tree::convertNodeImageToIcon(bitmap)); - return; - } - } -} -void EventEditorDialog::connectLogState(QCheckBox* box, uint32_t flag) { - connect(box, &QCheckBox::stateChanged, this, [this, flag](int state) { - if (cur_event < 0) { - return; - } - - bool enable = state == Qt::Checked; - if (enable) { - m_events[cur_event].mission_log_flags |= flag; - } else { - m_events[cur_event].mission_log_flags &= ~flag; - } - }); -} -void EventEditorDialog::newEventHandler() { - m_events.emplace_back(); - m_sig.push_back(-1); - reset_event((int)m_events.size() - 1, nullptr); -} -void EventEditorDialog::insertEventHandler() { - if (cur_event < 0 || m_events.empty()) { - //There are no events yet, so just create one - newEventHandler(); - } else { - m_events.insert(m_events.begin() + cur_event, mission_event()); - m_sig.insert(m_sig.begin() + cur_event, -1); - - if (cur_event != 0) { - reset_event(cur_event, get_event_handle(cur_event - 1)); - } else { - reset_event(cur_event, nullptr); - - // Since there is no TVI_FIRST in Qt we need to do some additional work to get this to work right - auto new_item = get_event_handle(cur_event); - auto index = ui->eventTree->indexOfTopLevelItem(new_item); - ui->eventTree->takeTopLevelItem(index); - ui->eventTree->insertTopLevelItem(0, new_item); - } - } -} -void EventEditorDialog::deleteEventHandler() { - if (cur_event < 0) { - return; - } - - // This is such an ugly hack but I don't want to rewrite sexp_tree just for this.. - auto item = ui->eventTree->currentItem(); - while (item->parent() != nullptr) { - item = item->parent(); - } - ui->eventTree->setCurrentItem(item); - - ui->eventTree->deleteCurrentItem(); -} - -QTreeWidgetItem* EventEditorDialog::get_event_handle(int num) -{ - for (auto i = 0; i < ui->eventTree->topLevelItemCount(); ++i) { - auto item = ui->eventTree->topLevelItem(i); - - if (item->data(0, sexp_tree::FormulaDataRole).toInt() == m_events[num].formula) { - return item; - } - } - return nullptr; -} -void EventEditorDialog::reset_event(int num, QTreeWidgetItem* after) { - // this is always called for a freshly constructed event, so all we have to do is set the name - m_events[num].name = "Event name"; - auto h = ui->eventTree->insert(m_events[num].name.c_str(), NodeImage::ROOT, nullptr, after); - - ui->eventTree->setCurrentItemIndex(-1); - auto index = m_events[num].formula = ui->eventTree->add_operator("when", h); - h->setData(0, sexp_tree::FormulaDataRole, index); - ui->eventTree->add_operator("true"); - ui->eventTree->setCurrentItemIndex(index); - ui->eventTree->add_operator("do-nothing"); - - // First clear the current selection since the add_operator calls added new items and select them by default - ui->eventTree->clearSelection(); - // This will automatically call set_cur_event - h->setSelected(true); -} -void EventEditorDialog::keyPressEvent(QKeyEvent* event) { - if (event->key() == Qt::Key_Escape) { - // Instead of calling reject when we close a dialog it should try to close the window which will will allow the - // user to save unsaved changes - event->ignore(); - this->close(); - return; - } - QDialog::keyPressEvent(event); -} - -} -} -} - diff --git a/qtfred/src/ui/dialogs/EventEditorDialog.h b/qtfred/src/ui/dialogs/EventEditorDialog.h deleted file mode 100644 index 728864d6377..00000000000 --- a/qtfred/src/ui/dialogs/EventEditorDialog.h +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -#include -#include - -#include "ui/widgets/sexp_tree.h" - -#include -#include - -#include - -class QCheckBox; - -namespace fso { -namespace fred { -namespace dialogs { - -namespace Ui { -class EventEditorDialog; -} - -const int MAX_SEARCH_MESSAGE_DEPTH = 5; // maximum search number of event nodes with message text - -class EventEditorDialog: public QDialog, public SexpTreeEditorInterface { - std::unique_ptr ui; - - Editor* _editor = nullptr; - - SCP_vector m_sig; - SCP_vector m_events; - int cur_event = -1; - void set_current_event(int evt); - - SCP_vector m_messages; - int m_cur_msg = -1; - void set_current_message(int msg); - - int m_wave_id = -1; - - // Message data - int m_last_message_node = -1; - - void connectCheckBox(QCheckBox* box, bool* var); - - bool modified = false; - - void initEventTree(); - void load_tree(); - void create_tree(); - - void initMessageList(); - void initHeadCombo(); - void initWaveFilenames(); - void initPersonas(); - - void applyChanges(); - void rejectChanges(); - - void messageDoubleClicked(QListWidgetItem* item); - - void createNewMessage(); - void deleteMessage(); - - void browseAni(); - void browseWave(); - - void playWave(); - void updateStuff(); - - void updatePersona(); - - void rebuildMessageList(); - - void initMessageWidgets(); - - void initEventWidgets(); - - void updateEventBitmap(); - - void connectLogState(QCheckBox* box, uint32_t flag); - - void newEventHandler(); - void insertEventHandler(); - void deleteEventHandler(); - QTreeWidgetItem* get_event_handle(int num); - void reset_event(int num, QTreeWidgetItem* after); - - bool query_modified(); - protected: - void keyPressEvent(QKeyEvent* event) override; - Q_OBJECT - protected: - void closeEvent(QCloseEvent* event) override; - public: - EventEditorDialog(QWidget* parent, EditorViewport* viewport); - ~EventEditorDialog() override; - - void rootNodeDeleted(int node); - void rootNodeRenamed(int node); - void rootNodeFormulaChanged(int old, int node); - - bool hasDefaultMessageParamter() override; - SCP_vector getMessages() override; - int getRootReturnType() const override; -}; - -} -} -} - diff --git a/qtfred/src/ui/dialogs/MissionEventsDialog.cpp b/qtfred/src/ui/dialogs/MissionEventsDialog.cpp new file mode 100644 index 00000000000..a848991023c --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionEventsDialog.cpp @@ -0,0 +1,1025 @@ +#include "MissionEventsDialog.h" +#include "ui_MissionEventsDialog.h" +#include "ui/util/SignalBlockers.h" +#include "ui/dialogs/General/ImagePickerDialog.h" + +#include "mission/util.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace fso::fred::dialogs { + +MissionEventsDialog::MissionEventsDialog(QWidget* parent, EditorViewport* viewport) : + QDialog(parent), + SexpTreeEditorInterface({ TreeFlags::LabeledRoot, TreeFlags::RootDeletable, TreeFlags::RootEditable, TreeFlags::AnnotationsAllowed }), + ui(new Ui::MissionEventsDialog()), _viewport(viewport) +{ + ui->setupUi(this); + + // Build the Qt adapter for our data model + // This is kinda messy but the sexp_tree widget owns both the ui and the data for the tree + // Simultaneously our tree model needs to be able to tell the tree when things change and also + // be able to read data from the tree as needed. So we pass in this small adapter object with + // the relevant tree operations allowing the model to do all the cross talk it needs + struct QtTreeOps final : IEventTreeOps { + explicit QtTreeOps(sexp_tree& t) : tree(t) {} + sexp_tree& tree; + + int load_sub_tree(int formula, bool allow_empty = false, const char* default_body = "do-nothing") override + { + return tree.load_sub_tree(formula, allow_empty, default_body); + } + + void post_load() override + { + tree.post_load(); + } + + void add_sub_tree(const SCP_string& name, NodeImage image, int formula) override + { + auto h = tree.insert(name.c_str(), image); + h->setData(0, sexp_tree::FormulaDataRole, formula); + tree.add_sub_tree(formula, h); + } + + QTreeWidgetItem* findRootByFormula(int formula) + { + const int n = tree.topLevelItemCount(); + for (int i = 0; i < n; ++i) { + auto* it = tree.topLevelItem(i); + if (it && it->data(0, sexp_tree::FormulaDataRole).toInt() == formula) + return it; + } + return nullptr; + } + + int build_default_root(const SCP_string& name, int after_root) override + { + QTreeWidgetItem* afterItem = (after_root >= 0) ? findRootByFormula(after_root) : nullptr; + + auto* root = tree.insert(name.c_str(), NodeImage::ROOT, /*parent*/ nullptr, afterItem); + + // Build default body: when -> true -> do-nothing + tree.setCurrentItemIndex(-1); + int whenIdx = tree.add_operator("when", root); + root->setData(0, sexp_tree::FormulaDataRole, whenIdx); + tree.add_operator("true"); + tree.setCurrentItemIndex(whenIdx); + tree.add_operator("do-nothing"); + + tree.clearSelection(); + root->setSelected(true); + + return root->data(0, sexp_tree::FormulaDataRole).toInt(); + } + + int save_tree(int root_formula) override + { + return tree.save_tree(root_formula); + } + + void ensure_top_level_index(int root_formula, int desired_index) override + { + if (auto* item = findRootByFormula(root_formula)) { + int cur = tree.indexOfTopLevelItem(item); + if (cur != desired_index) { + tree.takeTopLevelItem(cur); + tree.insertTopLevelItem(desired_index, item); + } + } + } + + void select_root(int root_formula) override + { + if (auto* item = findRootByFormula(root_formula)) + tree.setCurrentItem(item); + } + + void clear() override + { + tree.clear(); + } + + void delete_event() override + { + // This is such an ugly hack but I don't want to rewrite sexp_tree just for this.. + auto item = tree.currentItem(); + while (item->parent() != nullptr) { + item = item->parent(); + } + tree.setCurrentItem(item); + + tree.deleteCurrentItem(); + } + + Handle parent_of(Handle node) override + { + auto* it = static_cast(node); + return static_cast(it ? it->parent() : nullptr); + } + + int index_in_parent(Handle node) override + { + auto* it = static_cast(node); + if (!it) + return -1; + auto* p = it->parent(); + return p ? p->indexOfChild(it) : -1; + } + + int root_formula_of(Handle node) override + { + auto* it = static_cast(node); + if (!it) + return -1; + while (it->parent()) + it = it->parent(); + return it->data(0, sexp_tree::FormulaDataRole).toInt(); + } + + bool is_handle_valid(Handle h) override + { + auto* it = static_cast(h); + return it && it->treeWidget() == &tree; + } + + Handle get_root_by_formula(int formula) override + { + return static_cast(findRootByFormula(formula)); + } + + int child_count(Handle node) override + { + auto* it = static_cast(node); + return it ? it->childCount() : 0; + } + + Handle child_at(Handle node, int idx) override + { + auto* it = static_cast(node); + if (!it || idx < 0 || idx >= it->childCount()) + return nullptr; + return static_cast(it->child(idx)); + } + + void set_node_note(Handle node, const SCP_string& note) override + { + if (auto* it = static_cast(node)) { + const QString q = QString::fromStdString(note); + it->setData(0, sexp_tree::NoteRole, q); + it->setToolTip(0, q); + sexp_tree::applyVisuals(it); + } + } + + void set_node_bg_color(Handle node, int r, int g, int b, bool has_color) override + { + if (auto* it = static_cast(node)) { + it->setData(0, sexp_tree::BgColorRole, QColor(r, g, b)); + it->setBackground(0, has_color ? QBrush(QColor(r, g, b)) : QBrush()); + sexp_tree::applyVisuals(it); + } + } + }; + + _treeOps = std::make_unique(QtTreeOps{*ui->eventTree}); + + ui->eventTree->initializeEditor(viewport->editor, this); + ui->eventTree->clear_tree(); + ui->eventTree->post_load(); + + // Now construct the model with reference to tree ops + _model = std::make_unique(this, _viewport, *_treeOps); + + initMessageWidgets(); + + initEventWidgets(); +} + +MissionEventsDialog::~MissionEventsDialog() = default; + +void MissionEventsDialog::initEventWidgets() { + initEventTeams(); + + ui->miniHelpBox->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + ui->helpBox->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); + + // connect the sexp tree stuff + connect(ui->eventTree, &sexp_tree::modified, this, [this]() { _model->setModified(); }); + connect(ui->eventTree, &sexp_tree::rootNodeDeleted, this, &MissionEventsDialog::rootNodeDeleted); + connect(ui->eventTree, &sexp_tree::rootNodeRenamed, this, &MissionEventsDialog::rootNodeRenamed); + connect(ui->eventTree, &sexp_tree::rootNodeFormulaChanged, this, &MissionEventsDialog::rootNodeFormulaChanged); + connect(ui->eventTree, &sexp_tree::miniHelpChanged, this, [this](const QString& help) { ui->miniHelpBox->setText(help); }); + connect(ui->eventTree, &sexp_tree::helpChanged, this, [this](const QString& help) { ui->helpBox->setPlainText(help); }); + connect(ui->eventTree, &sexp_tree::selectedRootChanged, this, [this](int formula) { MissionEventsDialog::rootNodeSelectedByFormula(formula); }); + + connect(ui->eventTree, &sexp_tree::nodeAnnotationChanged, this, [this](void* h, const QString& note) { + SCP_string text = note.toUtf8().constData(); + _model->setNodeAnnotation(h, text); + }); + + connect(ui->eventTree, &sexp_tree::nodeBgColorChanged, this, [this](void* h, const QColor& c) { + _model->setNodeBgColor(h, c.red(), c.green(), c.blue(), c.isValid()); + }); + + connect(ui->eventTree, &sexp_tree::rootOrderChanged, this, [this] { + SCP_vector order; + order.reserve(ui->eventTree->topLevelItemCount()); + for (int i = 0; i < ui->eventTree->topLevelItemCount(); ++i) { + auto* it = ui->eventTree->topLevelItem(i); + order.push_back(it->data(0, sexp_tree::FormulaDataRole).toInt()); + } + _model->reorderByRootFormulaOrder(order); + m_last_message_node = -1; + }); + + _model->setCurrentlySelectedEvent(-1); + + updateEventUi(); +} + +void MissionEventsDialog::accept() +{ + // If apply() returns true, close the dialog + if (_model->apply()) { + QDialog::accept(); + } + // else: validation failed, don’t close +} + +void MissionEventsDialog::reject() +{ + // Asks the user if they want to save changes, if any + // If they do, it runs _model->apply() and returns the success value + // If they don't, it runs _model->reject() and returns true + if (rejectOrCloseHandler(this, _model.get(), _viewport)) { + QDialog::reject(); // actually close + } + // else: do nothing, don't close +} + +SCP_vector MissionEventsDialog::getMessages() +{ + SCP_vector out; + const auto& msgs = _model->getMessageList(); + out.reserve(msgs.size()); + for (const auto& m : msgs) { + out.emplace_back(m.name); + } + return out; +} + +bool MissionEventsDialog::hasDefaultMessageParamter() +{ + return !_model->getMessageList().empty(); +} + +void MissionEventsDialog::closeEvent(QCloseEvent* e) +{ + reject(); + e->ignore(); // Don't let the base class close the window +} + +void MissionEventsDialog::initMessageWidgets() { + initHeadCombo(); + initWaveFilenames(); + initPersonas(); + initMessageTeams(); + + initMessageList(); + + ui->messageName->setMaxLength(NAME_LENGTH - 1); + + if (auto* le = ui->aniCombo->lineEdit()) { + connect(le, &QLineEdit::editingFinished, this, &MissionEventsDialog::on_aniCombo_editingFinished); + } + + if (auto* le = ui->waveCombo->lineEdit()) { + connect(le, &QLineEdit::editingFinished, this, &MissionEventsDialog::on_waveCombo_editingFinished); + } + + updateMessageUi(); +} + +void MissionEventsDialog::rootNodeDeleted(int node) { + _model->deleteRootNode(node); +} + +void MissionEventsDialog::rootNodeRenamed(int node) { + QTreeWidgetItem* item = nullptr; + for (int i = 0; i < ui->eventTree->topLevelItemCount(); ++i) { + auto* it = ui->eventTree->topLevelItem(i); + if (it && it->data(0, sexp_tree::FormulaDataRole).toInt() == node) { + item = it; + break; + } + } + if (!item) + return; + + SCP_string newText = item->text(0).toUtf8().constData(); + + _model->renameRootNode(node, newText); +} + +void MissionEventsDialog::rootNodeFormulaChanged(int old, int node) { + _model->changeRootNodeFormula(old, node); +} + +void MissionEventsDialog::rootNodeSelectedByFormula(int formula) { + _model->setCurrentlySelectedEventByFormula(formula); + updateEventUi(); +} + +void MissionEventsDialog::initMessageList() { + rebuildMessageList(); + + _model->setCurrentlySelectedMessage(_model->getMessageList().empty() ? -1 : 0); +} + +void MissionEventsDialog::rebuildMessageList() { + // Block signals so that the current item index isn't overwritten by this + QSignalBlocker blocker(ui->messageList); + + const int curRow = _model->getCurrentlySelectedMessage(); + + ui->messageList->clear(); + for (auto& msg : _model->getMessageList()) { + auto item = new QListWidgetItem(msg.name, ui->messageList); + ui->messageList->addItem(item); + } + + if (curRow >= 0 && curRow < ui->messageList->count()) { + ui->messageList->setCurrentRow(curRow); + } +} + +void MissionEventsDialog::updateEventUi() { + util::SignalBlockers blockers(this); + + updateEventMoveButtons(); + + if (!_model->eventIsValid()) { + ui->repeatCountBox->setValue(1); + ui->triggerCountBox->setValue(1); + ui->intervalTimeBox->setValue(1); + ui->chainDelayBox->setValue(0); + ui->teamCombo->setCurrentIndex(0); // was MAX_TVT_TEAMS for none? + ui->editDirectiveText->setText(""); + ui->editDirectiveKeypressText->setText(""); + + ui->repeatCountBox->setEnabled(false); + ui->triggerCountBox->setEnabled(false); + ui->intervalTimeBox->setEnabled(false); + ui->chainDelayBox->setEnabled(false); + ui->teamCombo->setEnabled(false); + ui->editDirectiveText->setEnabled(false); + ui->editDirectiveKeypressText->setEnabled(false); + return; + } + + ui->teamCombo->setCurrentIndex(ui->teamCombo->findData(_model->getEventTeam())); + + ui->repeatCountBox->setValue(_model->getRepeatCount()); + ui->triggerCountBox->setValue(_model->getTriggerCount()); + ui->intervalTimeBox->setValue(_model->getIntervalTime()); + ui->scoreBox->setValue(_model->getEventScore()); + if (_model->getChained()) { + ui->chainedCheckBox->setChecked(true); + ui->chainDelayBox->setValue(_model->getChainDelay()); + ui->chainDelayBox->setEnabled(true); + } else { + ui->chainedCheckBox->setChecked(false); + ui->chainDelayBox->setValue(0); + ui->chainDelayBox->setEnabled(false); + } + + ui->editDirectiveText->setText(QString::fromStdString(_model->getEventDirectiveText())); + ui->editDirectiveKeypressText->setText(QString::fromStdString(_model->getEventDirectiveKeyText())); + + ui->repeatCountBox->setEnabled(true); + ui->triggerCountBox->setEnabled(true); + + if ((_model->getRepeatCount() > 1) || (_model->getRepeatCount() < 0) || + (_model->getTriggerCount() > 1) || (_model->getTriggerCount() < 0)) { + ui->intervalTimeBox->setEnabled(true); + } else { + ui->intervalTimeBox->setValue(_model->getIntervalTime()); + ui->intervalTimeBox->setEnabled(false); + } + + ui->scoreBox->setEnabled(true); + ui->chainedCheckBox->setEnabled(true); + ui->editDirectiveText->setEnabled(true); + ui->editDirectiveKeypressText->setEnabled(true); + ui->teamCombo->setEnabled(_model->getMissionIsMultiTeam()); + + // handle event log flags + ui->checkLogTrue->setChecked(_model->getLogTrue()); + ui->checkLogFalse->setChecked(_model->getLogFalse()); + ui->checkLogPrevious->setChecked(_model->getLogLogPrevious()); + ui->checkLogAlwaysFalse->setChecked(_model->getLogAlwaysFalse()); + ui->checkLogFirstRepeat->setChecked(_model->getLogFirstRepeat()); + ui->checkLogLastRepeat->setChecked(_model->getLogLastRepeat()); + ui->checkLogFirstTrigger->setChecked(_model->getLogFirstTrigger()); + ui->checkLogLastTrigger->setChecked(_model->getLogLastTrigger()); +} + +void MissionEventsDialog::updateEventMoveButtons() +{ + auto* cur = ui->eventTree->currentItem(); + + const bool isRoot = (cur && !cur->parent()); + const int count = ui->eventTree->topLevelItemCount(); + + bool canUp = false, canDown = false; + + if (isRoot && count > 1) { + const int idx = ui->eventTree->indexOfTopLevelItem(cur); + canUp = (idx > 0); + canDown = (idx >= 0 && idx < count - 1); + } + + ui->eventUpBtn->setEnabled(canUp); + ui->eventDownBtn->setEnabled(canDown); +} + +void MissionEventsDialog::initHeadCombo() { + auto list = _model->getHeadAniList(); + + ui->aniCombo->clear(); + + for (auto& head : list) { + ui->aniCombo->addItem(QString().fromStdString(head)); + } +} + +void MissionEventsDialog::initWaveFilenames() { + auto list = _model->getWaveList(); + + ui->waveCombo->clear(); + + for (auto& wave : list) { + ui->waveCombo->addItem(QString().fromStdString(wave)); + } +} + +void MissionEventsDialog::initPersonas() { + auto list = _model->getPersonaList(); + + ui->personaCombo->clear(); + + for (auto&& [name, id] : _model->getPersonaList()) { + ui->personaCombo->addItem(QString::fromStdString(name), id); + } +} + +void MissionEventsDialog::initMessageTeams() { + auto list = _model->getTeamList(); + + ui->messageTeamCombo->clear(); + + for (const auto& team : list) { + ui->messageTeamCombo->addItem(QString::fromStdString(team.first), team.second); + } + +} + +void MissionEventsDialog::initEventTeams() +{ + auto list = _model->getTeamList(); + + ui->teamCombo->clear(); + + for (const auto& team : list) { + ui->teamCombo->addItem(QString::fromStdString(team.first), team.second); + } +} + +void MissionEventsDialog::updateMessageUi() +{ + bool enable = true; + + if (!_model->messageIsValid()) { + enable = false; + + ui->messageName->setText(""); + ui->messageContent->setPlainText(""); + ui->aniCombo->setEditText(""); + ui->personaCombo->setCurrentIndex(-1); + ui->waveCombo->setEditText(""); + ui->messageTeamCombo->setCurrentIndex(-1); + ui->btnMsgNote->setText("Add Node"); + } else { + ui->messageName->setText(QString().fromStdString(_model->getMessageName())); + ui->messageContent->setPlainText(QString().fromStdString(_model->getMessageText())); + ui->aniCombo->setEditText(QString().fromStdString(_model->getMessageAni())); + ui->personaCombo->setCurrentIndex(ui->personaCombo->findData(_model->getMessagePersona())); + ui->waveCombo->setEditText(QString().fromStdString(_model->getMessageWave())); + ui->messageTeamCombo->setCurrentIndex(ui->messageTeamCombo->findData(_model->getMessageTeam())); + if (_model->getMessageNote().empty()) { + ui->btnMsgNote->setText("Add Note"); + } else { + ui->btnMsgNote->setText("Edit Note"); + } + } + + ui->messageName->setEnabled(enable); + ui->messageContent->setEnabled(enable); + ui->aniCombo->setEnabled(enable); + ui->btnAniBrowse->setEnabled(enable); + ui->btnBrowseWave->setEnabled(enable); + ui->btnWavePlay->setEnabled(enable); + ui->waveCombo->setEnabled(enable); + ui->btnDeleteMsg->setEnabled(enable); + ui->personaCombo->setEnabled(enable); + ui->messageTeamCombo->setEnabled(enable && _model->getMissionIsMultiTeam()); + ui->btnMsgNote->setEnabled(enable); + + updateMessageMoveButtons(); +} + +void MissionEventsDialog::updateMessageMoveButtons() +{ + const int count = ui->messageList->count(); + const int row = ui->messageList->currentItem() ? ui->messageList->row(ui->messageList->currentItem()) : -1; + + const bool hasSel = (row >= 0); + const bool canUp = hasSel && row > 0; + const bool canDown = hasSel && row < count - 1; + + ui->msgUpBtn->setEnabled(canUp); + ui->msgDownBtn->setEnabled(canDown); +} + +SCP_vector MissionEventsDialog::read_root_formula_order(sexp_tree* tree) +{ + SCP_vector order; + order.reserve(tree->topLevelItemCount()); + for (int i = 0; i < tree->topLevelItemCount(); ++i) { + auto* it = tree->topLevelItem(i); + order.push_back(it->data(0, sexp_tree::FormulaDataRole).toInt()); + } + return order; +} + +void MissionEventsDialog::updateEventBitmap() { + auto chained = _model->getChained(); + auto hasObjectiveText = !_model->getEventDirectiveText().empty(); + + NodeImage bitmap; + if (chained) { + if (!hasObjectiveText) { + bitmap = NodeImage::CHAIN; + } else { + bitmap = NodeImage::CHAIN_DIRECTIVE; + } + } else { + if (!hasObjectiveText) { + bitmap = NodeImage::ROOT; + } else { + bitmap = NodeImage::ROOT_DIRECTIVE; + } + } + for (int i = 0; i < ui->eventTree->topLevelItemCount(); ++i) { + auto item = ui->eventTree->topLevelItem(i); + + if (item->data(0, sexp_tree::FormulaDataRole).toInt() == _model->getFormula()) { + item->setIcon(0, sexp_tree::convertNodeImageToIcon(bitmap)); + return; + } + } +} + +void MissionEventsDialog::on_okAndCancelButtons_accepted() +{ + accept(); +} + +void MissionEventsDialog::on_okAndCancelButtons_rejected() +{ + reject(); +} + +void MissionEventsDialog::on_btnNewEvent_clicked() +{ + _model->createEvent(); + + updateEventUi(); +} + +void MissionEventsDialog::on_btnInsertEvent_clicked() +{ + _model->insertEvent(); + + updateEventUi(); +} + +void MissionEventsDialog::on_btnDeleteEvent_clicked() +{ + _model->deleteEvent(); + + updateEventUi(); +} + +void MissionEventsDialog::on_eventUpBtn_clicked() +{ + auto* cur = ui->eventTree->currentItem(); + if (!cur || cur->parent()) + return; // roots only + const int idx = ui->eventTree->indexOfTopLevelItem(cur); + if (idx <= 0) + return; // already at top + + QTreeWidgetItem* dest = ui->eventTree->topLevelItem(idx - 1); + ui->eventTree->move_root(cur, dest, /*insert_before=*/true); // visual move + modified() + + // Keep model in sync with the new root order TODO remove/add this pending sexp_tree widget refactor + //_model->reorderByRootFormulaOrder(read_root_formula_order(ui->eventTree)); + + // Ensure it stays selected and visible + ui->eventTree->setCurrentItem(cur); + ui->eventTree->scrollToItem(cur); + updateEventMoveButtons(); +} + +void MissionEventsDialog::on_eventDownBtn_clicked() +{ + auto* cur = ui->eventTree->currentItem(); + if (!cur || cur->parent()) + return; // roots only + const int idx = ui->eventTree->indexOfTopLevelItem(cur); + const int last = ui->eventTree->topLevelItemCount() - 1; + if (idx < 0 || idx >= last) + return; // already at bottom + + QTreeWidgetItem* dest = ui->eventTree->topLevelItem(idx + 1); + ui->eventTree->move_root(cur, dest, /*insert_before=*/false); // visual move + modified() + + // Keep model in sync with the new root order TODO remove/add this pending sexp_tree widget refactor + //_model->reorderByRootFormulaOrder(read_root_formula_order(ui->eventTree)); + + ui->eventTree->setCurrentItem(cur); + ui->eventTree->scrollToItem(cur); + updateEventMoveButtons(); +} + +void MissionEventsDialog::on_repeatCountBox_valueChanged(int value) +{ + _model->setRepeatCount(value); + updateEventUi(); +} + +void MissionEventsDialog::on_triggerCountBox_valueChanged(int value) +{ + _model->setTriggerCount(value); + updateEventUi(); +} + +void MissionEventsDialog::on_intervalTimeBox_valueChanged(int value) +{ + _model->setIntervalTime(value); +} + +void MissionEventsDialog::on_chainedCheckBox_stateChanged(int state) +{ + _model->setChained(state == Qt::Checked); + updateEventBitmap(); + updateEventUi(); +} + +void MissionEventsDialog::on_chainedDelayBox_valueChanged(int value) +{ + _model->setChainDelay(value); +} + +void MissionEventsDialog::on_scoreBox_valueChanged(int value) +{ + _model->setEventScore(value); +} + +void MissionEventsDialog::on_teamCombo_currentIndexChanged(int index) +{ + _model->setEventTeam(ui->teamCombo->itemData(index).toInt()); +} + +void MissionEventsDialog::on_editDirectiveText_textChanged(const QString& text) +{ + SCP_string dir = text.toUtf8().constData(); + _model->setEventDirectiveText(dir); + updateEventBitmap(); +} + +void MissionEventsDialog::on_editDirectiveKeypressText_textChanged(const QString& text) +{ + SCP_string dir = text.toUtf8().constData(); + _model->setEventDirectiveKeyText(dir); +} + +void MissionEventsDialog::on_checkLogTrue_stateChanged(int state) +{ + _model->setLogTrue(state == Qt::Checked); +} + +void MissionEventsDialog::on_checkLogFalse_stateChanged(int state) +{ + _model->setLogFalse(state == Qt::Checked); +} + +void MissionEventsDialog::on_checkLogPrevious_stateChanged(int state) +{ + _model->setLogLogPrevious(state == Qt::Checked); +} + +void MissionEventsDialog::on_checkLogAlwaysFalse_stateChanged(int state) +{ + _model->setLogAlwaysFalse(state == Qt::Checked); +} + +void MissionEventsDialog::on_checkLogFirstRepeat_stateChanged(int state) +{ + _model->setLogFirstRepeat(state == Qt::Checked); +} + +void MissionEventsDialog::on_checkLogLastRepeat_stateChanged(int state) +{ + _model->setLogLastRepeat(state == Qt::Checked); +} + +void MissionEventsDialog::on_checkLogFirstTrigger_stateChanged(int state) +{ + _model->setLogFirstTrigger(state == Qt::Checked); +} + +void MissionEventsDialog::on_checkLogLastTrigger_stateChanged(int state) +{ + _model->setLogLastTrigger(state == Qt::Checked); +} + +void MissionEventsDialog::on_messageList_currentRowChanged(int row) +{ + _model->setCurrentlySelectedMessage(row); + updateMessageUi(); +} + +void MissionEventsDialog::on_messageList_itemDoubleClicked(QListWidgetItem* item) +{ + if (!item || !ui->eventTree) + return; + + const QString name = item->text(); + if (name != m_last_message_name) { + m_last_message_name = name; + m_last_message_node = -1; // reset cycle when switching message + } + + int nodes[MAX_SEARCH_MESSAGE_DEPTH]; + const int num = ui->eventTree->find_text(name.toUtf8().constData(), nodes, MAX_SEARCH_MESSAGE_DEPTH); + if (num <= 0) { + QMessageBox::information(this, tr("Error"), tr("No events using message '%1'").arg(name)); + return; + } + + // cycle to next + int next = nodes[0]; + if (m_last_message_node != -1) { + int pos = -1; + for (int i = 0; i < num; ++i) { + if (nodes[i] == m_last_message_node) { + pos = i; + break; + } + } + next = (pos == -1 || pos == num - 1) ? nodes[0] : nodes[pos + 1]; + } + + m_last_message_node = next; + ui->eventTree->hilite_item(next); +} + +void MissionEventsDialog::on_btnNewMsg_clicked() +{ + _model->createMessage(); + + rebuildMessageList(); + updateMessageUi(); +} + +void MissionEventsDialog::on_btnInsertMsg_clicked() +{ + _model->insertMessage(); + + // Refresh list UI (replace with your actual refresh) + rebuildMessageList(); + + // Keep selection/visibility in sync + const int sel = _model->getCurrentlySelectedMessage(); // or expose accessor + if (auto* w = ui->messageList) { // your list widget id + w->setCurrentRow(sel); + if (auto* it = w->item(sel)) + w->scrollToItem(it); + } + updateMessageUi(); +} + +void MissionEventsDialog::on_btnDeleteMsg_clicked() +{ + _model->deleteMessage(); + + rebuildMessageList(); + updateMessageUi(); +} + +void MissionEventsDialog::on_msgUpBtn_clicked() +{ + _model->moveMessageUp(); + rebuildMessageList(); + const int sel = _model->getCurrentlySelectedMessage(); + if (auto* w = ui->messageList) { + w->setCurrentRow(sel); + if (auto* it = w->item(sel)) + w->scrollToItem(it); + } + updateMessageUi(); +} + +void MissionEventsDialog::on_msgDownBtn_clicked() +{ + _model->moveMessageDown(); + rebuildMessageList(); + const int sel = _model->getCurrentlySelectedMessage(); + if (auto* w = ui->messageList) { + w->setCurrentRow(sel); + if (auto* it = w->item(sel)) + w->scrollToItem(it); + } + updateMessageUi(); +} + +void MissionEventsDialog::on_messageName_textChanged(const QString& text) +{ + SCP_string name = text.toUtf8().constData(); + _model->setMessageName(name); + + rebuildMessageList(); +} + +void MissionEventsDialog::on_messageContent_textChanged() +{ + SCP_string content = ui->messageContent->toPlainText().toUtf8().constData(); + _model->setMessageText(content); +} + +void MissionEventsDialog::on_btnMsgNote_clicked() +{ + if (!_model->messageIsValid()) + return; + + QDialog dlg(this); + dlg.setWindowTitle(tr("Message Note")); + auto* layout = new QVBoxLayout(&dlg); + auto* label = new QLabel(tr("Enter a note for this message:"), &dlg); + auto* edit = new QTextEdit(&dlg); + edit->setPlainText(QString::fromUtf8(_model->getMessageNote().c_str())); + edit->setMinimumSize(700, 500); // big! + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); + + layout->addWidget(label); + layout->addWidget(edit, 1); + layout->addWidget(buttons); + + QObject::connect(buttons, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); + QObject::connect(buttons, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); + + if (dlg.exec() != QDialog::Accepted) + return; + + SCP_string note = edit->toPlainText().toUtf8().constData(); + _model->setMessageNote(note); + + // Update the button text + if (note.empty()) { + ui->btnMsgNote->setText("Add Note"); + } else { + ui->btnMsgNote->setText("Edit Note"); + } +} + +void MissionEventsDialog::on_aniCombo_editingFinished() +{ + SCP_string name = ui->aniCombo->currentText().toUtf8().constData(); + _model->setMessageAni(name); + + initHeadCombo(); + ui->aniCombo->setCurrentText(QString::fromStdString(name)); +} + +void MissionEventsDialog::on_aniCombo_selectedIndexChanged(int index) +{ + SCP_string name = ui->aniCombo->itemText(index).toUtf8().constData(); + _model->setMessageAni(name); +} + +void MissionEventsDialog::on_btnAniBrowse_clicked() +{ + // TODO Build gallery from the model's known head ANIs + const QString filters = + "FSO Images (*.ani *.eff *.png);;All files (*.*)"; + const QString file = QFileDialog::getOpenFileName(this, tr("Select Head Animation"), QString(), filters); + if (file.isEmpty()) + return; + _model->setMessageAni(file.toUtf8().constData()); +} + +void MissionEventsDialog::on_waveCombo_editingFinished() +{ + SCP_string name = ui->waveCombo->currentText().toUtf8().constData(); + _model->setMessageWave(name); + + initWaveFilenames(); + ui->waveCombo->setCurrentText(QString::fromStdString(name)); +} + +void MissionEventsDialog::on_waveCombo_selectedIndexChanged(int index) +{ + SCP_string name = ui->waveCombo->itemText(index).toUtf8().constData(); + _model->setMessageWave(name); +} + +void MissionEventsDialog::on_btnBrowseWave_clicked() +{ + if (!_model->messageIsValid()) { + return; + } + + int z; + if (The_mission.game_type & MISSION_TYPE_TRAINING) { + z = cfile_push_chdir(CF_TYPE_VOICE_TRAINING); + } else { + z = cfile_push_chdir(CF_TYPE_VOICE_SPECIAL); + } + auto interface_path = QDir::currentPath(); + if (!z) { + cfile_pop_dir(); + } + + auto name = QFileDialog::getOpenFileName(this, + tr("Select message animation"), + interface_path, + "Voice Files (*.ogg *.wav);;Ogg Vorbis Files (*.ogg);;Wave Files (*.wav);;All Files (*)"); + + if (name.isEmpty()) { + // Nothing was selected + return; + } + + QFileInfo info(name); + + SCP_string file_name = info.fileName().toUtf8().constData(); + + _model->setMessageWave(file_name); + + initWaveFilenames(); + ui->waveCombo->setCurrentText(QString::fromStdString(file_name)); +} + +void MissionEventsDialog::on_btnWavePlay_clicked() +{ + _model->playMessageWave(); +} + +void MissionEventsDialog::on_personaCombo_currentIndexChanged(int index) +{ + _model->setMessagePersona(ui->personaCombo->itemData(index).toInt()); +} + +void MissionEventsDialog::on_btnUpdateStuff_clicked() +{ + auto result = _viewport->dialogProvider->showButtonDialog( + DialogType::Question, + "Update Message Stuff", + "This will update the message animation and persona to match the current mission settings. " + "Are you sure you want to do this?", + {DialogButton::Yes, DialogButton::No}); + + if (result != DialogButton::Yes) { + _model->autoSelectPersona(); + updateMessageUi(); + } +} + +void MissionEventsDialog::on_messageTeamCombo_currentIndexChanged(int index) +{ + _model->setMessageTeam(ui->messageTeamCombo->itemData(index).toInt()); +} + +} // namespace fso::fred::dialogs + diff --git a/qtfred/src/ui/dialogs/MissionEventsDialog.h b/qtfred/src/ui/dialogs/MissionEventsDialog.h new file mode 100644 index 00000000000..e44a56c687a --- /dev/null +++ b/qtfred/src/ui/dialogs/MissionEventsDialog.h @@ -0,0 +1,129 @@ +#pragma once + +#include "mission/dialogs/MissionEventsDialogModel.h" + +#include +#include + +#include "ui/widgets/sexp_tree.h" + +#include +#include + +#include + +class QCheckBox; + +namespace fso::fred::dialogs { + +namespace Ui { +class MissionEventsDialog; +} + +class MissionEventsDialog: public QDialog, public SexpTreeEditorInterface { + Q_OBJECT + + public: + explicit MissionEventsDialog(QWidget* parent, EditorViewport* viewport); + ~MissionEventsDialog() override; + + void accept() override; + void reject() override; + + SCP_vector getMessages() override; + bool hasDefaultMessageParamter() override; + + protected: + void closeEvent(QCloseEvent* event) override; + +private slots: + void on_okAndCancelButtons_accepted(); + void on_okAndCancelButtons_rejected(); + + void on_btnNewEvent_clicked(); + void on_btnInsertEvent_clicked(); + void on_btnDeleteEvent_clicked(); + void on_eventUpBtn_clicked(); + void on_eventDownBtn_clicked(); + + void on_repeatCountBox_valueChanged(int value); + void on_triggerCountBox_valueChanged(int value); + void on_intervalTimeBox_valueChanged(int value); + void on_chainedCheckBox_stateChanged(int state); + void on_chainedDelayBox_valueChanged(int value); + void on_scoreBox_valueChanged(int value); + void on_teamCombo_currentIndexChanged(int index); + + void on_editDirectiveText_textChanged(const QString& text); + void on_editDirectiveKeypressText_textChanged(const QString& text); + + void on_checkLogTrue_stateChanged(int state); + void on_checkLogFalse_stateChanged(int state); + void on_checkLogPrevious_stateChanged(int state); + void on_checkLogAlwaysFalse_stateChanged(int state); + void on_checkLogFirstRepeat_stateChanged(int state); + void on_checkLogLastRepeat_stateChanged(int state); + void on_checkLogFirstTrigger_stateChanged(int state); + void on_checkLogLastTrigger_stateChanged(int state); + + void on_messageList_currentRowChanged(int row); + void on_messageList_itemDoubleClicked(QListWidgetItem* item); + + void on_btnNewMsg_clicked(); + void on_btnInsertMsg_clicked(); + void on_btnDeleteMsg_clicked(); + void on_msgUpBtn_clicked(); + void on_msgDownBtn_clicked(); + + void on_messageName_textChanged(const QString& text); + void on_messageContent_textChanged(); + void on_btnMsgNote_clicked(); + void on_aniCombo_editingFinished(); + void on_aniCombo_selectedIndexChanged(int index); + void on_btnAniBrowse_clicked(); + void on_waveCombo_editingFinished(); + void on_waveCombo_selectedIndexChanged(int index); + void on_btnBrowseWave_clicked(); + void on_btnWavePlay_clicked(); + void on_personaCombo_currentIndexChanged(int index); + void on_btnUpdateStuff_clicked(); + void on_messageTeamCombo_currentIndexChanged(int index); + + +private: // NOLINT(readability-redundant-access-specifiers) + std::unique_ptr ui; + EditorViewport* _viewport; + std::unique_ptr _treeOps; + std::unique_ptr _model; + + int m_last_message_node = -1; + QString m_last_message_name; + + void updateEventUi(); + void updateEventMoveButtons(); + void updateMessageUi(); + void updateMessageMoveButtons(); + + void initMessageList(); + void initHeadCombo(); + void initWaveFilenames(); + void initPersonas(); + + void initMessageTeams(); + void initEventTeams(); + + void rootNodeDeleted(int node); + void rootNodeRenamed(int node); + void rootNodeFormulaChanged(int old, int node); + void rootNodeSelectedByFormula(int formula); + + void rebuildMessageList(); + void initMessageWidgets(); + void initEventWidgets(); + void updateEventBitmap(); + + static SCP_vector read_root_formula_order(sexp_tree* tree); +}; + +} // namespace fso::fred::dialogs + diff --git a/qtfred/src/ui/widgets/sexp_tree.cpp b/qtfred/src/ui/widgets/sexp_tree.cpp index 985847a4eb0..2a6975d9abd 100644 --- a/qtfred/src/ui/widgets/sexp_tree.cpp +++ b/qtfred/src/ui/widgets/sexp_tree.cpp @@ -49,6 +49,18 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #define TREE_NODE_INCREMENT 100 @@ -133,6 +145,15 @@ QString node_image_to_resource_name(NodeImage image) { } return ":/images/bitmap1.png"; } + +QPoint s_dragStartPos; +QTreeWidgetItem* s_dragSourceRoot = nullptr; +bool s_dragging = false; + +bool isRoot(QTreeWidgetItem* it) +{ + return it && !it->parent(); +} } SexpTreeEditorInterface::SexpTreeEditorInterface() : @@ -208,6 +229,57 @@ QIcon sexp_tree::convertNodeImageToIcon(NodeImage image) { return QIcon(node_image_to_resource_name(image)); } +class NoteBadgeDelegate final : public QStyledItemDelegate { + public: + explicit NoteBadgeDelegate(sexp_tree* tree) : QStyledItemDelegate(tree) {} + + void paint(QPainter* p, const QStyleOptionViewItem& option, const QModelIndex& index) const override + { + QStyleOptionViewItem opt(option); + initStyleOption(&opt, index); + + // draw the standard icon + text first + const QWidget* w = opt.widget; + const QStyle* s = w ? w->style() : QApplication::style(); + s->drawControl(QStyle::CE_ItemViewItem, &opt, p, w); + + // if there’s a note, paint the badge directly after the text + const QString note = index.data(sexp_tree::NoteRole).toString(); + if (!note.isEmpty()) { + // where Qt drew the text + QRect textRect = s->subElementRect(QStyle::SE_ItemViewItemText, &opt, w); + + // compute how much text actually fit (respect eliding) + QFontMetrics fm(opt.font); + const QString shown = fm.elidedText(opt.text, opt.textElideMode, textRect.width()); + const int textWidth = fm.horizontalAdvance(shown); + + // pick an icon; use your existing mapping + const QIcon icon = sexp_tree::convertNodeImageToIcon(NodeImage::COMMENT); + const int dpi = p->device() ? p->device()->logicalDpiX() : 96; + const int sz = opt.decorationSize.isValid() ? opt.decorationSize.height() : int(16 * dpi / 96); + const QPixmap pm = icon.pixmap(sz, sz); + + // place badge just after the text, with a small pad + const int pad = 10; + int x = textRect.left() + textWidth + pad; + int y = textRect.center().y() - pm.height() / 2; + + // keep inside cell if the row is very tight + const int rightBound = option.rect.right() - 2; + if (x + pm.width() > rightBound) + x = rightBound - pm.width(); + + p->save(); + // ensure good contrast on selected rows + if (opt.state & QStyle::State_Selected) + p->setCompositionMode(QPainter::CompositionMode_SourceOver); + p->drawPixmap(x, y, pm); + p->restore(); + } + } +}; + // constructor sexp_tree::sexp_tree(QWidget* parent) : QTreeWidget(parent) { setSelectionMode(QTreeWidget::SingleSelection); @@ -224,6 +296,9 @@ sexp_tree::sexp_tree(QWidget* parent) : QTreeWidget(parent) { connect(this, &QWidget::customContextMenuRequested, this, &sexp_tree::customMenuHandler); connect(this, &QTreeWidget::itemChanged, this, &sexp_tree::handleItemChange); connect(this, &QTreeWidget::itemSelectionChanged, this, &sexp_tree::handleNewItemSelected); + connect(this, &QTreeWidget::itemDoubleClicked, this, [this](QTreeWidgetItem* item, int /*column*/) {openNodeEditor(item);}); + + setItemDelegateForColumn(0, new NoteBadgeDelegate(this)); } sexp_tree::~sexp_tree() = default; @@ -1740,6 +1815,32 @@ void sexp_tree::expand_branch(QTreeWidgetItem* h) { } } +// edit the comment for the operator pointed to by item_index +void sexp_tree::editNoteForItem(QTreeWidgetItem* it) +{ + const QString old = it->data(0, NoteRole).toString(); + bool ok = false; + const QString text = QInputDialog::getMultiLineText(this, tr("Edit Note"), tr("Node note:"), old, &ok); + if (!ok) + return; + it->setData(0, NoteRole, text); + applyVisuals(it); + + Q_EMIT nodeAnnotationChanged(static_cast(it), text); +} + +void sexp_tree::editBgColorForItem(QTreeWidgetItem* it) +{ + const auto start = it->data(0, BgColorRole).value(); + const QColor c = QColorDialog::getColor(start.isValid() ? start : Qt::yellow, this, tr("Choose Background Color")); + if (!c.isValid()) + return; + it->setData(0, BgColorRole, c); + applyVisuals(it); + + Q_EMIT nodeBgColorChanged(static_cast(it), c); +} + void sexp_tree::merge_operator(int /*node*/) { /* char buf[256]; int child; @@ -2569,73 +2670,77 @@ void sexp_tree::move_branch(int source, int parent) { } } -QTreeWidgetItem* sexp_tree::move_branch(QTreeWidgetItem* source, QTreeWidgetItem* parent, QTreeWidgetItem* after) { - QTreeWidgetItem* h = nullptr; - if (source) { - uint i; +QTreeWidgetItem* sexp_tree::move_branch(QTreeWidgetItem* source, QTreeWidgetItem* parent, QTreeWidgetItem* after) +{ + if (!source) + return nullptr; - for (i = 0; i < tree_nodes.size(); i++) { - if (tree_nodes[i].handle == source) { - break; - } - } + // Find matching tree_nodes slot, if any, to update its handle + uint idx = 0; + while (idx < tree_nodes.size() && tree_nodes[idx].handle != source) { + ++idx; + } - if (i < tree_nodes.size()) { - auto icon = source->icon(0); - h = insertWithIcon(source->text(0), icon, parent, after); - tree_nodes[i].handle = h; - } else { - auto icon = source->icon(0); - h = insertWithIcon(source->text(0), icon, parent, after); - } + // Create the destination item + const auto icon = source->icon(0); + QTreeWidgetItem* h = insertWithIcon(source->text(0), icon, parent, after); + if (idx < tree_nodes.size()) { + tree_nodes[idx].handle = h; + } - h->setData(0, FormulaDataRole, source->data(0, FormulaDataRole)); - for (auto childIdx = 0; childIdx < source->childCount(); ++i) { - auto child = source->child(childIdx); + // Copy all per-item data we rely on for annotations/visuals + h->setData(0, FormulaDataRole, source->data(0, FormulaDataRole)); + h->setData(0, NoteRole, source->data(0, NoteRole)); + h->setData(0, BgColorRole, source->data(0, BgColorRole)); + applyVisuals(h); - move_branch(child, h); - } + // Move children safely + while (source->childCount() > 0) { + auto* child = source->child(0); + move_branch(child, h); + } - h->setExpanded(source->isExpanded()); + h->setExpanded(source->isExpanded()); - source->parent()->removeChild(source); + // Remove the old item from the tree + if (auto* p = source->parent()) { + p->removeChild(source); + delete source; + } else if (auto* tw = source->treeWidget()) { + const int topIdx = tw->indexOfTopLevelItem(source); + if (topIdx >= 0) + tw->takeTopLevelItem(topIdx); + delete source; } return h; } -void sexp_tree::copy_branch(QTreeWidgetItem* source, QTreeWidgetItem* parent, QTreeWidgetItem* after) { - QTreeWidgetItem* h = nullptr; - if (source) { - uint i; - - for (i = 0; i < tree_nodes.size(); i++) { - if (tree_nodes[i].handle == source) { - break; - } - } - - if (i < tree_nodes.size()) { - auto icon = source->icon(0); - h = insertWithIcon(source->text(0), icon, parent, after); - tree_nodes[i].handle = h; - } else { - auto icon = source->icon(0); - h = insertWithIcon(source->text(0), icon, parent, after); - } +void sexp_tree::copy_branch(QTreeWidgetItem* source, QTreeWidgetItem* parent, QTreeWidgetItem* after) +{ + if (!source) + return; - h->setData(0, FormulaDataRole, source->data(0, FormulaDataRole)); - for (auto childIdx = 0; childIdx < source->childCount(); ++i) { - auto child = source->child(childIdx); + const auto icon = source->icon(0); + QTreeWidgetItem* h = insertWithIcon(source->text(0), icon, parent, after); - move_branch(child, h); - } + // Copy per-item data/annotations + h->setData(0, FormulaDataRole, source->data(0, FormulaDataRole)); + h->setData(0, NoteRole, source->data(0, NoteRole)); + h->setData(0, BgColorRole, source->data(0, BgColorRole)); + applyVisuals(h); - h->setExpanded(source->isExpanded()); + // Copy children (recursively COPY, not move) + for (int i = 0; i < source->childCount(); ++i) { + copy_branch(source->child(i), h); } + + h->setExpanded(source->isExpanded()); } -void sexp_tree::move_root(QTreeWidgetItem* source, QTreeWidgetItem* dest, bool insert_before) { +// Old version of move_root +/*void sexp_tree::move_root(QTreeWidgetItem* source, QTreeWidgetItem* dest, bool insert_before) +{ auto after = dest; if (insert_before) { @@ -2645,6 +2750,45 @@ void sexp_tree::move_root(QTreeWidgetItem* source, QTreeWidgetItem* dest, bool i auto h = move_branch(source, itemFromIndex(rootIndex()), after); setCurrentItem(h); modified(); +}*/ + +void sexp_tree::move_root(QTreeWidgetItem* source, QTreeWidgetItem* dest, bool insert_before) +{ + if (!source || !dest) + return; + if (source->parent() || dest->parent()) + return; // roots only + + // Take the source out of the top-level list + const int srcIdx = indexOfTopLevelItem(source); + if (srcIdx < 0) + return; + + // Remove first so the destination index we compute is correct after removal + QTreeWidgetItem* moved = takeTopLevelItem(srcIdx); + + // Recompute the current index of dest after the removal + int dstIdx = indexOfTopLevelItem(dest); + if (dstIdx < 0) { + // put it back where it was + insertTopLevelItem(srcIdx, moved); + return; + } + + if (!insert_before) { + // inserting after the drop target + ++dstIdx; + } + + // Clamp and insert + dstIdx = std::max(0, std::min(dstIdx, topLevelItemCount())); + insertTopLevelItem(dstIdx, moved); + setCurrentItem(moved); + + // Mark the tree modified + modified(); + + Q_EMIT rootOrderChanged(); } QTreeWidgetItem* @@ -2652,6 +2796,123 @@ sexp_tree::insert(const QString& lpszItem, NodeImage image, QTreeWidgetItem* hPa return insertWithIcon(lpszItem, convertNodeImageToIcon(image), hParent, hInsertAfter); } +void sexp_tree::keyPressEvent(QKeyEvent* e) +{ + // Clear stale state if popup was closed externally + if (_opPopup && _opPopupActive && !_opPopup->isVisible()) { + _opPopupActive = false; + _opNodeIndex = -1; + } + + // Route keys while popup is active + if (_opPopupActive && _opPopup) { + switch (e->key()) { + case Qt::Key_Escape: + endOperatorQuickSearch(false); + return; + case Qt::Key_Return: + case Qt::Key_Enter: + endOperatorQuickSearch(true); + return; + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_PageUp: + case Qt::Key_PageDown: + case Qt::Key_Home: + case Qt::Key_End: + QCoreApplication::sendEvent(_opList, e); + return; + default: + QCoreApplication::sendEvent(_opEdit, e); + return; + } + } + + // Space opens the editor for the selected node + if (e->key() == Qt::Key_Space && currentItem()) { + openNodeEditor(currentItem()); + return; + } + + QTreeWidget::keyPressEvent(e); +} + +bool sexp_tree::eventFilter(QObject* obj, QEvent* ev) +{ + if (obj == _opPopup) { + switch (ev->type()) { + case QEvent::Hide: + case QEvent::Close: + case QEvent::WindowDeactivate: + // Treat any external close as cancel; just clear state. + _opPopupActive = false; + _opNodeIndex = -1; + setFocus(Qt::OtherFocusReason); + break; + default: + break; + } + } + return QTreeWidget::eventFilter(obj, ev); +} + +void sexp_tree::mousePressEvent(QMouseEvent* e) +{ + s_dragStartPos = e->pos(); + s_dragSourceRoot = itemAt(e->pos()); + if (!isRoot(s_dragSourceRoot)) + s_dragSourceRoot = nullptr; // roots only + s_dragging = false; + QTreeWidget::mousePressEvent(e); +} + +void sexp_tree::mouseMoveEvent(QMouseEvent* e) +{ + if (!s_dragSourceRoot) { + QTreeWidget::mouseMoveEvent(e); + return; + } + if (!(e->buttons() & Qt::LeftButton)) { + QTreeWidget::mouseMoveEvent(e); + return; + } + const int dist = (e->pos() - s_dragStartPos).manhattanLength(); + if (!s_dragging && dist < QApplication::startDragDistance()) { + QTreeWidget::mouseMoveEvent(e); + return; + } + + // “Dragging” – we just highlight potential drop target (a root under the cursor) + s_dragging = true; + if (auto* over = itemAt(e->pos())) { + if (isRoot(over)) + setCurrentItem(over); // simple visual cue like OG’s SelectDropTarget + } + + // No QDrag payload; we’ll do the move on mouse release to keep logic simple. + QTreeWidget::mouseMoveEvent(e); +} + +void sexp_tree::mouseReleaseEvent(QMouseEvent* e) +{ + if (s_dragging && s_dragSourceRoot) { + auto* dropTarget = itemAt(e->pos()); + if (dropTarget && isRoot(dropTarget) && dropTarget != s_dragSourceRoot) { + // OG rule: if moving up, insert_before=true; if moving down, insert_after + // (so we “end up where we dropped”). :contentReference[oaicite:1]{index=1} + const int srcIdx = indexOfTopLevelItem(s_dragSourceRoot); + const int dstIdx = indexOfTopLevelItem(dropTarget); + const bool insert_before = (srcIdx > dstIdx); + + // Perform the visual move + move_root(s_dragSourceRoot, dropTarget, insert_before); + } + } + s_dragSourceRoot = nullptr; + s_dragging = false; + QTreeWidget::mouseReleaseEvent(e); +} + QTreeWidgetItem* sexp_tree::insertWithIcon(const QString& lpszItem, const QIcon& image, QTreeWidgetItem* hParent, @@ -5783,11 +6044,17 @@ std::unique_ptr sexp_tree::buildContextMenu(QTreeWidgetItem* h) { auto edit_data_act = popup_menu->addAction(tr("&Edit Data"), this, [this]() { editDataActionHandler(); }); popup_menu->addAction(tr("Expand All"), this, [this]() { expand_branch(currentItem()); }); + popup_menu->addSection(tr("Annotations")); + auto edit_comment_act = popup_menu->addAction(tr("Edit Comment"), this, [this, h]() { editNoteForItem(h); }); + auto edit_color_act = popup_menu->addAction(tr("Edit Color"), this, [this, h]() { editBgColorForItem(h); }); + edit_comment_act->setEnabled(_interface->getFlags()[TreeFlags::AnnotationsAllowed]); + edit_color_act->setEnabled(_interface->getFlags()[TreeFlags::AnnotationsAllowed]); + popup_menu->addSection(tr("Copy operations")); auto cut_act = popup_menu->addAction(tr("Cut"), this, [this]() { cutActionHandler(); }, QKeySequence::Cut); cut_act->setEnabled(false); auto copy_act = popup_menu->addAction(tr("Copy"), this, [this]() { copyActionHandler(); }, QKeySequence::Copy); - auto paste_act = popup_menu->addAction(tr("Paste"), this, [this]() { pasteActionHandler(); }, QKeySequence::Paste); + auto paste_act = popup_menu->addAction(tr("Paste"), this, [this]() { pasteActionHandler(); }, QKeySequence::Paste); //TODO match paste/add paste paste_act->setEnabled(false); popup_menu->addSection(tr("Add")); @@ -6902,36 +7169,309 @@ void sexp_tree::cutActionHandler() { // fall through to ID_DELETE case. deleteActionHandler(); } -void sexp_tree::deleteActionHandler() { - if (currentItem() == nullptr) { +void sexp_tree::deleteActionHandler() +{ + if (currentItem() == nullptr || !_interface) { return; } - if (_interface->getFlags()[TreeFlags::RootDeletable] && (item_index == -1)) { - auto item = currentItem(); - item_index = item->data(0, FormulaDataRole).toInt(); + auto* item = currentItem(); + const bool isRootItem = (item->parent() == nullptr); - rootNodeDeleted(item_index); + // Root delete: allowed if flag is set and the selected item is a top-level row + if (_interface->getFlags()[TreeFlags::RootDeletable] && isRootItem) { + const int formulaNode = item->data(0, FormulaDataRole).toInt(); - free_node2(item_index); + // Tell the dialog/model first so it can drop the event row + rootNodeDeleted(formulaNode); + + // Free the underlying SEXP subtree safely + if (formulaNode >= 0) { + free_node2(formulaNode); + } + + // Remove the UI item and reset selection/index delete item; + setCurrentItemIndex(-1); modified(); return; } - Assert(item_index >= 0); - auto h_parent = currentItem()->parent(); - auto parent = tree_nodes[item_index].parent; + // Non-root delete + Assertion(item_index >= 0, "Attempt to delete node at invalid index!"); + auto* h_parent = item->parent(); + const int parent = tree_nodes[item_index].parent; + + // If we somehow reached here on a root, bail safely + if (parent == -1) { + // Treat it as a root delete fallback + const int formulaNode = item->data(0, FormulaDataRole).toInt(); + rootNodeDeleted(formulaNode); + if (formulaNode >= 0) + free_node2(formulaNode); + delete item; + setCurrentItemIndex(-1); + modified(); + return; + } - Assert(parent != -1 && tree_nodes[parent].handle == h_parent); + Assertion(tree_nodes[parent].handle == h_parent, "Tree node handle mismatch!"); free_node(item_index); - delete currentItem(); + delete item; modified(); } void sexp_tree::editDataActionHandler() { beginItemEdit(currentItem()); } + +// Compute the valid operators for replacing/adding at the given node, based on parent arg type. +// This mirrors the original menu enable/disable logic. See original for how "type" is computed. +QStringList sexp_tree::validOperatorsForNode(int nodeIndex) +{ + QStringList out; + if (nodeIndex < 0 || nodeIndex >= static_cast(tree_nodes.size())) + return out; + + const int parent = tree_nodes[nodeIndex].parent; + const int argIndex = (parent >= 0) ? find_argument_number(parent, nodeIndex) : 0; + + // Original behavior: compute the OPF type expected at this node + const int opf = query_node_argument_type(nodeIndex); // handles top-level = OPF_NULL, etc. + if (opf < 0) + return out; + + // Build the canonical list for this OPF (this mirrors classic FRED) + sexp_list_item* list = get_listing_opf(opf, parent, argIndex); // may be nullptr + for (auto* p = list; p; p = p->next) { + if (p->op >= 0) { + const int opIndex = p->op; + + // Optional: keep parity with the menu, which disables ops lacking default args + if (!query_default_argument_available(opIndex)) + continue; + + out.push_back(QString::fromStdString(Operators[opIndex].text)); + } + // (items with p->op < 0 are data items like strings/ships/etc.; we ignore for operator search) + } + + if (list) + list->destroy(); + + out.removeDuplicates(); + std::sort(out.begin(), out.end(), [](const QString& a, const QString& b) { + return a.compare(b, Qt::CaseInsensitive) < 0; + }); + return out; +} + +void sexp_tree::openNodeEditor(QTreeWidgetItem* item) +{ + if (!item || !_interface) + return; + + // if this is the root and it's not editable, bail. + if (!_interface->getFlags()[TreeFlags::RootEditable] && !item->parent()) + return; + + if (item && !item->parent()) { // root only + beginItemEdit(item); // sets _currently_editing + calls editItem + return; + } + + // If an operator popup is already up, ignore + if (_opPopupActive && _opPopup && _opPopup->isVisible()) + return; + + // Map item -> internal node index + int nodeIdx = -1; + for (uint i = 0; i < tree_nodes.size(); ++i) { + if (tree_nodes[i].handle == item) { + nodeIdx = static_cast(i); + break; + } + } + if (nodeIdx < 0) + return; + + // operator chooser vs inline data edit + const QStringList ops = validOperatorsForNode(nodeIdx); // uses get_listing_opf(...) + if (!ops.isEmpty()) { + startOperatorQuickSearch(item, QString()); + return; + } + + // Fallback to inline edit + beginItemEdit(item); +} + +void sexp_tree::startOperatorQuickSearch(QTreeWidgetItem* item, const QString& seed) +{ + if (!item) + return; + + // Map item -> node index + int nodeIdx = -1; + for (uint i = 0; i < tree_nodes.size(); ++i) { + if (tree_nodes[i].handle == item) { + nodeIdx = static_cast(i); + break; + } + } + if (nodeIdx < 0) + return; + + // Only allow on editable positions (operator or data) that live beneath a parent + // (We’ll compute OPF from parent or root as necessary) + _opAll = validOperatorsForNode(nodeIdx); + if (_opAll.isEmpty()) + return; + + _opNodeIndex = nodeIdx; + + if (!_opPopup) { + _opPopup = new QFrame(viewport(), Qt::Popup); + _opPopup->setFrameShape(QFrame::Box); + _opPopup->setFrameShadow(QFrame::Plain); + _opPopup->installEventFilter(this); // <-- important + auto* layout = new QVBoxLayout(_opPopup); + layout->setContentsMargins(4, 4, 4, 4); + _opEdit = new QLineEdit(_opPopup); + _opList = new QListWidget(_opPopup); + _opList->setSelectionMode(QAbstractItemView::SingleSelection); + _opList->setUniformItemSizes(true); + layout->addWidget(_opEdit); + layout->addWidget(_opList); + connect(_opEdit, &QLineEdit::textChanged, this, &sexp_tree::filterOperatorPopup); + connect(_opEdit, &QLineEdit::returnPressed, [this]() { endOperatorQuickSearch(true); }); + connect(_opList, &QListWidget::itemActivated, [this](QListWidgetItem*) { endOperatorQuickSearch(true); }); + connect(_opList, &QListWidget::itemClicked, [this](QListWidgetItem*) { endOperatorQuickSearch(true); }); + } + + _opList->clear(); + _opList->addItems(_opAll); + if (!seed.isEmpty()) { + _opEdit->setText(seed); + _opEdit->selectAll(); + filterOperatorPopup(seed); + } else { + _opEdit->clear(); + if (_opList->count() > 0) + _opList->setCurrentRow(0); + } + + // Size the popup: width = widest operator text + scrollbar + padding; height ~10 rows + QFontMetrics fm(_opList->font()); + int w = 0; + for (const auto& s : _opAll) + w = std::max(w, fm.horizontalAdvance(s)); + w += _opList->verticalScrollBar()->sizeHint().width() + 24; // padding + int rowH = fm.height() + 6; + int h = (std::min(10, std::max(4, _opList->count())) * rowH) + _opEdit->sizeHint().height() + 12; + + // Place below the item + QRect itemRect = visualItemRect(item); + QPoint topLeft = viewport()->mapToGlobal(itemRect.topLeft()); + _opPopup->setGeometry(QRect(topLeft.x(), topLeft.y(), std::max(w, 260), h)); + _opPopup->show(); + _opEdit->setFocus(); + _opPopupActive = true; +} + +void sexp_tree::filterOperatorPopup(const QString& text) +{ + _opList->clear(); + if (text.isEmpty()) { + _opList->addItems(_opAll); + } else { + for (const auto& s : _opAll) { + if (s.contains(text, Qt::CaseInsensitive)) + _opList->addItem(s); + } + } + if (_opList->count() > 0) + _opList->setCurrentRow(0); +} + +void sexp_tree::endOperatorQuickSearch(bool confirm) +{ + if (!_opPopupActive) + return; + + // Cache before hiding since hide triggers eventFilter which clears state + const int node = _opNodeIndex; + + QString chosenOp; + QString typed = (_opEdit ? _opEdit->text().trimmed() : QString()); + + if (confirm) { + // If user selected an operator in the list, prefer that + if (_opList && _opList->currentItem()) + chosenOp = _opList->currentItem()->text(); + + // If nothing selected, see if typed text is a valid *number* for this slot + if (chosenOp.isEmpty() && !typed.isEmpty()) { + const int expected = query_node_argument_type(node); // OPF_* + const bool expectsNumber = (expected == OPF_NUMBER) || (expected == OPF_POSITIVE) || + (expected == OPF_AMBIGUOUS); // allow numerics here too??? + + // Accept +/- integers + static const QRegularExpression kIntRx(QStringLiteral(R"(^[+-]?\d+$)")); + const bool isInt = kIntRx.match(typed).hasMatch(); + + // Enforce positivity if required + bool okForPositive = true; + if (expected == OPF_POSITIVE && isInt) { + okForPositive = typed.toLongLong() > 0; + } + + if (expectsNumber && isInt && okForPositive) { + // Commit as NUMBER data + if (_opPopup) + _opPopup->hide(); + _opPopupActive = false; + _opNodeIndex = -1; + + setCurrentItemIndex(node); // sets item_index for replace_data() + int type = SEXPT_NUMBER | SEXPT_VALID; + if (tree_nodes[item_index].type & SEXPT_MODIFIER) + type |= SEXPT_MODIFIER; + + replace_data(typed.toUtf8().constData(), type); + setFocus(Qt::OtherFocusReason); + return; // done + } + } + + // fall back to closest operator match from typed text + if (chosenOp.isEmpty() && !typed.isEmpty()) { + auto best = match_closest_operator(typed.toStdString(), node); + if (!best.empty()) + chosenOp = QString::fromStdString(best); + } + } + + // Close popup and reset state + if (_opPopup) + _opPopup->hide(); + _opPopupActive = false; + _opNodeIndex = -1; + + // Commit operator if we resolved one + if (confirm && !chosenOp.isEmpty() && node >= 0 && node < static_cast(tree_nodes.size())) { + setCurrentItemIndex(node); + const int op_num = get_operator_index(chosenOp.toUtf8().constData()); + if (op_num >= 0) { + add_or_replace_operator(op_num, /*replace_flag*/ 1); + if (tree_nodes[node].handle) + tree_nodes[node].handle->setExpanded(true); + } + } + + setFocus(Qt::OtherFocusReason); +} + void sexp_tree::handleItemChange(QTreeWidgetItem* item, int /*column*/) { if (!_currently_editing) { return; @@ -6962,7 +7502,8 @@ void sexp_tree::handleItemChange(QTreeWidgetItem* item, int /*column*/) { Assert(node < tree_nodes.size()); if (tree_nodes[node].type & SEXPT_OPERATOR) { - auto op = match_closest_operator(str.toStdString(), node); + SCP_string text = str.toUtf8().constData(); + auto op = match_closest_operator(text, node); if (op.empty()) { return; } // Goober5000 - avoids crashing @@ -7360,6 +7901,17 @@ void sexp_tree::handleNewItemSelected() { void sexp_tree::deleteCurrentItem() { deleteActionHandler(); } +void sexp_tree::applyVisuals(QTreeWidgetItem* it) +{ + const auto note = it->data(0, NoteRole).toString(); + const auto color = it->data(0, BgColorRole).value(); + it->setToolTip(0, note); + + // Background color for the entire row + if (color.isValid()) { + it->setBackground(0, QBrush(color)); + } +} int sexp_tree::getCurrentItemIndex() const { return item_index; } diff --git a/qtfred/src/ui/widgets/sexp_tree.h b/qtfred/src/ui/widgets/sexp_tree.h index 04f4503b1b3..4f45110aad5 100644 --- a/qtfred/src/ui/widgets/sexp_tree.h +++ b/qtfred/src/ui/widgets/sexp_tree.h @@ -17,6 +17,7 @@ #include #include +#include namespace fso { namespace fred { @@ -58,6 +59,7 @@ FLAG_LIST(TreeFlags) { LabeledRoot = 0, RootDeletable, RootEditable, + AnnotationsAllowed, NUM_VALUES }; @@ -171,7 +173,12 @@ class sexp_tree: public QTreeWidget { Q_OBJECT public: - static const int FormulaDataRole = Qt::UserRole; + enum { + FormulaDataRole = Qt::UserRole + 1, + SexpNodeIdRole = Qt::UserRole + 2, + NoteRole = Qt::UserRole + 100, + BgColorRole = Qt::UserRole + 101 + }; static QIcon convertNodeImageToIcon(NodeImage image); @@ -217,6 +224,8 @@ class sexp_tree: public QTreeWidget { void ensure_visible(int node); int node_error(int node, const char* msg, int* bypass); void expand_branch(QTreeWidgetItem* h); + void editNoteForItem(QTreeWidgetItem* h); + void editBgColorForItem(QTreeWidgetItem* h); void expand_operator(int node); void merge_operator(int node); int identify_arg_type(int node); @@ -380,6 +389,8 @@ class sexp_tree: public QTreeWidget { void initializeEditor(Editor* edit, SexpTreeEditorInterface* editorInterface = nullptr); void deleteCurrentItem(); + + static void applyVisuals(QTreeWidgetItem* it); signals: void miniHelpChanged(const QString& text); void helpChanged(const QString& text); @@ -389,16 +400,32 @@ class sexp_tree: public QTreeWidget { void rootNodeRenamed(int node); void rootNodeFormulaChanged(int old, int node); void nodeChanged(int node); + void rootOrderChanged(); void selectedRootChanged(int formula); + void nodeAnnotationChanged(void* handle, const QString& note); + void nodeBgColorChanged(void* handle, const QColor& color); + // Generated message map functions protected: + void keyPressEvent(QKeyEvent* e) override; + bool eventFilter(QObject* obj, QEvent* ev) override; + void mousePressEvent(QMouseEvent* e) override; + void mouseMoveEvent(QMouseEvent* e) override; + void mouseReleaseEvent(QMouseEvent* e) override; + QTreeWidgetItem* insertWithIcon(const QString& lpszItem, const QIcon& image, QTreeWidgetItem* hParent = nullptr, QTreeWidgetItem* hInsertAfter = nullptr); + //bool edit(const QModelIndex& index, QAbstractItemView::EditTrigger trigger, QEvent* event) override + //{ + //_currently_editing = true; // mark explicit edit + //return QTreeWidget::edit(index, trigger, event); + //} + void customMenuHandler(const QPoint& pos); void handleNewItemSelected(); @@ -437,6 +464,20 @@ class sexp_tree: public QTreeWidget { int Add_count, Replace_count; int Modify_variable; + // Operator quick search popup + QFrame* _opPopup = nullptr; + QLineEdit* _opEdit = nullptr; + QListWidget* _opList = nullptr; + QStringList _opAll; // all valid operators for current context + int _opNodeIndex = -1; // tree_nodes[] index of the node being edited + bool _opPopupActive = false; + + void openNodeEditor(QTreeWidgetItem* item); + void startOperatorQuickSearch(QTreeWidgetItem* item, const QString& seed = QString()); + void endOperatorQuickSearch(bool confirm); + void filterOperatorPopup(const QString& text); + QStringList validOperatorsForNode(int nodeIndex); + void handleItemChange(QTreeWidgetItem* item, int column); void beginItemEdit(QTreeWidgetItem* item); diff --git a/qtfred/ui/EventEditorDialog.ui b/qtfred/ui/MissionEventsDialog.ui similarity index 72% rename from qtfred/ui/EventEditorDialog.ui rename to qtfred/ui/MissionEventsDialog.ui index c4b4f32abae..9ae78409685 100644 --- a/qtfred/ui/EventEditorDialog.ui +++ b/qtfred/ui/MissionEventsDialog.ui @@ -1,13 +1,13 @@ - fso::fred::dialogs::EventEditorDialog - + fso::fred::dialogs::MissionEventsDialog + 0 0 - 926 - 857 + 1012 + 930 @@ -128,48 +128,9 @@ - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - true - - - - - - - - Team 1 - - - - - Team 2 - - - - - none - - - - - - + + + 0 @@ -177,15 +138,22 @@ - Delete Event + Insert Event - - + + + + -1 + + + 16777215 + + - - + + 0 @@ -193,31 +161,33 @@ - Insert Event + + + + + :/images/arrow_up.png + - - - - Repeat Co&unt - - - repeatCountBox + + + + 16777215 - - + + - Score + Interval time - scoreBox + intervalTimeBox - + Chain Dela&y @@ -227,62 +197,59 @@ - - - - Qt::ScrollBarAlwaysOff + + + + Score - - true + + scoreBox - - - - Chained + + + + 16777215 - - - - - - - Tri&gger Count + + + + + 0 + 0 + - - triggerCountBox + + New Event - - - - Interval time + + + + Qt::Horizontal - - intervalTimeBox + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok - + Messages - - - - + Properties - + @@ -295,7 +262,7 @@ - + ANI file @@ -305,14 +272,17 @@ - + + + + true - + Wave file @@ -322,46 +292,17 @@ - - - - &Team - - - messageTeamCombo - - - - - - - true - - - - + - - - - - Team 1 - - - - - Team 2 - - - - - none - - + + + + (multiplayer TvT only) + - + Persona @@ -371,7 +312,7 @@ - + @@ -388,51 +329,71 @@ - - + + - Browse + Update Stuff - - + + - Update Stuff + &Team + + + messageTeamCombo - - + + + + true + + + + + - (multiplayer TvT only) + Browse + + + + + + + Note - - - - Name - - - messageName - - - QAbstractItemView::NoEditTriggers - true + false + + + Message Text + + + Message Text + + + Message Text + + + + 0 @@ -450,6 +411,19 @@ + + + + + 0 + 0 + + + + Insert Msg + + + @@ -465,27 +439,95 @@ - - - - Message Text - - - Message Text - - - Message Text - - + + + + + + Name + + + messageName + + + + + + + + + + + 0 + 0 + + + + + + + + :/images/arrow_up.png + + + + + + + + + 0 + 0 + + + + + + + + :/images/arrow_down.png + + + + + - - + + + + Chained + + - - + + + + Qt::ScrollBarAlwaysOff + + + true + + + + + + + true + + + + + + + 16777215 + + + + + 0 @@ -493,12 +535,60 @@ - New Event + Delete Event - - + + + + Repeat Co&unt + + + repeatCountBox + + + + + + + + + + -1 + + + 16777215 + + + + + + + Tri&gger Count + + + triggerCountBox + + + + + + + + 0 + 0 + + + + + + + + :/images/arrow_down.png + + + @@ -516,38 +606,5 @@ - - - buttonBox - rejected() - fso::fred::dialogs::EventEditorDialog - reject() - - - 917 - 636 - - - 530 - 7 - - - - - buttonBox - accepted() - fso::fred::dialogs::EventEditorDialog - accept() - - - 917 - 636 - - - 504 - -11 - - - - + From 42eae002d24b317787c1ce9f7895c210d3fbad87 Mon Sep 17 00:00:00 2001 From: The Force <2040992+TheForce172@users.noreply.github.com> Date: Fri, 29 Aug 2025 16:01:11 +0100 Subject: [PATCH 445/466] Fix Permanent Guardian (#7009) * Switch to using signals/slots rather than connect * Adds coloured persona box Adds coloured persona box/ fixes persona loading, and reduces amount of data reset on model change * Update To new flags widget Updates To new flags widget this reqires moving several non flag items to other dialogs, which also lead to adding a new feature that lets a custom guardian threshold be set in editor * Switch to size_t * Make Mission Spec work with changes to Flaglist * Remove Duplicate member * Clang Appeasement * Make static * Properly initialise guardian threshold --- code/mission/missionparse.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/mission/missionparse.h b/code/mission/missionparse.h index a564675de84..df0adbe91bc 100644 --- a/code/mission/missionparse.h +++ b/code/mission/missionparse.h @@ -461,7 +461,7 @@ class p_object flagset flags; // mission savable flags int escort_priority = 0; // priority in escort list - int ship_guardian_threshold; + int ship_guardian_threshold = 0; int ai_class = -1; int hotkey = -1; // hotkey number (between 0 and 9) -1 means no hotkey int score = 0; From 730b294d7b81a4d313fe91a712602bdaef7ca800 Mon Sep 17 00:00:00 2001 From: Asteroth Date: Fri, 29 Aug 2025 20:39:06 -0400 Subject: [PATCH 446/466] add 'direct fire lead target' --- code/weapon/beam.cpp | 7 ++++++- code/weapon/weapon_flags.h | 1 + code/weapon/weapons.cpp | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/code/weapon/beam.cpp b/code/weapon/beam.cpp index 7c8c914b5a0..4a0a25e13f6 100644 --- a/code/weapon/beam.cpp +++ b/code/weapon/beam.cpp @@ -2239,7 +2239,9 @@ int beam_start_firing(beam *b) // re-aim direct fire and antifighter beam weapons here, otherwise they tend to miss case BeamType::DIRECT_FIRE: case BeamType::ANTIFIGHTER: - beam_aim(b); + // ...unless it's intentional they sometimes miss + if (!Weapon_info[b->weapon_info_index].b_info.flags[Weapon::Beam_Info_Flags::Direct_fire_lead_target]) + beam_aim(b); break; case BeamType::SLASHING: @@ -2916,6 +2918,9 @@ void beam_aim(beam *b) // after pointing, jitter based on shot_aim (if we have a target object) if (!(b->flags & BF_TARGETING_COORDS)) { + if (Weapon_info[b->weapon_info_index].b_info.flags[Weapon::Beam_Info_Flags::Direct_fire_lead_target]) + b->last_shot += b->target->phys_info.vel * ((float)Weapon_info[b->weapon_info_index].b_info.beam_warmup * 0.001f); + beam_jitter_aim(b, b->binfo.shot_aim[b->shot_index]); } } diff --git a/code/weapon/weapon_flags.h b/code/weapon/weapon_flags.h index 9104cfb60ad..40ddfe294aa 100644 --- a/code/weapon/weapon_flags.h +++ b/code/weapon/weapon_flags.h @@ -145,6 +145,7 @@ namespace Weapon { FLAG_LIST(Beam_Info_Flags) { Burst_share_random, Track_own_texture_tiling, + Direct_fire_lead_target, NUM_VALUES }; diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 76ab37b09df..00c0281d424 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -125,7 +125,8 @@ const size_t Num_burst_fire_flags = sizeof(Burst_fire_flags)/sizeof(flag_def_lis flag_def_list_new Beam_info_flags[] = { { "burst shares random target", Weapon::Beam_Info_Flags::Burst_share_random, true, false }, - { "track own texture tiling", Weapon::Beam_Info_Flags::Track_own_texture_tiling, true, false } + { "track own texture tiling", Weapon::Beam_Info_Flags::Track_own_texture_tiling, true, false }, + { "direct fire lead target", Weapon::Beam_Info_Flags::Direct_fire_lead_target, true, false } }; const size_t Num_beam_info_flags = sizeof(Beam_info_flags) / sizeof(flag_def_list_new); From 90e1f18fa229e80562577780fdeead70f824e3cd Mon Sep 17 00:00:00 2001 From: Asteroth Date: Sun, 31 Aug 2025 10:01:31 -0400 Subject: [PATCH 447/466] add recoil for secondaries (#7007) --- code/ship/ship.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 1654896251e..786f9d2ce50 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -14470,6 +14470,14 @@ int ship_fire_secondary( object *obj, int allow_swarm, bool rollback_shot ) shipp->weapon_energy -= wip->energy_consumed; } + + if (wip->wi_flags[Weapon::Info_Flags::Apply_Recoil]) { + float recoil_force = (wip->mass * wip->max_speed * wip->recoil_modifier * sip->ship_recoil_modifier); + + vec3d impulse = firing_orient.vec.fvec * -recoil_force; + + ship_apply_whack(&impulse, &firing_pos, obj); + } } } } From b7aa53dd79de54577eaebb46dfba9864bf929780 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 1 Sep 2025 18:54:15 -0500 Subject: [PATCH 448/466] address feedback and fix some ui issues --- .../dialogs/BackgroundEditorDialogModel.cpp | 15 +++------------ qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp | 6 +++++- qtfred/src/ui/dialogs/BackgroundEditorDialog.h | 2 +- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp index d75f4c3e075..eb9739258c4 100644 --- a/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -1218,10 +1218,7 @@ int BackgroundEditorDialogModel::getSkyboxPitch() angles a; vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); int d = static_cast(fl2ir(fl_degrees(a.p))); - if (d < 0) - d += 360; - if (d >= 360) - d -= 360; + d = (d % 360 + 360) % 360; // wrap to [0, 359] return d; } @@ -1244,10 +1241,7 @@ int BackgroundEditorDialogModel::getSkyboxBank() angles a; vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); int d = static_cast(fl2ir(fl_degrees(a.b))); - if (d < 0) - d += 360; - if (d >= 360) - d -= 360; + d = (d % 360 + 360) % 360; // wrap to [0, 359] return d; } @@ -1270,10 +1264,7 @@ int BackgroundEditorDialogModel::getSkyboxHeading() angles a; vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); int d = static_cast(fl2ir(fl_degrees(a.h))); - if (d < 0) - d += 360; - if (d >= 360) - d -= 360; + d = (d % 360 + 360) % 360; // wrap to [0, 359] return d; } diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp index bfebd5b45ca..74c890af53c 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp @@ -40,6 +40,9 @@ void BackgroundEditorDialog::initializeUi() ui->bitmapScaleYDoubleSpinBox->setRange(_model->getBitmapScaleLimit().first, _model->getBitmapScaleLimit().second); ui->bitmapDivXSpinBox->setRange(_model->getDivisionLimit().first, _model->getDivisionLimit().second); ui->bitmapDivYSpinBox->setRange(_model->getDivisionLimit().first, _model->getDivisionLimit().second); + ui->skyboxPitchSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); + ui->skyboxBankSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); + ui->skyboxHeadingSpin->setRange(_model->getOrientLimit().first, _model->getOrientLimit().second); const auto& names = _model->getAvailableBitmapNames(); @@ -836,7 +839,7 @@ void BackgroundEditorDialog::updateAmbientSwatch() .arg(b)); } -void BackgroundEditorDialog::on_skyboxButton_clicked() +void BackgroundEditorDialog::on_skyboxModelButton_clicked() { QSettings settings("QtFRED", "BackgroundEditor"); const QString lastDir = settings.value("skybox/lastDir", QDir::homePath()).toString(); @@ -858,6 +861,7 @@ void BackgroundEditorDialog::on_skyboxButton_clicked() void BackgroundEditorDialog::on_skyboxEdit_textChanged(const QString& arg1) { _model->setSkyboxModelName(arg1.toUtf8().constData()); + updateSkyboxControls(); } void BackgroundEditorDialog::on_skyboxPitchSpin_valueChanged(int arg1) diff --git a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h index b89eb779fbb..60a8d526fed 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -81,7 +81,7 @@ private slots: void on_ambientLightBlueSlider_valueChanged(int value); // Skybox - void on_skyboxButton_clicked(); + void on_skyboxModelButton_clicked(); void on_skyboxEdit_textChanged(const QString& arg1); void on_skyboxPitchSpin_valueChanged(int arg1); void on_skyboxBankSpin_valueChanged(int arg1); From 7a75aaa172212f1c16a7f5d7d0b67e60f4788a97 Mon Sep 17 00:00:00 2001 From: Salvador Cipolla Date: Tue, 2 Sep 2025 19:17:16 -0300 Subject: [PATCH 449/466] Check cmdline_mod for nullptr before using it --- code/hud/hudconfig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/hud/hudconfig.cpp b/code/hud/hudconfig.cpp index 7d008b7f5ab..739f473af53 100644 --- a/code/hud/hudconfig.cpp +++ b/code/hud/hudconfig.cpp @@ -2035,7 +2035,7 @@ SCP_string create_custom_gauge_id(const SCP_string& gauge_name) { Assertion(!gauge_name.empty(), "Custom gauge has no name!"); SCP_string id; - if (Mod_title.empty()) { + if (Mod_title.empty() && Cmdline_mod != nullptr) { id = Cmdline_mod; // Basic cleanup attempt From 2820532ab30c66f03e6a1d84ac2b09d784d0e728 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Tue, 2 Sep 2025 22:55:03 -0400 Subject: [PATCH 450/466] Expose value of idle circling with 'attack-any' (#7011) * Expose value of idle circling with 'attack-any' The attack-any order is extremely useful and utilized order, and there was one aspect that was ripe for exposure to modders: the `CHASE_CIRCLE_DIST` that was used to determine how big of a circle to fly around while the AI waited for new hostiles to arrive. For ships with high speeds, like FotG and others, the default radius of 200 m was a bit on the small side, which lead to ships flying in tight circles. This was was counterproductive to goals of tuning the AI to have a more cinematic style, such as where AI would fly around in large distances while waiting for enemies to arrive. Fortunately, this value is easy to expose and allow modders to tune with just a new ai_profiles value. Tested and works as expected. * improve name and use correct distance --- code/ai/ai_profiles.cpp | 5 +++++ code/ai/ai_profiles.h | 3 +++ code/ai/aicode.cpp | 6 ++---- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/code/ai/ai_profiles.cpp b/code/ai/ai_profiles.cpp index ebb6f15d221..9dab5b92fdf 100644 --- a/code/ai/ai_profiles.cpp +++ b/code/ai/ai_profiles.cpp @@ -699,6 +699,10 @@ void parse_ai_profiles_tbl(const char *filename) } } + if (optional_string("$attack-any idle circle distance:")) { + stuff_float(&profile->attack_any_idle_circle_distance); + } + set_flag(profile, "$unify usage of AI Shield Manage Delay:", AI::Profile_Flags::Unify_usage_ai_shield_manage_delay); set_flag(profile, "$fix AI shield management bug:", AI::Profile_Flags::Fix_AI_shield_management_bug); @@ -815,6 +819,7 @@ void ai_profile_t::reset() guard_big_orbit_above_target_radius = 500.0f; guard_big_orbit_max_speed_percent = 1.0f; + attack_any_idle_circle_distance = 100.0f; for (int i = 0; i < NUM_SKILL_LEVELS; ++i) { max_incoming_asteroids[i] = 0; diff --git a/code/ai/ai_profiles.h b/code/ai/ai_profiles.h index dbdd9b82d88..23e9f67fccc 100644 --- a/code/ai/ai_profiles.h +++ b/code/ai/ai_profiles.h @@ -144,6 +144,9 @@ class ai_profile_t { float guard_big_orbit_above_target_radius; // Radius of guardee that triggers ai_big_guard() float guard_big_orbit_max_speed_percent; // Max percent of forward speed that is used in ai_big_guard() + // AI attack any option --wookieejedi + float attack_any_idle_circle_distance; // Radius that AI circles around a point while waiting for new enemies in attack-any mode + void reset(); }; diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index e10342bb0d3..8b09ed94218 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -13244,8 +13244,6 @@ static void ai_preprocess_ignore_objnum(ai_info *aip) aip->target_objnum = -1; } -#define CHASE_CIRCLE_DIST 100.0f - void ai_chase_circle(object *objp) { float dist_to_goal; @@ -13266,11 +13264,11 @@ void ai_chase_circle(object *objp) if (aip->ignore_objnum == UNUSED_OBJNUM) { dist_to_goal = vm_vec_dist_quick(&aip->goal_point, &objp->pos); - if (dist_to_goal > 2*CHASE_CIRCLE_DIST) { + if (dist_to_goal > 2 * The_mission.ai_profile->attack_any_idle_circle_distance) { vec3d vec_to_goal; // Too far from circle goal, create a new goal point. vm_vec_normalized_dir(&vec_to_goal, &aip->goal_point, &objp->pos); - vm_vec_scale_add(&aip->goal_point, &objp->pos, &vec_to_goal, CHASE_CIRCLE_DIST); + vm_vec_scale_add(&aip->goal_point, &objp->pos, &vec_to_goal, The_mission.ai_profile->attack_any_idle_circle_distance); } goal_point = aip->goal_point; From 5848df31dd050bd2824bf4e9faf1f2e17ef06f93 Mon Sep 17 00:00:00 2001 From: Mara Huldra Date: Tue, 2 Sep 2025 15:02:01 +0000 Subject: [PATCH 451/466] Fix build issues with Vulkan SDK 1.3.301+ This fixes two build issues: - `vk::PFN_DebugReportCallbackEXT` takes `vk::DebugReport*EXT` not C type `VkDebugReport*EXT` since version 1.3.304. (KhronosGroup/Vulkan-Hpp@d5a18dc87efbcc291bbd50a892187af4cbb60285) - `vk::ObjectDestroy` was moved to namespace `vk::detail` in version 1.3.301. (KhronosGroup/Vulkan-Hpp@6e5489fcd96ba1e2cf994ebe3e4d1f5f4f690f4b) Guard them with version checks, so that compiling with older verions of Vulkan keeps working. Closes #6416. --- code/graphics/vulkan/VulkanRenderer.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/code/graphics/vulkan/VulkanRenderer.cpp b/code/graphics/vulkan/VulkanRenderer.cpp index 8de061e39db..50b6da7e220 100644 --- a/code/graphics/vulkan/VulkanRenderer.cpp +++ b/code/graphics/vulkan/VulkanRenderer.cpp @@ -25,8 +25,14 @@ const char* EngineName = "FreeSpaceOpen"; const gameversion::version MinVulkanVersion(1, 1, 0, 0); -VkBool32 VKAPI_PTR debugReportCallback(VkDebugReportFlagsEXT /*flags*/, +VkBool32 VKAPI_PTR debugReportCallback( +#if VK_HEADER_VERSION >= 304 + vk::DebugReportFlagsEXT /*flags*/, + vk::DebugReportObjectTypeEXT /*objectType*/, +#else + VkDebugReportFlagsEXT /*flags*/, VkDebugReportObjectTypeEXT /*objectType*/, +#endif uint64_t /*object*/, size_t /*location*/, int32_t /*messageCode*/, @@ -457,7 +463,11 @@ bool VulkanRenderer::initializeSurface() return false; } +#if VK_HEADER_VERSION >= 301 + const vk::detail::ObjectDestroy deleter(*m_vkInstance, +#else const vk::ObjectDestroy deleter(*m_vkInstance, +#endif nullptr, VULKAN_HPP_DEFAULT_DISPATCHER); m_vkSurface = vk::UniqueSurfaceKHR(vk::SurfaceKHR(surface), deleter); From f88e5c475e9c11fa600334c483b31bfe40eba63b Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 3 Sep 2025 22:04:48 -0400 Subject: [PATCH 452/466] modular table fix for cinematic warp Small and subtle fix: move the CINEMATIC options inside the preceding if() block in the same way that the flare style options are inside their preceding if() block. This prevents the CINEMATIC options from being reset to their default values in subsequent modular table parsing. Followup to #6500. --- code/fireball/fireballs.cpp | 77 +++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/code/fireball/fireballs.cpp b/code/fireball/fireballs.cpp index 7783a612fe4..9733fdb1f37 100644 --- a/code/fireball/fireballs.cpp +++ b/code/fireball/fireballs.cpp @@ -437,47 +437,48 @@ static void parse_fireball_tbl(const char *table_filename) default: error_display(0, "Invalid warp model style. Must be classic or cinematic."); } - } else if (first_time) { - fi->warp_model_style = warp_style::CLASSIC; - } - - // Set warp_model_style options if cinematic style is chosen - if (fi->warp_model_style == warp_style::CINEMATIC) { - if (optional_string("+Warp size ratio:")) { - stuff_float(&fi->warp_size_ratio); - } else { - fi->warp_size_ratio = 1.6f; - } - // The first two values need to be implied multiples of PI - // for convenience. These shouldn't need to be faster than a full - // rotation per second, which is already ridiculous. - if (optional_string("+Rotation anim:")) { - stuff_float_list(fi->rot_anim, 3); - - CLAMP(fi->rot_anim[0], 0.0f, 2.0f); - CLAMP(fi->rot_anim[1], 0.0f, 2.0f); - fi->rot_anim[2] = MAX(0.0f, fi->rot_anim[2]); - } else { - // PI / 2.75f, PI / 10.0f, 2.0f - fi->rot_anim[0] = 0.365f; - fi->rot_anim[1] = 0.083f; - fi->rot_anim[2] = 2.0f; + // Set warp_model_style options if cinematic style is chosen + if (fi->warp_model_style == warp_style::CINEMATIC) { + if (optional_string("+Warp size ratio:")) { + stuff_float(&fi->warp_size_ratio); + } else { + fi->warp_size_ratio = 1.6f; + } + + // The first two values need to be implied multiples of PI + // for convenience. These shouldn't need to be faster than a full + // rotation per second, which is already ridiculous. + if (optional_string("+Rotation anim:")) { + stuff_float_list(fi->rot_anim, 3); + + CLAMP(fi->rot_anim[0], 0.0f, 2.0f); + CLAMP(fi->rot_anim[1], 0.0f, 2.0f); + fi->rot_anim[2] = MAX(0.0f, fi->rot_anim[2]); + } else { + // PI / 2.75f, PI / 10.0f, 2.0f + fi->rot_anim[0] = 0.365f; + fi->rot_anim[1] = 0.083f; + fi->rot_anim[2] = 2.0f; + } + + // Variable frame rate for faster propagation of ripples + if (optional_string("+Frame anim:")) { + stuff_float_list(fi->frame_anim, 3); + + // A frame rate that is 4 times the normal speed is ridiculous + CLAMP(fi->frame_anim[0], 0.0f, 4.0f); + CLAMP(fi->frame_anim[1], 1.0f, 4.0f); + fi->frame_anim[2] = MAX(0.0f, fi->frame_anim[2]); + } else { + fi->frame_anim[0] = 1.0f; + fi->frame_anim[1] = 1.0f; + fi->frame_anim[2] = 3.0f; + } } - // Variable frame rate for faster propagation of ripples - if (optional_string("+Frame anim:")) { - stuff_float_list(fi->frame_anim, 3); - - // A frame rate that is 4 times the normal speed is ridiculous - CLAMP(fi->frame_anim[0], 0.0f, 4.0f); - CLAMP(fi->frame_anim[1], 1.0f, 4.0f); - fi->frame_anim[2] = MAX(0.0f, fi->frame_anim[2]); - } else { - fi->frame_anim[0] = 1.0f; - fi->frame_anim[1] = 1.0f; - fi->frame_anim[2] = 3.0f; - } + } else if (first_time) { + fi->warp_model_style = warp_style::CLASSIC; } } From f2bc71c13a58f2748ad29751f7eb533570333ed2 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Wed, 3 Sep 2025 22:45:31 -0400 Subject: [PATCH 453/466] small optimization --- code/fireball/fireballs.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/code/fireball/fireballs.cpp b/code/fireball/fireballs.cpp index 9733fdb1f37..10b6c7aceed 100644 --- a/code/fireball/fireballs.cpp +++ b/code/fireball/fireballs.cpp @@ -1151,12 +1151,12 @@ static float cutscene_wormhole(float t) { float fireball_wormhole_intensity(fireball *fb) { float t = fb->time_elapsed; - - float rad = cutscene_wormhole(t / fb->warp_open_duration); + float rad; fireball_info* fi = &Fireball_info[fb->fireball_info_index]; if (fi->warp_model_style == warp_style::CINEMATIC) { + rad = cutscene_wormhole(t / fb->warp_open_duration); rad *= cutscene_wormhole((fb->total_time - t) / fb->warp_close_duration); rad /= cutscene_wormhole(fb->total_time / (2.0f * fb->warp_open_duration)); rad /= cutscene_wormhole(fb->total_time / (2.0f * fb->warp_close_duration)); From 31d73b7ed5161b2604cc9e85fcfd33ae156d90a1 Mon Sep 17 00:00:00 2001 From: combatlombax <78774622+combatlombax@users.noreply.github.com> Date: Fri, 5 Sep 2025 16:00:06 -0600 Subject: [PATCH 454/466] Removing block that prevented volumetric and background nebulae from rendering simultaneously (#6853) * Removing block to allow volumetric and background nebulas to render at the same time * Updating code from feedback on pull request #6853 * What the... did I miss a line? * Dual fog layering should now work when both nebula types are active --- .gitignore | 2 ++ code/graphics/opengl/gropengldeferred.cpp | 30 ++++++++++++++++++++--- code/nebula/neb.cpp | 23 +++++++++++------ 3 files changed, 44 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 045ac3d3802..f0111823959 100644 --- a/.gitignore +++ b/.gitignore @@ -260,3 +260,5 @@ CMakePresets.json !/code/scripting/lua/LuaConvert.h !/code/debugconsole/ !/docker/build +CMakeFiles/cmake.check_cache +CMakeCache.txt diff --git a/code/graphics/opengl/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp index 6c3ef67ca49..b3acf8faf84 100644 --- a/code/graphics/opengl/gropengldeferred.cpp +++ b/code/graphics/opengl/gropengldeferred.cpp @@ -529,7 +529,10 @@ void gr_opengl_deferred_lighting_finish() // Now reset back to drawing into the color buffer glDrawBuffer(GL_COLOR_ATTACHMENT0); - if (The_mission.flags[Mission::Mission_Flags::Fullneb] && Neb2_render_mode != NEB2_RENDER_NONE && !override_fog) { + bool bDrawFullNeb = The_mission.flags[Mission::Mission_Flags::Fullneb] && Neb2_render_mode != NEB2_RENDER_NONE && !override_fog; + bool bDrawNebVolumetrics = The_mission.volumetrics && The_mission.volumetrics->get_enabled() && !override_fog; + + if (bDrawFullNeb) { GL_state.SetAlphaBlendMode(ALPHA_BLEND_NONE); gr_zbuffer_set(GR_ZBUFF_NONE); opengl_shader_set_current(gr_opengl_maybe_create_shader(SDR_TYPE_SCENE_FOG, 0)); @@ -556,8 +559,26 @@ void gr_opengl_deferred_lighting_finish() }); opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f); - } - else if (The_mission.volumetrics && The_mission.volumetrics->get_enabled() && !override_fog) { + + if (bDrawNebVolumetrics) { + glReadBuffer(GL_COLOR_ATTACHMENT0); + glDrawBuffer(GL_COLOR_ATTACHMENT5); + glBlitFramebuffer(0, + 0, + gr_screen.max_w, + gr_screen.max_h, + 0, + 0, + gr_screen.max_w, + gr_screen.max_h, + GL_COLOR_BUFFER_BIT, + GL_NEAREST); + glDrawBuffer(GL_COLOR_ATTACHMENT0); + glReadBuffer(GL_COLOR_ATTACHMENT0); + } + + } + if (bDrawNebVolumetrics) { GR_DEBUG_SCOPE("Volumetric Nebulae"); TRACE_SCOPE(tracing::Volumetrics); @@ -637,7 +658,8 @@ void gr_opengl_deferred_lighting_finish() gr_end_view_matrix(); gr_end_proj_matrix(); } - else { + + if(!bDrawFullNeb && !bDrawNebVolumetrics) { // Transfer the resolved lighting back to the color texture // TODO: Maybe this could be improved so that it doesn't require the copy back operation? glReadBuffer(GL_COLOR_ATTACHMENT5); diff --git a/code/nebula/neb.cpp b/code/nebula/neb.cpp index 4d8a8ba670f..4d49ee0a6a0 100644 --- a/code/nebula/neb.cpp +++ b/code/nebula/neb.cpp @@ -1143,15 +1143,24 @@ float neb2_get_fog_visibility(const vec3d *pos, float distance_mult) } bool nebula_handle_alpha(float& alpha, const vec3d* pos, float distance_mult) { - if (The_mission.flags[Mission::Mission_Flags::Fullneb]) { - alpha *= neb2_get_fog_visibility(pos, distance_mult); - return true; + + bool bHasNebula = false; + float fAlphaMult = 1.0f; + + if (The_mission.flags[Mission::Mission_Flags::Fullneb]) + { + fAlphaMult *= neb2_get_fog_visibility(pos, distance_mult); + bHasNebula = true; } - else if (The_mission.volumetrics) { - alpha *= The_mission.volumetrics->getAlphaToPos(*pos, distance_mult); - return true; + + if (The_mission.volumetrics) + { + fAlphaMult *= The_mission.volumetrics->getAlphaToPos(*pos, distance_mult); + bHasNebula = true; } - return false; + + alpha *= fAlphaMult; + return bHasNebula; } // fogging stuff -------------------------------------------------------------------- From 877373f25796bc8c33ed6297c26086acd4a33383 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Sat, 6 Sep 2025 00:31:55 -0400 Subject: [PATCH 455/466] Fix for modern Thruster Particles (#7021) Allows `$Thruster Effect:` by fixing copy-paste bug. Tested and fix confirms to work. --- code/ship/ship.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 786f9d2ce50..b4f9dbc9cdc 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -4976,7 +4976,7 @@ static void parse_ship_values(ship_info* sip, const bool is_template, const bool else if ( optional_string("$Afterburner Particle Bitmap:") ) afterburner = true; else if ( optional_string("$Thruster Effect:") ) { - afterburner = true; + afterburner = false; modern_particle = true; } else if ( optional_string("$Afterburner Effect:") ) { From fc102056b853aa60c9209eaee96316fa449e207d Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 6 Sep 2025 22:32:07 -0400 Subject: [PATCH 456/466] make legacy particle spews more robust (#7023) If a pspew specifies a single-frame image, be sure that it is loaded correctly. Fixes a particle crash in Inferno. --- code/weapon/weapons.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/weapon/weapons.cpp b/code/weapon/weapons.cpp index 00c0281d424..c5c90ecccef 100644 --- a/code/weapon/weapons.cpp +++ b/code/weapon/weapons.cpp @@ -776,7 +776,7 @@ static particle::ParticleEffectHandle convertLegacyPspewBuffer(const pspew_legac IS_VEC_NULL(&pspew_buffer.particle_spew_offset) ? std::nullopt : std::optional(pspew_buffer.particle_spew_offset), //Local offset ::util::UniformFloatRange(pspew_buffer.particle_spew_lifetime), //Lifetime ::util::UniformFloatRange(pspew_buffer.particle_spew_radius), //Radius - hasAnim ? bm_load_animation(pspew_buffer.particle_spew_anim.c_str()) : particle::Anim_bitmap_id_smoke)); //Bitmap + hasAnim ? bm_load_either(pspew_buffer.particle_spew_anim.c_str()) : particle::Anim_bitmap_id_smoke)); //Bitmap or Anim } /** From 0ee8c4b01ac5717a09490a19610291bb4f549804 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 7 Sep 2025 23:23:02 -0400 Subject: [PATCH 457/466] restore double quote conversion in event editor The double quote conversions added in #7008 were removed in #6961, probably inadvertently due to the branch merge. This adds them back. --- qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp index 3b22f04af11..022bad69e72 100644 --- a/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp @@ -865,6 +865,7 @@ void MissionEventsDialogModel::setEventDirectiveText(const SCP_string& text) } auto& event = m_events[m_cur_event]; modify(event.objective_text, text); + lcl_fred_replace_stuff(event.objective_text); } SCP_string MissionEventsDialogModel::getEventDirectiveKeyText() const @@ -882,6 +883,7 @@ void MissionEventsDialogModel::setEventDirectiveKeyText(const SCP_string& text) } auto& event = m_events[m_cur_event]; modify(event.objective_key_text, text); + lcl_fred_replace_stuff(event.objective_key_text); } bool MissionEventsDialogModel::getLogTrue() const From 9d3ba25606f309ab4fc10e0dd612b53bdbc86ee7 Mon Sep 17 00:00:00 2001 From: Taylor Richards Date: Mon, 8 Sep 2025 21:49:48 -0400 Subject: [PATCH 458/466] fix various issues with DDS decompression (#7022) Fix heap corruption with smaller mipmaps due to decompression output always being a 4x4 block rather than actual mipmap size. It's necessary to pad the allocated data size to allow for the overflow. Fix heap corruption with DXT1 due to using improper data offsets for the type. Fix data offsets not always taking the depth value into account. Adjust code for slight increase in decode performance. Update bcdec to 0.98 --- code/ddsutils/bcdec.h | 220 ++++++++++++++++++++++++++++++++----- code/ddsutils/ddsutils.cpp | 78 +++++++++---- 2 files changed, 247 insertions(+), 51 deletions(-) diff --git a/code/ddsutils/bcdec.h b/code/ddsutils/bcdec.h index 3b2884c8309..4d9e38ca7ce 100644 --- a/code/ddsutils/bcdec.h +++ b/code/ddsutils/bcdec.h @@ -1,4 +1,4 @@ -/* bcdec.h - v0.96 +/* bcdec.h - v0.97 provides functions to decompress blocks of BC compressed images written by Sergii "iOrange" Kudlai in 2022 @@ -23,6 +23,10 @@ For more info, issues and suggestions please visit https://github.com/iOrange/bcdec + Configuration: + #define BCDEC_BC4BC5_PRECISE: + enables more precise but slower BC4/BC5 decoding + signed/unsigned mode + CREDITS: Aras Pranckevicius (@aras-p) - BC1/BC3 decoders optimizations (up to 3x the speed) - BC6H/BC7 bits pulling routines optimizations @@ -30,6 +34,11 @@ - Split BC6H decompression function into 'half' and 'float' variants + Michael Schmidt (@RunDevelopment) - Found better "magic" coefficients for integer interpolation + of reference colors in BC1 color block, that match with + the floating point interpolation. This also made it faster + than integer division by 3! + bugfixes: @linkmauve @@ -39,6 +48,9 @@ #ifndef BCDEC_HEADER_INCLUDED #define BCDEC_HEADER_INCLUDED +#define BCDEC_VERSION_MAJOR 0 +#define BCDEC_VERSION_MINOR 98 + /* if BCDEC_STATIC causes problems, try defining BCDECDEF to 'inline' or 'static inline' */ #ifndef BCDECDEF #ifdef BCDEC_STATIC @@ -90,12 +102,20 @@ BCDECDEF void bcdec_bc1(const void* compressedBlock, void* decompressedBlock, int destinationPitch); BCDECDEF void bcdec_bc2(const void* compressedBlock, void* decompressedBlock, int destinationPitch); BCDECDEF void bcdec_bc3(const void* compressedBlock, void* decompressedBlock, int destinationPitch); +#ifndef BCDEC_BC4BC5_PRECISE BCDECDEF void bcdec_bc4(const void* compressedBlock, void* decompressedBlock, int destinationPitch); BCDECDEF void bcdec_bc5(const void* compressedBlock, void* decompressedBlock, int destinationPitch); +#else +BCDECDEF void bcdec_bc4(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); +BCDECDEF void bcdec_bc5(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); +BCDECDEF void bcdec_bc4_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); +BCDECDEF void bcdec_bc5_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); +#endif BCDECDEF void bcdec_bc6h_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); BCDECDEF void bcdec_bc6h_half(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned); BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, int destinationPitch); +#endif /* BCDEC_HEADER_INCLUDED */ #ifdef BCDEC_IMPLEMENTATION @@ -110,35 +130,44 @@ static void bcdec__color_block(const void* compressedBlock, void* decompressedBl c0 = ((unsigned short*)compressedBlock)[0]; c1 = ((unsigned short*)compressedBlock)[1]; + /* Unpack 565 ref colors */ + r0 = (c0 >> 11) & 0x1F; + g0 = (c0 >> 5) & 0x3F; + b0 = c0 & 0x1F; + + r1 = (c1 >> 11) & 0x1F; + g1 = (c1 >> 5) & 0x3F; + b1 = c1 & 0x1F; + /* Expand 565 ref colors to 888 */ - r0 = (((c0 >> 11) & 0x1F) * 527 + 23) >> 6; - g0 = (((c0 >> 5) & 0x3F) * 259 + 33) >> 6; - b0 = ((c0 & 0x1F) * 527 + 23) >> 6; - refColors[0] = 0xFF000000 | (b0 << 16) | (g0 << 8) | r0; + r = (r0 * 527 + 23) >> 6; + g = (g0 * 259 + 33) >> 6; + b = (b0 * 527 + 23) >> 6; + refColors[0] = 0xFF000000 | (b << 16) | (g << 8) | r; - r1 = (((c1 >> 11) & 0x1F) * 527 + 23) >> 6; - g1 = (((c1 >> 5) & 0x3F) * 259 + 33) >> 6; - b1 = ((c1 & 0x1F) * 527 + 23) >> 6; - refColors[1] = 0xFF000000 | (b1 << 16) | (g1 << 8) | r1; + r = (r1 * 527 + 23) >> 6; + g = (g1 * 259 + 33) >> 6; + b = (b1 * 527 + 23) >> 6; + refColors[1] = 0xFF000000 | (b << 16) | (g << 8) | r; if (c0 > c1 || onlyOpaqueMode) { /* Standard BC1 mode (also BC3 color block uses ONLY this mode) */ /* color_2 = 2/3*color_0 + 1/3*color_1 color_3 = 1/3*color_0 + 2/3*color_1 */ - r = (2 * r0 + r1 + 1) / 3; - g = (2 * g0 + g1 + 1) / 3; - b = (2 * b0 + b1 + 1) / 3; + r = ((2 * r0 + r1) * 351 + 61) >> 7; + g = ((2 * g0 + g1) * 2763 + 1039) >> 11; + b = ((2 * b0 + b1) * 351 + 61) >> 7; refColors[2] = 0xFF000000 | (b << 16) | (g << 8) | r; - r = (r0 + 2 * r1 + 1) / 3; - g = (g0 + 2 * g1 + 1) / 3; - b = (b0 + 2 * b1 + 1) / 3; + r = ((r0 + r1 * 2) * 351 + 61) >> 7; + g = ((g0 + g1 * 2) * 2763 + 1039) >> 11; + b = ((b0 + b1 * 2) * 351 + 61) >> 7; refColors[3] = 0xFF000000 | (b << 16) | (g << 8) | r; } else { /* Quite rare BC1A mode */ /* color_2 = 1/2*color_0 + 1/2*color_1; color_3 = 0; */ - r = (r0 + r1 + 1) >> 1; - g = (g0 + g1 + 1) >> 1; - b = (b0 + b1 + 1) >> 1; + r = ((r0 + r1) * 1053 + 125) >> 8; + g = ((g0 + g1) * 4145 + 1019) >> 11; + b = ((b0 + b1) * 1053 + 125) >> 8; refColors[2] = 0xFF000000 | (b << 16) | (g << 8) | r; refColors[3] = 0x00000000; @@ -190,19 +219,19 @@ static void bcdec__smooth_alpha_block(const void* compressedBlock, void* decompr if (alpha[0] > alpha[1]) { /* 6 interpolated alpha values. */ - alpha[2] = (6 * alpha[0] + alpha[1] + 1) / 7; /* 6/7*alpha_0 + 1/7*alpha_1 */ - alpha[3] = (5 * alpha[0] + 2 * alpha[1] + 1) / 7; /* 5/7*alpha_0 + 2/7*alpha_1 */ - alpha[4] = (4 * alpha[0] + 3 * alpha[1] + 1) / 7; /* 4/7*alpha_0 + 3/7*alpha_1 */ - alpha[5] = (3 * alpha[0] + 4 * alpha[1] + 1) / 7; /* 3/7*alpha_0 + 4/7*alpha_1 */ - alpha[6] = (2 * alpha[0] + 5 * alpha[1] + 1) / 7; /* 2/7*alpha_0 + 5/7*alpha_1 */ - alpha[7] = ( alpha[0] + 6 * alpha[1] + 1) / 7; /* 1/7*alpha_0 + 6/7*alpha_1 */ + alpha[2] = (6 * alpha[0] + alpha[1]) / 7; /* 6/7*alpha_0 + 1/7*alpha_1 */ + alpha[3] = (5 * alpha[0] + 2 * alpha[1]) / 7; /* 5/7*alpha_0 + 2/7*alpha_1 */ + alpha[4] = (4 * alpha[0] + 3 * alpha[1]) / 7; /* 4/7*alpha_0 + 3/7*alpha_1 */ + alpha[5] = (3 * alpha[0] + 4 * alpha[1]) / 7; /* 3/7*alpha_0 + 4/7*alpha_1 */ + alpha[6] = (2 * alpha[0] + 5 * alpha[1]) / 7; /* 2/7*alpha_0 + 5/7*alpha_1 */ + alpha[7] = ( alpha[0] + 6 * alpha[1]) / 7; /* 1/7*alpha_0 + 6/7*alpha_1 */ } else { /* 4 interpolated alpha values. */ - alpha[2] = (4 * alpha[0] + alpha[1] + 1) / 5; /* 4/5*alpha_0 + 1/5*alpha_1 */ - alpha[3] = (3 * alpha[0] + 2 * alpha[1] + 1) / 5; /* 3/5*alpha_0 + 2/5*alpha_1 */ - alpha[4] = (2 * alpha[0] + 3 * alpha[1] + 1) / 5; /* 2/5*alpha_0 + 3/5*alpha_1 */ - alpha[5] = ( alpha[0] + 4 * alpha[1] + 1) / 5; /* 1/5*alpha_0 + 4/5*alpha_1 */ + alpha[2] = (4 * alpha[0] + alpha[1]) / 5; /* 4/5*alpha_0 + 1/5*alpha_1 */ + alpha[3] = (3 * alpha[0] + 2 * alpha[1]) / 5; /* 3/5*alpha_0 + 2/5*alpha_1 */ + alpha[4] = (2 * alpha[0] + 3 * alpha[1]) / 5; /* 2/5*alpha_0 + 3/5*alpha_1 */ + alpha[5] = ( alpha[0] + 4 * alpha[1]) / 5; /* 1/5*alpha_0 + 4/5*alpha_1 */ alpha[6] = 0x00; alpha[7] = 0xFF; } @@ -218,6 +247,117 @@ static void bcdec__smooth_alpha_block(const void* compressedBlock, void* decompr } } +#ifdef BCDEC_BC4BC5_PRECISE +static void bcdec__bc4_block(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int pixelSize, int isSigned) { + signed char* sblock; + unsigned char* ublock; + int alpha[8]; + int i, j; + unsigned long long block, indices; + + static int aWeights4[4] = { 13107, 26215, 39321, 52429 }; + static int aWeights6[6] = { 9363, 18724, 28086, 37450, 46812, 56173 }; + + block = *(unsigned long long*)compressedBlock; + + if (isSigned) { + alpha[0] = (char)(block & 0xFF); + alpha[1] = (char)((block >> 8) & 0xFF); + if (alpha[0] < -127) alpha[0] = -127; /* -128 clamps to -127 */ + if (alpha[1] < -127) alpha[1] = -127; /* -128 clamps to -127 */ + } else { + alpha[0] = block & 0xFF; + alpha[1] = (block >> 8) & 0xFF; + } + + if (alpha[0] > alpha[1]) { + /* 6 interpolated alpha values. */ + alpha[2] = (aWeights6[5] * alpha[0] + aWeights6[0] * alpha[1] + 32768) >> 16; /* 6/7*alpha_0 + 1/7*alpha_1 */ + alpha[3] = (aWeights6[4] * alpha[0] + aWeights6[1] * alpha[1] + 32768) >> 16; /* 5/7*alpha_0 + 2/7*alpha_1 */ + alpha[4] = (aWeights6[3] * alpha[0] + aWeights6[2] * alpha[1] + 32768) >> 16; /* 4/7*alpha_0 + 3/7*alpha_1 */ + alpha[5] = (aWeights6[2] * alpha[0] + aWeights6[3] * alpha[1] + 32768) >> 16; /* 3/7*alpha_0 + 4/7*alpha_1 */ + alpha[6] = (aWeights6[1] * alpha[0] + aWeights6[4] * alpha[1] + 32768) >> 16; /* 2/7*alpha_0 + 5/7*alpha_1 */ + alpha[7] = (aWeights6[0] * alpha[0] + aWeights6[5] * alpha[1] + 32768) >> 16; /* 1/7*alpha_0 + 6/7*alpha_1 */ + } else { + /* 4 interpolated alpha values. */ + alpha[2] = (aWeights4[3] * alpha[0] + aWeights4[0] * alpha[1] + 32768) >> 16; /* 4/5*alpha_0 + 1/5*alpha_1 */ + alpha[3] = (aWeights4[2] * alpha[0] + aWeights4[1] * alpha[1] + 32768) >> 16; /* 3/5*alpha_0 + 2/5*alpha_1 */ + alpha[4] = (aWeights4[1] * alpha[0] + aWeights4[2] * alpha[1] + 32768) >> 16; /* 2/5*alpha_0 + 3/5*alpha_1 */ + alpha[5] = (aWeights4[0] * alpha[0] + aWeights4[3] * alpha[1] + 32768) >> 16; /* 1/5*alpha_0 + 4/5*alpha_1 */ + alpha[6] = isSigned ? -127 : 0; + alpha[7] = isSigned ? 127 : 255; + } + + indices = block >> 16; + if (isSigned) { + sblock = (char*)decompressedBlock; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + sblock[j * pixelSize] = (char)alpha[indices & 0x07]; + indices >>= 3; + } + sblock += destinationPitch; + } + } else { + ublock = (unsigned char*)decompressedBlock; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + ublock[j * pixelSize] = (unsigned char)alpha[indices & 0x07]; + indices >>= 3; + } + ublock += destinationPitch; + } + } +} + +static void bcdec__bc4_block_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int pixelSize, int isSigned) { + float* decompressed; + float alpha[8]; + int i, j; + unsigned long long block, indices; + + block = *(unsigned long long*)compressedBlock; + decompressed = (float*)decompressedBlock; + + if (isSigned) { + alpha[0] = (float)((char)(block & 0xFF)) / 127.0f; + alpha[1] = (float)((char)((block >> 8) & 0xFF)) / 127.0f; + if (alpha[0] < -1.0f) alpha[0] = -1.0f; /* -128 clamps to -127 */ + if (alpha[1] < -1.0f) alpha[1] = -1.0f; /* -128 clamps to -127 */ + } else { + alpha[0] = (float)(block & 0xFF) / 255.0f; + alpha[1] = (float)((block >> 8) & 0xFF) / 255.0f; + } + + if (alpha[0] > alpha[1]) { + /* 6 interpolated alpha values. */ + alpha[2] = (6.0f * alpha[0] + alpha[1]) / 7.0f; /* 6/7*alpha_0 + 1/7*alpha_1 */ + alpha[3] = (5.0f * alpha[0] + 2.0f * alpha[1]) / 7.0f; /* 5/7*alpha_0 + 2/7*alpha_1 */ + alpha[4] = (4.0f * alpha[0] + 3.0f * alpha[1]) / 7.0f; /* 4/7*alpha_0 + 3/7*alpha_1 */ + alpha[5] = (3.0f * alpha[0] + 4.0f * alpha[1]) / 7.0f; /* 3/7*alpha_0 + 4/7*alpha_1 */ + alpha[6] = (2.0f * alpha[0] + 5.0f * alpha[1]) / 7.0f; /* 2/7*alpha_0 + 5/7*alpha_1 */ + alpha[7] = ( alpha[0] + 6.0f * alpha[1]) / 7.0f; /* 1/7*alpha_0 + 6/7*alpha_1 */ + } else { + /* 4 interpolated alpha values. */ + alpha[2] = (4.0f * alpha[0] + alpha[1]) / 5.0f; /* 4/5*alpha_0 + 1/5*alpha_1 */ + alpha[3] = (3.0f * alpha[0] + 2.0f * alpha[1]) / 5.0f; /* 3/5*alpha_0 + 2/5*alpha_1 */ + alpha[4] = (2.0f * alpha[0] + 3.0f * alpha[1]) / 5.0f; /* 2/5*alpha_0 + 3/5*alpha_1 */ + alpha[5] = ( alpha[0] + 4.0f * alpha[1]) / 5.0f; /* 1/5*alpha_0 + 4/5*alpha_1 */ + alpha[6] = isSigned ? -1.0f : 0.0f; + alpha[7] = 1.0f; + } + + indices = block >> 16; + for (i = 0; i < 4; ++i) { + for (j = 0; j < 4; ++j) { + decompressed[j * pixelSize] = alpha[indices & 0x07]; + indices >>= 3; + } + decompressed += destinationPitch; + } +} +#endif /* BCDEC_BC4BC5_PRECISE */ + typedef struct bcdec__bitstream { unsigned long long low; unsigned long long high; @@ -270,15 +410,37 @@ BCDECDEF void bcdec_bc3(const void* compressedBlock, void* decompressedBlock, in bcdec__smooth_alpha_block(compressedBlock, ((char*)decompressedBlock) + 3, destinationPitch, 4); } +#ifndef BCDEC_BC4BC5_PRECISE BCDECDEF void bcdec_bc4(const void* compressedBlock, void* decompressedBlock, int destinationPitch) { bcdec__smooth_alpha_block(compressedBlock, decompressedBlock, destinationPitch, 1); +#else +BCDECDEF void bcdec_bc4(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned) { + bcdec__bc4_block(compressedBlock, decompressedBlock, destinationPitch, 1, isSigned); +#endif } +#ifndef BCDEC_BC4BC5_PRECISE BCDECDEF void bcdec_bc5(const void* compressedBlock, void* decompressedBlock, int destinationPitch) { bcdec__smooth_alpha_block(compressedBlock, decompressedBlock, destinationPitch, 2); bcdec__smooth_alpha_block(((char*)compressedBlock) + 8, ((char*)decompressedBlock) + 1, destinationPitch, 2); +#else +BCDECDEF void bcdec_bc5(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned) { + bcdec__bc4_block(compressedBlock, decompressedBlock, destinationPitch, 2, isSigned); + bcdec__bc4_block(((char*)compressedBlock) + 8, ((char*)decompressedBlock) + 1, destinationPitch, 2, isSigned); +#endif +} + +#ifdef BCDEC_BC4BC5_PRECISE +BCDECDEF void bcdec_bc4_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned) { + bcdec__bc4_block_float(compressedBlock, decompressedBlock, destinationPitch, 1, isSigned); } +BCDECDEF void bcdec_bc5_float(const void* compressedBlock, void* decompressedBlock, int destinationPitch, int isSigned) { + bcdec__bc4_block_float(compressedBlock, decompressedBlock, destinationPitch, 2, isSigned); + bcdec__bc4_block_float(((char*)compressedBlock) + 8, ((float*)decompressedBlock) + 1, destinationPitch, 2, isSigned); +} +#endif /* BCDEC_BC4BC5_PRECISE */ + /* http://graphics.stanford.edu/~seander/bithacks.html#VariableSignExtend */ static int bcdec__extend_sign(int val, int bits) { return (val << (32 - bits)) >> (32 - bits); @@ -1269,8 +1431,6 @@ BCDECDEF void bcdec_bc7(const void* compressedBlock, void* decompressedBlock, in #endif /* BCDEC_IMPLEMENTATION */ -#endif /* BCDEC_HEADER_INCLUDED */ - /* LICENSE: This software is available under 2 licenses -- choose whichever you prefer. diff --git a/code/ddsutils/ddsutils.cpp b/code/ddsutils/ddsutils.cpp index 9f896048459..3f42e532dcc 100644 --- a/code/ddsutils/ddsutils.cpp +++ b/code/ddsutils/ddsutils.cpp @@ -96,6 +96,11 @@ static uint conversion_resize(DDS_HEADER &dds_header) // drop levels until we get to an appropriate size, but make sure we have // at least 1 mipmap level remaining at the end (in case there's not a full chain) while (((width > MAX_SIZE) || (height > MAX_SIZE)) && (offset < dds_header.dwMipMapCount-1)) { + // this shouldn't happen, but catch the obscure case (like 8192x4) + if ((width <= 4) || (height <= 4)) { + break; + } + width >>= 1; height >>= 1; depth >>= 1; @@ -194,9 +199,10 @@ static int _dds_read_header(CFILE *ddsfile, DDS_HEADER &dds_header, DDS_HEADER_D return DDS_ERROR_NONE; } -static size_t compute_dds_size(const DDS_HEADER &dds_header) +static size_t compute_dds_size(const DDS_HEADER &dds_header, bool converting = false) { - uint d_width, d_height, d_depth; + const uint block_sz = 4; + uint d_width = 0, d_height = 0, d_depth = 0; size_t d_size = 0; for (uint i = 0; i < dds_header.dwMipMapCount; i++) { @@ -204,6 +210,17 @@ static size_t compute_dds_size(const DDS_HEADER &dds_header) d_height = std::max(1U, dds_header.dwHeight >> i); d_depth = std::max(1U, dds_header.dwDepth >> i); + // When converting we need to pad a bit to compensate for the decompression + // size on smaller mipmap levels. We need to ensure there is always enough + // room to decode an entire 4x4 block in rgba space + if (converting) { + auto sz = std::min(d_width, d_height); + + if (sz < block_sz) { + d_size += ((block_sz * block_sz) - (sz * sz)) * d_depth * 4; + } + } + if (dds_header.ddspf.dwFlags & DDPF_FOURCC) { // size of data block (4x4) d_size += ((d_width + 3) / 4) * ((d_height + 3) / 4) * d_depth * ((dds_header.ddspf.dwFourCC == FOURCC_DXT1) ? 8 : 16); @@ -247,6 +264,7 @@ int dds_read_header(const char *filename, CFILE *img_cfp, int *width, int *heigh int retval = DDS_ERROR_NONE; int ct = DDS_UNCOMPRESSED; int is_cubemap = 0; + bool convert = false; if (img_cfp == NULL) { @@ -337,7 +355,9 @@ int dds_read_header(const char *filename, CFILE *img_cfp, int *width, int *heigh } // maybe do conversion if format not supported - if (conversion_needed(dds_header)) { + convert = conversion_needed(dds_header); + + if (convert) { // switch to uncompressed format and reset vars dds_header.ddspf.dwFlags &= ~DDPF_FOURCC; dds_header.ddspf.dwFlags |= DDPF_RGB; @@ -359,7 +379,7 @@ int dds_read_header(const char *filename, CFILE *img_cfp, int *width, int *heigh // stuff important info if (size) - *size = compute_dds_size(dds_header); + *size = compute_dds_size(dds_header, convert); if (bpp) *bpp = get_bit_count(dds_header); @@ -386,6 +406,9 @@ int dds_read_header(const char *filename, CFILE *img_cfp, int *width, int *heigh return retval; } +static void (*decompress_dds)(const void *in, void *out, int pitch) = nullptr; +static uint32_t BLOCK_SIZE = 0; + //reads pixel info from a dds file int dds_read_bitmap(const char *filename, ubyte *data, ubyte *bpp, int cf_type) { @@ -423,7 +446,7 @@ int dds_read_bitmap(const char *filename, ubyte *data, ubyte *bpp, int cf_type) cfseek(cfp, (dds_header.ddspf.dwFourCC == FOURCC_DX10) ? DX10_OFFSET : DDS_OFFSET, CF_SEEK_SET); - size = compute_dds_size(dds_header); + size = compute_dds_size(dds_header); // don't add padding on this one!! // read in the data if ( !conversion_needed(dds_header) ) { @@ -447,6 +470,28 @@ int dds_read_bitmap(const char *filename, ubyte *data, ubyte *bpp, int cf_type) const int num_faces = (dds_header.dwCaps2 & DDSCAPS2_CUBEMAP) ? 6 : 1; const bool has_depth = (dds_header.dwFlags & DDSD_DEPTH) == DDSD_DEPTH; + switch (dds_header.ddspf.dwFourCC) { + case FOURCC_DX10: + decompress_dds = bcdec_bc7; + BLOCK_SIZE = BCDEC_BC7_BLOCK_SIZE; + break; + case FOURCC_DXT5: + decompress_dds = bcdec_bc3; + BLOCK_SIZE = BCDEC_BC3_BLOCK_SIZE; + break; + case FOURCC_DXT1: + decompress_dds = bcdec_bc1; + BLOCK_SIZE = BCDEC_BC1_BLOCK_SIZE; + break; + case FOURCC_DXT3: + decompress_dds = bcdec_bc2; + BLOCK_SIZE = BCDEC_BC2_BLOCK_SIZE; + break; + default: + Error(LOCATION, "Invalid FourCC (%d) for DDS decompression!", dds_header.ddspf.dwFourCC); + break; + } + for (int f = 0; f < num_faces; ++f) { // if we resized then skip over all of that data (altering values to match pre-resize) for (uint x = 0; x < mipmap_offset; ++x) { @@ -454,7 +499,7 @@ int dds_read_bitmap(const char *filename, ubyte *data, ubyte *bpp, int cf_type) d_height = std::max(1U, dds_header.dwHeight << (mipmap_offset - x)); d_depth = has_depth ? std::max(1U, dds_header.dwDepth << (mipmap_offset - x)) : 1U; - src += (d_width * d_height * d_depth); + src += ((d_width + 3) / 4) * ((d_height + 3) / 4) * d_depth * BLOCK_SIZE; } for (uint m = mipmap_offset; m < dds_header.dwMipMapCount; ++m) { @@ -464,23 +509,14 @@ int dds_read_bitmap(const char *filename, ubyte *data, ubyte *bpp, int cf_type) d_depth = std::max(1U, dds_header.dwDepth >> (m - mipmap_offset)); for (uint d = 0; d < d_depth; ++d) { + auto depth_offset = d * d_width * d_height * 4; + for (uint i = 0; i < d_height; i += 4) { for (uint j = 0; j < d_width; j += 4) { - dst = data + data_offset + ((i * d_width + j) * 4); - - if (dds_header.ddspf.dwFourCC == FOURCC_DX10) { - bcdec_bc7(src, dst, d_width * 4); - src += BCDEC_BC7_BLOCK_SIZE; - } else if (dds_header.ddspf.dwFourCC == FOURCC_DXT5) { - bcdec_bc3(src, dst, d_width * 4); - src += BCDEC_BC3_BLOCK_SIZE; - } else if (dds_header.ddspf.dwFourCC == FOURCC_DXT1) { - bcdec_bc1(src, dst, d_width * 4); - src += BCDEC_BC1_BLOCK_SIZE; - } else if (dds_header.ddspf.dwFourCC == FOURCC_DXT3) { - bcdec_bc2(src, dst, d_width * 4); - src += BCDEC_BC2_BLOCK_SIZE; - } + dst = data + data_offset + depth_offset + ((i * d_width + j) * 4); + + decompress_dds(src, dst, d_width * 4); + src += BLOCK_SIZE; } } } From bf51f8ee91dc8205f12383181643cf11552c6dca Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 14 Sep 2025 16:45:58 -0400 Subject: [PATCH 459/466] fix load/save mismatch in missions When `$Contrail Speed Threshold:` and `+Volumetric Nebula:` are used in a mission, builds since 23.1 would throw an error, since the fields were loaded in a different order than they were saved. It turned out that the save order was the correct order, so this fixes the load order. --- code/mission/missionparse.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index e8b93f700f1..125fd2f2e40 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -804,15 +804,15 @@ void parse_mission_info(mission *pm, bool basic = false) stuff_float(&Neb2_fog_far_mult); } - if (optional_string("+Volumetric Nebula:")) { - pm->volumetrics.emplace().parse_volumetric_nebula(); - } - // Goober5000 - ship contrail speed threshold if (optional_string("$Contrail Speed Threshold:")){ stuff_int(&pm->contrail_threshold); } + if (optional_string("+Volumetric Nebula:")) { + pm->volumetrics.emplace().parse_volumetric_nebula(); + } + // get the number of players if in a multiplayer mission if ( pm->game_type & MISSION_TYPE_MULTI ) { if ( optional_string("+Num Players:") ) { From b0166dd6a64f4a996b5f4e1dd8616970357eb681 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sun, 14 Sep 2025 22:42:00 -0400 Subject: [PATCH 460/466] fix null vector assertion in slash beam collisions (#7038) Beams in general, and slash beams most likely, can collide with the edge of a model, which will cause a non-normalized vector assertion when particle effects are created. So comment out the normal assignment until a more comprehensive fix can be coded. --- code/weapon/beam.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/code/weapon/beam.cpp b/code/weapon/beam.cpp index 4a0a25e13f6..f2e05b477cf 100644 --- a/code/weapon/beam.cpp +++ b/code/weapon/beam.cpp @@ -3962,7 +3962,9 @@ void beam_handle_collisions(beam *b) if (wi->flash_impact_weapon_expl_effect.isValid()) { auto particleSource = particle::ParticleManager::get()->createSource(wi->flash_impact_weapon_expl_effect); particleSource->setHost(beam_hit_make_effect_host(b, &Objects[target], b->f_collisions[idx].cinfo.hit_submodel, &b->f_collisions[idx].cinfo.hit_point_world, &b->f_collisions[idx].cinfo.hit_point)); - particleSource->setNormal(worldNormal); +// TODO: Commenting out until the collision code can be enhanced to return a valid normal when a beam collides with an edge. +// (This can happen when a slash beam moves off the edge of a model; edge_hit will be true and hit_normal will be 0,0,0.) +// particleSource->setNormal(worldNormal); particleSource->setTriggerRadius(width); particleSource->finishCreation(); } @@ -3970,7 +3972,8 @@ void beam_handle_collisions(beam *b) if(do_expl){ auto particleSource = particle::ParticleManager::get()->createSource(wi->impact_weapon_expl_effect); particleSource->setHost(beam_hit_make_effect_host(b, &Objects[target], b->f_collisions[idx].cinfo.hit_submodel, &b->f_collisions[idx].cinfo.hit_point_world, &b->f_collisions[idx].cinfo.hit_point)); - particleSource->setNormal(worldNormal); +// TODO: see comment above +// particleSource->setNormal(worldNormal); particleSource->setTriggerRadius(width); particleSource->finishCreation(); } @@ -4026,7 +4029,8 @@ void beam_handle_collisions(beam *b) auto particleSource = particle::ParticleManager::get()->createSource(wi->piercing_impact_effect); particleSource->setHost(beam_hit_make_effect_host(b, &Objects[target], b->f_collisions[idx].cinfo.hit_submodel, &b->f_collisions[idx].cinfo.hit_point_world, &b->f_collisions[idx].cinfo.hit_point)); - particleSource->setNormal(worldNormal); +// TODO: see comment above +// particleSource->setNormal(worldNormal); particleSource->setTriggerRadius(width); particleSource->finishCreation(); } From 1e7431a04448d669b1b00229660b992b7d58fcb3 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:42:06 +0900 Subject: [PATCH 461/466] Give Particles Access to Their Spawner Effect (#7034) * Make particles aware of the effect that spawned them * Allow modular curves to call global functions from submember inputs * Add more in and outputs for particle lifetime curves * Allow post-curve velocity as a curve input * fix warning and incorrect assig * Disable CheckTriviallyCopyableMove * Clang Tidy --- .clang-tidy | 2 + code/particle/ParticleEffect.cpp | 59 +++++---- code/particle/ParticleEffect.h | 38 +++++- code/particle/ParticleManager.cpp | 8 +- code/particle/ParticleParse.cpp | 19 +-- code/particle/ParticleSource.h | 5 - code/particle/hosts/EffectHostParticle.cpp | 17 ++- code/particle/particle.cpp | 141 +++++++-------------- code/particle/particle.h | 59 +++------ code/utils/modular_curves.h | 26 +++- 10 files changed, 165 insertions(+), 209 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index ec1398b341a..160e1925a4c 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -48,5 +48,7 @@ CheckOptions: value: 'std::vector;std::deque;std::list;SCP_vector;SCP_deque;SCP_list' - key: 'readability-braces-around-statements.ShortStatementLines' value: '4' # Avoid flagging simple if (...) return false; statements + - key: 'performance-move-const-arg.CheckTriviallyCopyableMove' # This isn't actually performance relevant, but using move on trivially copyable types can well indicate that the variable should not be used anymore, such as when passing a builder type to the finialization function + value: 'false' ... diff --git a/code/particle/ParticleEffect.cpp b/code/particle/ParticleEffect.cpp index e1d61391def..fec51b3a5f1 100644 --- a/code/particle/ParticleEffect.cpp +++ b/code/particle/ParticleEffect.cpp @@ -48,8 +48,6 @@ ParticleEffect::ParticleEffect(SCP_string name) m_manual_offset (std::nullopt), m_manual_velocity_offset(std::nullopt), m_particleTrail(ParticleEffectHandle::invalid()), - m_size_lifetime_curve(-1), - m_vel_lifetime_curve (-1), m_particleChance(1.f), m_distanceCulled(-1.f) {} @@ -119,8 +117,6 @@ ParticleEffect::ParticleEffect(SCP_string name, m_manual_offset(offsetLocal), m_manual_velocity_offset(velocityOffsetLocal), m_particleTrail(particleTrail), - m_size_lifetime_curve(-1), - m_vel_lifetime_curve (-1), m_particleChance(particleChance), m_distanceCulled(distanceCulled) {} @@ -268,11 +264,11 @@ auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& s for (uint i = 0; i < num_spawn; ++i) { float particleFraction = static_cast(i) / static_cast(num_spawn); - particle_info info; + particle info; info.reverse = m_reverseAnimation; info.pos = pos; - info.vel = velParent; + info.velocity = velParent; if (m_parent_local) { info.attached_objnum = parent; @@ -284,9 +280,9 @@ auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& s } if (m_vel_inherit_absolute) - vm_vec_normalize_safe(&info.vel, true); + vm_vec_normalize_safe(&info.velocity, true); - info.vel *= (m_ignore_velocity_inherit_if_has_parent && parent >= 0) ? 0.f : m_vel_inherit.next() * inheritVelocityMultiplier; + info.velocity *= (m_ignore_velocity_inherit_if_has_parent && parent >= 0) ? 0.f : m_vel_inherit.next() * inheritVelocityMultiplier; vec3d localVelocity = velNoise; vec3d localPos = posNoise; @@ -325,32 +321,43 @@ auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& s m_velocity_directional_scaling == VelocityScaling::DOT ? dot : 1.f / std::max(0.001f, dot)); } - info.vel += localVelocity; - info.pos += localPos + info.vel * (interp * f2fl(Frametime)); + info.velocity += localVelocity; + info.pos += localPos + info.velocity * (interp * f2fl(Frametime)); info.bitmap = m_bitmap_list[m_bitmap_range.next()]; if (m_parentScale) // if we were spawned by a particle, parentRadius is the parent's radius and m_radius is a factor of that - info.rad = parentRadius * m_radius.next() * radiusMultiplier; + info.radius = parentRadius * m_radius.next() * radiusMultiplier; else - info.rad = m_radius.next() * radiusMultiplier; + info.radius = m_radius.next() * radiusMultiplier; info.length = m_length.next() * lengthMultiplier; - if (m_hasLifetime) { - if (m_parentLifetime) - // if we were spawned by a particle, parentLifetime is the parent's remaining lifetime and m_lifetime is a factor of that - info.lifetime = parentLifetime * m_lifetime.next() * lifetimeMultiplier; - else - info.lifetime = m_lifetime.next() * lifetimeMultiplier; - info.lifetime_from_animation = m_keep_anim_length_if_available; + int fps = 1; + if (info.nframes < 0) { + Assertion(bm_is_valid(info.bitmap), "Invalid bitmap handle passed to particle create."); + bm_get_info(info.bitmap, nullptr, nullptr, nullptr, &info.nframes, &fps); + } + + if (m_hasLifetime) { + if (m_keep_anim_length_if_available && info.nframes > 1) { + // Recalculate max life for ani's + info.max_life = i2fl(info.nframes) / i2fl(fps); + } + else { + if (m_parentLifetime) + // if we were spawned by a particle, parentLifetime is the parent's remaining lifetime and m_lifetime is a factor of that + info.max_life = parentLifetime * m_lifetime.next() * lifetimeMultiplier; + else + info.max_life = m_lifetime.next() * lifetimeMultiplier; + } } - info.starting_age = interp * f2fl(Frametime); - - info.size_lifetime_curve = m_size_lifetime_curve; - info.vel_lifetime_curve = m_vel_lifetime_curve; + info.age = interp * f2fl(Frametime); + info.looping = false; + info.angle = frand_range(0.0f, PI2); + info.parent_effect = m_self; switch (m_rotation_type) { case RotationType::DEFAULT: @@ -367,7 +374,7 @@ auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& s } if (m_particleTrail.isValid()) { - auto part = createPersistent(&info); + auto part = createPersistent(std::move(info)); if constexpr (isPersistent) createdParticles.push_back(part); @@ -381,12 +388,12 @@ auto ParticleEffect::processSourceInternal(float interp, const ParticleSource& s } } else { if constexpr (isPersistent){ - auto part = createPersistent(&info); + auto part = createPersistent(std::move(info)); createdParticles.push_back(part); } else { // We don't have a trail so we don't need a persistent particle - create(&info); + create(std::move(info)); } } } diff --git a/code/particle/ParticleEffect.h b/code/particle/ParticleEffect.h index 39b4be8da8a..fb1a0a044e3 100644 --- a/code/particle/ParticleEffect.h +++ b/code/particle/ParticleEffect.h @@ -84,15 +84,25 @@ class ParticleEffect { NUM_VALUES }; + enum class ParticleLifetimeCurvesOutput : uint8_t { + VELOCITY_MULT, + RADIUS_MULT, + LENGTH_MULT, + ANIM_STATE, + + NUM_VALUES + }; + private: friend struct ParticleParse; - + friend class ParticleManager; friend int ::parse_weapon(int, bool, const char*); - friend ParticleEffectHandle scripting::api::getLegacyScriptingParticleEffect(int bitmap, bool reversed); SCP_string m_name; //!< The name of this effect + ParticleSubeffectHandle m_self; + Duration m_duration; RotationType m_rotation_type; ShapeDirection m_direction; @@ -138,9 +148,6 @@ class ParticleEffect { ParticleEffectHandle m_particleTrail; - int m_size_lifetime_curve; //This is a curve of the particle, not of the particle effect, as such, it should not be part of the curve set - int m_vel_lifetime_curve; //This is a curve of the particle, not of the particle effect, as such, it should not be part of the curve set - float m_particleChance; //Deprecated. Use particle num random ranges instead. float m_distanceCulled; //Kinda deprecated. Only used by the oldest of legacy effects. @@ -251,7 +258,28 @@ class ParticleEffect { ModularCurvesMathOperators::division>{}} ); + constexpr static auto modular_curves_lifetime_definition = make_modular_curve_definition( + std::array { + std::pair {"Radius", ParticleLifetimeCurvesOutput::RADIUS_MULT}, + std::pair {"Velocity", ParticleLifetimeCurvesOutput::VELOCITY_MULT}, + std::pair {"Length", ParticleLifetimeCurvesOutput::LENGTH_MULT}, + std::pair {"Anim State", ParticleLifetimeCurvesOutput::ANIM_STATE}, + }, + //Should you ever need to access something from the effect as a modular curve input: + //std::pair {"", modular_curves_submember_input<&particle::parent_effect, &ParticleSubeffectHandle::getParticleEffect, &ParticleEffect::>{}} + std::pair {"Age", modular_curves_submember_input<&particle::age>{}}, + std::pair {"Lifetime", modular_curves_math_input< + modular_curves_submember_input<&particle::age>, + modular_curves_submember_input<&particle::max_life>, + ModularCurvesMathOperators::division>{}}, + std::pair {"Radius", modular_curves_submember_input<&particle::radius>{}}, + std::pair {"Velocity", modular_curves_submember_input<&particle::velocity, &vm_vec_mag_quick>{}}) + .derive_modular_curves_input_only_subset( + std::pair {"Post-Curves Velocity", modular_curves_self_input{}} + ); + MODULAR_CURVE_SET(m_modular_curves, modular_curves_definition); + MODULAR_CURVE_SET(m_lifetime_curves, modular_curves_lifetime_definition); private: float getCurrentFrequencyMult(decltype(modular_curves_definition)::input_type_t source) const; diff --git a/code/particle/ParticleManager.cpp b/code/particle/ParticleManager.cpp index 698a3f30c0a..ed73d9620b9 100644 --- a/code/particle/ParticleManager.cpp +++ b/code/particle/ParticleManager.cpp @@ -135,9 +135,13 @@ ParticleEffectHandle ParticleManager::addEffect(SCP_vector&& eff } #endif - m_effects.emplace_back(std::move(effect)); + auto& effect_after_emplace = m_effects.emplace_back(std::move(effect)); - return ParticleEffectHandle(static_cast(m_effects.size() - 1)); + auto handle = ParticleEffectHandle(static_cast(m_effects.size() - 1)); + for (size_t i = 0; i < effect_after_emplace.size(); i++) + effect_after_emplace[i].m_self = ParticleSubeffectHandle{handle, i}; + + return handle; } void ParticleManager::pageIn() { diff --git a/code/particle/ParticleParse.cpp b/code/particle/ParticleParse.cpp index 387cb8c96c9..6a7e982d763 100644 --- a/code/particle/ParticleParse.cpp +++ b/code/particle/ParticleParse.cpp @@ -283,20 +283,7 @@ namespace particle { } static void parseModularCurvesLifetime(ParticleEffect& effect) { - //TODO The following loop behaves as a true subset of how parsing will work once the particle modular curve set is implemented. - //As such, once that's added the loop can be replaced with a modular_curve_set.parse without worry about breaking tables. - while (optional_string("$Particle Lifetime Curve:")) { - required_string("+Input: Lifetime"); - - required_string("+Output:"); - int output = required_string_one_of(2, "Radius", "Velocity"); - //The required string part enforces this to be either 0 or 1 - required_string(output == 0 ? "Radius" : "Velocity"); - int& curve = output == 0 ? effect.m_size_lifetime_curve : effect.m_vel_lifetime_curve; - - required_string_either("+Curve Name:", "+Curve:", true); - curve = curve_parse(" Unknown curve requested for modular curves!"); - } + effect.m_lifetime_curves.parse("$Particle Lifetime Curve:"); } static void parseModularCurvesSource(ParticleEffect& effect) { @@ -368,13 +355,13 @@ namespace particle { static void parseSizeLifetimeCurve(ParticleEffect &effect) { if (optional_string("+Size over lifetime curve:")) { - effect.m_size_lifetime_curve = curve_parse(""); + effect.m_lifetime_curves.add_curve("Lifetime", ParticleEffect::ParticleLifetimeCurvesOutput::RADIUS_MULT, modular_curves_entry{curve_parse("")}); } } static void parseVelocityLifetimeCurve(ParticleEffect &effect) { if (optional_string("+Velocity scalar over lifetime curve:")) { - effect.m_vel_lifetime_curve = curve_parse(""); + effect.m_lifetime_curves.add_curve("Lifetime", ParticleEffect::ParticleLifetimeCurvesOutput::VELOCITY_MULT, modular_curves_entry{curve_parse("")}); } } diff --git a/code/particle/ParticleSource.h b/code/particle/ParticleSource.h index 5422950152d..df5c2c2c7f8 100644 --- a/code/particle/ParticleSource.h +++ b/code/particle/ParticleSource.h @@ -19,11 +19,6 @@ struct weapon_info; enum class WeaponState: uint32_t; namespace particle { - -class ParticleEffect; -struct particle_effect_tag { -}; -using ParticleEffectHandle = ::util::ID; /** * @brief The orientation of a particle source diff --git a/code/particle/hosts/EffectHostParticle.cpp b/code/particle/hosts/EffectHostParticle.cpp index c6663a85b78..a1c0ed01d5b 100644 --- a/code/particle/hosts/EffectHostParticle.cpp +++ b/code/particle/hosts/EffectHostParticle.cpp @@ -7,6 +7,8 @@ #include "freespace.h" +#include "particle/ParticleEffect.h" + EffectHostParticle::EffectHostParticle(particle::WeakParticlePtr particle, matrix orientationOverride, bool orientationOverrideRelative) : EffectHost(orientationOverride, orientationOverrideRelative), m_particle(std::move(particle)) {} @@ -16,10 +18,7 @@ std::pair EffectHostParticle::getPositionAndOrientation(bool /*re vec3d pos; if (interp != 0.0f) { - float vel_scalar = 1.0f; - if (particle->vel_lifetime_curve >= 0) { - vel_scalar = Curves[particle->vel_lifetime_curve].GetValue(particle->age / particle->max_life); - } + float vel_scalar = particle->parent_effect.getParticleEffect().m_lifetime_curves.get_output(particle::ParticleEffect::ParticleLifetimeCurvesOutput::VELOCITY_MULT, std::forward_as_tuple(*particle, vm_vec_mag_quick(&particle->velocity))); vec3d pos_last = particle->pos - (particle->velocity * vel_scalar * flFrametime); vm_vec_linear_interpolate(&pos, &particle->pos, &pos_last, interp); } else { @@ -52,11 +51,11 @@ float EffectHostParticle::getLifetime() const { float EffectHostParticle::getScale() const { const auto& particle = m_particle.lock(); - int idx = particle->size_lifetime_curve; - if (idx >= 0) - return particle->radius * Curves[idx].GetValue(particle->age / particle->max_life); - else - return particle->radius; + //For anything apart from the velocity curve, "Post-Curves Velocity" is well defined. This is needed to facilitate complex but common particle scaling and appearance curves. + const auto& curve_input = std::forward_as_tuple(*particle, + vm_vec_mag_quick(&particle->velocity) * particle->parent_effect.getParticleEffect().m_lifetime_curves.get_output(particle::ParticleEffect::ParticleLifetimeCurvesOutput::ANIM_STATE, std::forward_as_tuple(*particle, vm_vec_mag_quick(&particle->velocity)))); + + return particle->radius * particle->parent_effect.getParticleEffect().m_lifetime_curves.get_output(particle::ParticleEffect::ParticleLifetimeCurvesOutput::RADIUS_MULT, curve_input); } bool EffectHostParticle::isValid() const { diff --git a/code/particle/particle.cpp b/code/particle/particle.cpp index 471c937e3eb..79795c6bae3 100644 --- a/code/particle/particle.cpp +++ b/code/particle/particle.cpp @@ -106,6 +106,11 @@ namespace particle } } + const ParticleEffect& ParticleSubeffectHandle::getParticleEffect() const { + //TODO possibly cache this! + return ParticleManager::get()->getEffect(handle)[subeffect]; + } + // only call from game_shutdown()!!! void close() { @@ -134,118 +139,51 @@ namespace particle DCF_BOOL2(particles, Particles_enabled, "Turns particles on/off", "Usage: particles [bool]\nTurns particle system on/off. If nothing passed, then toggles it.\n"); - bool init_particle(particle* part, particle_info* info) { + static bool maybe_cull_particle(const particle& new_particle) { if (!Particles_enabled) { - return false; + return true; } - vec3d world_pos = info->pos; - if (info->attached_objnum >= 0) { - vm_vec_unrotate(&world_pos, &world_pos, &Objects[info->attached_objnum].orient); - world_pos += Objects[info->attached_objnum].pos; + vec3d world_pos = new_particle.pos; + if (new_particle.attached_objnum >= 0) { + vm_vec_unrotate(&world_pos, &world_pos, &Objects[new_particle.attached_objnum].orient); + world_pos += Objects[new_particle.attached_objnum].pos; } // treat particles on lower detail levels as 'further away' for the purposes of culling float adjusted_dist = vm_vec_dist(&Eye_position, &world_pos) * powf(2.5f, (float)(static_cast(DefaultDetailPreset::Num_detail_presets) - Detail.num_particles)); // treat bigger particles as 'closer' - adjusted_dist /= info->rad; + adjusted_dist /= new_particle.radius; float cull_start_dist = 1000.f; if (adjusted_dist > cull_start_dist) { if (frand() > 1.0f / (log2(adjusted_dist / cull_start_dist) + 1.0f)) - return false; + return true; } - int fps = 1; - - part->pos = info->pos; - part->velocity = info->vel; - part->age = info->starting_age; - part->max_life = info->lifetime; - part->radius = info->rad; - part->bitmap = info->bitmap; - part->attached_objnum = info->attached_objnum; - part->attached_sig = info->attached_sig; - part->reverse = info->reverse; - part->looping = false; - part->length = info->length; - part->angle = frand_range(0.0f, PI2); - part->use_angle = info->use_angle; - part->size_lifetime_curve = info->size_lifetime_curve; - part->vel_lifetime_curve = info->vel_lifetime_curve; - - if (info->nframes < 0) { - Assertion(bm_is_valid(info->bitmap), "Invalid bitmap handle passed to particle create."); - - bm_get_info(info->bitmap, nullptr, nullptr, nullptr, &part->nframes, &fps); - - if (part->nframes > 1 && info->lifetime_from_animation) - { - // Recalculate max life for ani's - part->max_life = i2fl(part->nframes) / i2fl(fps); - } - } - else { - if (part->bitmap < 0) - return false; - - part->nframes = info->nframes; - } + if (new_particle.nframes >= 0 && new_particle.bitmap < 0) + return true; - return true; + return false; } - void create(particle_info* pinfo) { - particle part; - if (!init_particle(&part, pinfo)) { + void create(particle&& new_particle) { + if (maybe_cull_particle(new_particle)) return; - } - Particles.push_back(std::move(part)); + Particles.push_back(new_particle); } // Creates a single particle. See the PARTICLE_?? defines for types. - WeakParticlePtr createPersistent(particle_info* pinfo) + WeakParticlePtr createPersistent(particle&& new_particle) { - ParticlePtr new_particle = std::make_shared(); + if (maybe_cull_particle(new_particle)) + return {}; - if (!init_particle(new_particle.get(), pinfo)) { - return WeakParticlePtr(); - } - - Persistent_particles.push_back(new_particle); - - return WeakParticlePtr(new_particle); - } + ParticlePtr new_particle_ptr = std::make_shared(new_particle); - void create(const vec3d* pos, - const vec3d* vel, - float lifetime, - float rad, - int bitmap, - const object* objp, - bool reverse) { - particle_info pinfo; - - // setup old data - pinfo.pos = *pos; - pinfo.vel = *vel; - pinfo.lifetime = lifetime; - pinfo.rad = rad; - pinfo.bitmap = bitmap; - pinfo.nframes = -1; - - // setup new data - if (objp == NULL) { - pinfo.attached_objnum = -1; - pinfo.attached_sig = -1; - } else { - pinfo.attached_objnum = OBJ_INDEX(objp); - pinfo.attached_sig = objp->signature; - } - pinfo.reverse = reverse; + Persistent_particles.push_back(new_particle_ptr); - // lower level function - create(&pinfo); + return {new_particle_ptr}; } /** @@ -292,10 +230,9 @@ namespace particle return true; } - float vel_scalar = 1.0f; - if (part->vel_lifetime_curve >= 0) { - vel_scalar = Curves[part->vel_lifetime_curve].GetValue(part->age / part->max_life); - } + const auto& source_effect = part->parent_effect.getParticleEffect(); + + float vel_scalar = source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::VELOCITY_MULT, std::forward_as_tuple(*part, vm_vec_mag_quick(&part->velocity)) ); // move as a regular particle part->pos += (part->velocity * vel_scalar) * frametime; @@ -406,12 +343,23 @@ namespace particle g3_transfer_vertex(&pos, &p_pos); + const auto& source_effect = part->parent_effect.getParticleEffect(); + + //For anything apart from the velocity curve, "Post-Curves Velocity" is well defined. This is needed to facilitate complex but common particle scaling and appearance curves. + const auto& curve_input = std::forward_as_tuple(*part, + vm_vec_mag_quick(&part->velocity) * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::ANIM_STATE, std::forward_as_tuple(*part, vm_vec_mag_quick(&part->velocity)))); + // figure out which frame we should be using int framenum; int cur_frame; if (part->nframes > 1) { - framenum = bm_get_anim_frame(part->bitmap, part->age, part->max_life, part->looping); - cur_frame = part->reverse ? (part->nframes - framenum - 1) : framenum; + if (source_effect.m_lifetime_curves.has_curve(ParticleEffect::ParticleLifetimeCurvesOutput::ANIM_STATE)) { + cur_frame = fl2i(i2fl(part->nframes - 1) * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::ANIM_STATE, curve_input)); + } + else { + framenum = bm_get_anim_frame(part->bitmap, part->age, part->max_life, part->looping); + cur_frame = part->reverse ? (part->nframes - framenum - 1) : framenum; + } } else { @@ -422,10 +370,7 @@ namespace particle Assert( cur_frame < part->nframes ); - float radius = part->radius; - if (part->size_lifetime_curve >= 0) { - radius *= Curves[part->size_lifetime_curve].GetValue(part->age / part->max_life); - } + float radius = part->radius * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::VELOCITY_MULT, curve_input); if (part->length != 0.0f) { vec3d p0 = p_pos; @@ -435,7 +380,7 @@ namespace particle if (part->attached_objnum >= 0) { vm_vec_unrotate(&p1, &p1, &Objects[part->attached_objnum].orient); } - p1 *= part->length; + p1 *= part->length * source_effect.m_lifetime_curves.get_output(ParticleEffect::ParticleLifetimeCurvesOutput::LENGTH_MULT, curve_input); p1 += p_pos; batching_add_laser(framenum + cur_frame, &p0, radius, &p1, radius); diff --git a/code/particle/particle.h b/code/particle/particle.h index 13ff2a5094d..9fd229c1ad0 100644 --- a/code/particle/particle.h +++ b/code/particle/particle.h @@ -21,6 +21,19 @@ extern bool Randomize_particle_rotation; namespace particle { + + class ParticleEffect; + struct particle_effect_tag { + }; + using ParticleEffectHandle = ::util::ID; + + struct ParticleSubeffectHandle { + ParticleEffectHandle handle; + size_t subeffect; + + const ParticleEffect& getParticleEffect() const; + }; + //============================================================================ //==================== PARTICLE SYSTEM GAME SEQUENCING CODE ================== //============================================================================ @@ -56,29 +69,6 @@ namespace particle extern int Anim_bitmap_id_smoke2; extern int Anim_num_frames_smoke2; - // particle creation stuff - typedef struct particle_info { - // old-style particle info - vec3d pos = vmd_zero_vector; - vec3d vel = vmd_zero_vector; - float lifetime = -1.0f; - float starting_age = 0.0f; - float rad = -1.0f; - int bitmap = -1; - int nframes = -1; - - // new-style particle info - int attached_objnum = -1; // if these are set, the pos is relative to the pos of the origin of the attached object - int attached_sig = -1; // to make sure the object hasn't changed or died. velocity is ignored in this case - bool reverse = false; // play any animations in reverse - bool lifetime_from_animation = true; // if the particle plays an animation then use the anim length for the particle life - float length = 0.f; // if set, makes the particle render like a laser, oriented along its path - bool use_angle = Randomize_particle_rotation; // whether particles created from this will use angles (i.e. can be rotated) - // (the field is initialized to the game_settings variable here because not all particle creation goes through ParticleProperties::createParticle) - int size_lifetime_curve = -1; // a curve idx for size over lifetime, if applicable - int vel_lifetime_curve = -1; // a curve idx for velocity over lifetime, if applicable - } particle_info; - typedef struct particle { // old style data vec3d pos; // position @@ -97,8 +87,8 @@ namespace particle float length; // the length of the particle for laser-style rendering float angle; bool use_angle; // whether this particle can be rotated - int size_lifetime_curve;// a curve idx for size over lifetime, if applicable - int vel_lifetime_curve; // a curve idx for velocity over lifetime, if applicable + + ParticleSubeffectHandle parent_effect; } particle; typedef std::weak_ptr WeakParticlePtr; @@ -113,22 +103,7 @@ namespace particle * * @param pinfo A structure containg information about how the particle should be created */ - void create(particle_info* pinfo); - - /** - * @brief Convenience function for creating a non-persistent particle without explicitly creating a particle_info - * structure. - * @return The particle handle - * - * @see particle::create(particle_info* pinfo) - */ - void create(const vec3d* pos, - const vec3d* vel, - float lifetime, - float rad, - int bitmap = -1, - const object* objp = nullptr, - bool reverse = false); + void create(particle&& new_particle); /** * @brief Creates a persistent particle @@ -140,7 +115,7 @@ namespace particle * @param pinfo A structure containg information about how the particle should be created * @return A weak reference to the particle */ - WeakParticlePtr createPersistent(particle_info* pinfo); + WeakParticlePtr createPersistent(particle&& new_particle); } #endif // _PARTICLE_H diff --git a/code/utils/modular_curves.h b/code/utils/modular_curves.h index a3fec77b1a2..683f50e5a61 100644 --- a/code/utils/modular_curves.h +++ b/code/utils/modular_curves.h @@ -39,12 +39,26 @@ struct modular_curves_submember_input { } //Pointer to static variable, i.e. used to index into things. else if constexpr (std::is_pointer_v) { - static_assert(std::is_integral_v>>, "Can only index into array from an integral input"); - using indexing_type = std::decay_t; - if (input >= 0) - return std::optional>{ std::cref((*grabber)[input]) }; - else - return std::optional>(std::nullopt); + if constexpr (std::is_invocable_v) { + //Global func by ref + return grabber(input); + } + else if constexpr (is_dereferenceable_pointer_v> && std::is_invocable_v>>) { + //Global func by ref from ptr + return grabber(*input); + } + else if constexpr (std::is_invocable_v) { + //Global func by ptr + return grabber(&input); + } + else { + static_assert(std::is_integral_v>>, "Can only index into array from an integral input"); + using indexing_type = std::decay_t; + if (input >= 0) + return std::optional>{std::cref((*grabber)[input])}; + else + return std::optional>(std::nullopt); + } } //Integer, used to index into tuples. Should be rarely used by actual users, but is required to do child-types. else if constexpr (std::is_integral_v) { From d42789decde84abb0dd54fca532871aba4171bf5 Mon Sep 17 00:00:00 2001 From: wookieejedi Date: Mon, 15 Sep 2025 07:31:02 -0400 Subject: [PATCH 462/466] Add new option for 'better combat collision avoidance' (#7036) The `$better combat collision avoidance for fightercraft:` flag is incredibly useful in preventing AI from hitting large ships while attacking, and the ability to tune the collision avoidance aggression factor further helps mods tune behavior based on their ship speeds (tuning that value is via `+combat collision avoidance aggression for fightercraft:` ). Nevertheless, the improve collision avoidance still is not performed for that actual fightercraft's actual target, so there can be situations where a fast fighter attacks a large ship with high speed, and then when that fighter break off from the attack they turn directly into the large ship's hull. Allowing the target to be incorporated into the `better_collision_avoidance_triggered` check prevents this from happening. To allow the target to be a part of the checks we just simply need to not pass an 'ignor_ship` argument to `maybe_avoid_big_ship`. This PR adds that ability via flag. This new flag is tested and works as expected. For example, without the flag a squadron of TIEs in FotG would literally ram themselves to death attacking some of our larger capital ships because they would break off attack too late, but with this flag they no longer accidentally collide at all. In discussion with Asteroth it was decided to make this enhanced behavior behind a flag, too. This PR also fixes a small oversight from the original #2810 where `next_check_time` was never actually used within the `else` block of `maybe_avoid_big_ship`. --- code/ai/ai_flags.h | 1 + code/ai/ai_profiles.cpp | 2 ++ code/ai/aicode.cpp | 15 +++++++++------ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/code/ai/ai_flags.h b/code/ai/ai_flags.h index 409044fa0b0..ff8980327bf 100644 --- a/code/ai/ai_flags.h +++ b/code/ai/ai_flags.h @@ -142,6 +142,7 @@ namespace AI { Fightercraft_nonshielded_ships_can_manage_ets, Ships_playing_dead_dont_manage_ets, Better_combat_collision_avoidance, + Better_combat_collision_avoid_includes_target, Better_guard_collision_avoidance, Require_exact_los, Improved_missile_avoidance, diff --git a/code/ai/ai_profiles.cpp b/code/ai/ai_profiles.cpp index 9dab5b92fdf..889106b9ff3 100644 --- a/code/ai/ai_profiles.cpp +++ b/code/ai/ai_profiles.cpp @@ -588,6 +588,8 @@ void parse_ai_profiles_tbl(const char *filename) stuff_float(&profile->better_collision_avoid_aggression_combat); } + set_flag(profile, "+combat collision avoidance for fightercraft includes target:", AI::Profile_Flags::Better_combat_collision_avoid_includes_target); + set_flag(profile, "$better guard collision avoidance for fightercraft:", AI::Profile_Flags::Better_guard_collision_avoidance); if (optional_string("+guard collision avoidance aggression for fightercraft:")) { diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index 8b09ed94218..818f1a8e7d0 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -7711,7 +7711,7 @@ bool maybe_avoid_big_ship(object *objp, object *ignore_objp, ai_info *aip, vec3d aip->ai_flags.remove(AI::AI_Flags::Avoiding_small_ship); aip->avoid_ship_num = -1; next_check_time = (int) (1500 * time_scale); - aip->avoid_check_timestamp = timestamp(1500); + aip->avoid_check_timestamp = timestamp(next_check_time); } } @@ -7737,7 +7737,7 @@ bool maybe_avoid_big_ship(object *objp, object *ignore_objp, ai_info *aip, vec3d * Return true if small ship and it will likely collide with large ship * developed by Asteroth */ -bool better_collision_avoidance_triggered(bool flag_to_check, float avoidance_aggression, object* pl_objp, object* en_objp) +bool better_collision_avoidance_triggered(bool flag_to_check, float avoidance_aggression, object* pl_objp, object* ignore_objp) { ship* shipp = &Ships[pl_objp->instance]; ship_info* sip = &Ship_info[shipp->ship_info_index]; @@ -7748,7 +7748,7 @@ bool better_collision_avoidance_triggered(bool flag_to_check, float avoidance_ag collide_vec *= radius_contribution; collide_vec += pl_objp->pos; - return (maybe_avoid_big_ship(pl_objp, en_objp, &Ai_info[shipp->ai_index], &collide_vec, 0.f, 0.1f)); + return (maybe_avoid_big_ship(pl_objp, ignore_objp, &Ai_info[shipp->ai_index], &collide_vec, 0.f, 0.1f)); } return false; } @@ -8978,7 +8978,8 @@ void ai_chase() if (better_collision_avoidance_triggered( The_mission.ai_profile->flags[AI::Profile_Flags::Better_combat_collision_avoidance], The_mission.ai_profile->better_collision_avoid_aggression_combat, - Pl_objp, En_objp)) { + Pl_objp, + The_mission.ai_profile->flags[AI::Profile_Flags::Better_combat_collision_avoid_includes_target] ? nullptr : En_objp)) { return; } @@ -10896,7 +10897,8 @@ void ai_guard() if (better_collision_avoidance_triggered( The_mission.ai_profile->flags[AI::Profile_Flags::Better_guard_collision_avoidance], The_mission.ai_profile->better_collision_avoid_aggression_guard, - Pl_objp, En_objp)) { + Pl_objp, + En_objp)) { return; } @@ -14190,7 +14192,8 @@ void ai_execute_behavior(ai_info *aip) if (!(better_collision_avoidance_triggered( The_mission.ai_profile->flags[AI::Profile_Flags::Better_combat_collision_avoidance], The_mission.ai_profile->better_collision_avoid_aggression_combat, - Pl_objp, En_objp))) { + Pl_objp, + The_mission.ai_profile->flags[AI::Profile_Flags::Better_combat_collision_avoid_includes_target] ? nullptr : En_objp))) { ai_big_strafe(); // strafe a big ship } } else { From ce3597f5a67515f76957d552c974ef224f770dc8 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 15 Sep 2025 14:56:58 -0500 Subject: [PATCH 463/466] QtFRED Relative Coordinates Dialog (#7031) * qtfred relative coords dialog * clang --- qtfred/source_groups.cmake | 7 +- .../RelativeCoordinatesDialogModel.cpp | 161 +++++++++++++++ .../dialogs/RelativeCoordinatesDialogModel.h | 44 ++++ qtfred/src/ui/FredView.cpp | 7 + qtfred/src/ui/FredView.h | 1 + .../ui/dialogs/RelativeCoordinatesDialog.cpp | 87 ++++++++ .../ui/dialogs/RelativeCoordinatesDialog.h | 34 +++ qtfred/ui/FredView.ui | 8 +- qtfred/ui/RelativeCoordinatesDialog.ui | 195 ++++++++++++++++++ 9 files changed, 542 insertions(+), 2 deletions(-) create mode 100644 qtfred/src/mission/dialogs/RelativeCoordinatesDialogModel.cpp create mode 100644 qtfred/src/mission/dialogs/RelativeCoordinatesDialogModel.h create mode 100644 qtfred/src/ui/dialogs/RelativeCoordinatesDialog.cpp create mode 100644 qtfred/src/ui/dialogs/RelativeCoordinatesDialog.h create mode 100644 qtfred/ui/RelativeCoordinatesDialog.ui diff --git a/qtfred/source_groups.cmake b/qtfred/source_groups.cmake index b5979920de5..941ba377551 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -73,7 +73,9 @@ add_file_folder("Source/Mission/Dialogs" src/mission/dialogs/ObjectOrientEditorDialogModel.cpp src/mission/dialogs/ObjectOrientEditorDialogModel.h src/mission/dialogs/ReinforcementsEditorDialogModel.cpp - src/mission/dialogs/ReinforcementsEditorDialogModel.h + src/mission/dialogs/ReinforcementsEditorDialogModel.h + src/mission/dialogs/RelativeCoordinatesDialogModel.cpp + src/mission/dialogs/RelativeCoordinatesDialogModel.h src/mission/dialogs/SelectionDialogModel.cpp src/mission/dialogs/SelectionDialogModel.h src/mission/dialogs/ShieldSystemDialogModel.cpp @@ -174,6 +176,8 @@ add_file_folder("Source/UI/Dialogs" src/ui/dialogs/ObjectOrientEditorDialog.h src/ui/dialogs/ReinforcementsEditorDialog.cpp src/ui/dialogs/ReinforcementsEditorDialog.h + src/ui/dialogs/RelativeCoordinatesDialog.cpp + src/ui/dialogs/RelativeCoordinatesDialog.h src/ui/dialogs/SelectionDialog.cpp src/ui/dialogs/SelectionDialog.h src/ui/dialogs/ShieldSystemDialog.h @@ -288,6 +292,7 @@ add_file_folder("UI" ui/MusicPlayerDialog.ui ui/ObjectOrientationDialog.ui ui/ReinforcementsDialog.ui + ui/RelativeCoordinatesDialog.ui ui/SelectionDialog.ui ui/ShieldSystemDialog.ui ui/SoundEnvironmentDialog.ui diff --git a/qtfred/src/mission/dialogs/RelativeCoordinatesDialogModel.cpp b/qtfred/src/mission/dialogs/RelativeCoordinatesDialogModel.cpp new file mode 100644 index 00000000000..7fea060bc7b --- /dev/null +++ b/qtfred/src/mission/dialogs/RelativeCoordinatesDialogModel.cpp @@ -0,0 +1,161 @@ +#include "RelativeCoordinatesDialogModel.h" + +#include "math/vecmat.h" +#include + + +namespace fso::fred::dialogs { + +RelativeCoordinatesDialogModel::RelativeCoordinatesDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + _objects.clear(); + + for (auto ptr = GET_FIRST(&obj_used_list); ptr != END_OF_LIST(&obj_used_list); ptr = GET_NEXT(ptr)) { + bool added = false; + + int objnum = OBJ_INDEX(ptr); + + if (ptr->type == OBJ_START || ptr->type == OBJ_SHIP) { + _objects.emplace_back(Ships[ptr->instance].ship_name, objnum); + + added = true; + } else if (ptr->type == OBJ_WAYPOINT) { + SCP_string text; + int waypoint_num; + + auto wp_list = find_waypoint_list_with_instance(ptr->instance, &waypoint_num); + Assert(wp_list != nullptr); + text = wp_list->get_name(); + text += ":"; + text += std::to_string(waypoint_num + 1); + _objects.emplace_back(text, objnum); + + added = true; + } + + bool marked = (ptr->flags[Object::Object_Flags::Marked]); + + if (added && marked && _originIndex == -1) { + _originIndex = objnum; // TODO: select first marked object as origin.. not sure QtFRED has cur_object_index available yet + } else if (added && marked && _satelliteIndex == -1 && objnum != _originIndex) { + _satelliteIndex = objnum; + } + } + + computeCoordinates(); +} + +bool RelativeCoordinatesDialogModel::apply() +{ + // Read only dialog + return true; +} + +void RelativeCoordinatesDialogModel::reject() +{ + // Read only dialog +} + +float RelativeCoordinatesDialogModel::getDistance() const +{ + return _distance; +} +float RelativeCoordinatesDialogModel::getPitch() const +{ + return _orientation_p; +} +float RelativeCoordinatesDialogModel::getBank() const +{ + return _orientation_b; +} +float RelativeCoordinatesDialogModel::getHeading() const +{ + return _orientation_h; +} + +int RelativeCoordinatesDialogModel::getOrigin() const +{ + return _originIndex; +} + +void RelativeCoordinatesDialogModel::setOrigin(int index) +{ + if (_originIndex == index) + return; + _originIndex = index; + computeCoordinates(); +} + +int RelativeCoordinatesDialogModel::getSatellite() const +{ + return _satelliteIndex; +} + +void RelativeCoordinatesDialogModel::setSatellite(int index) +{ + if (_satelliteIndex == index) + return; + _satelliteIndex = index; + computeCoordinates(); +} + +SCP_vector> RelativeCoordinatesDialogModel::getObjectsList() const +{ + return _objects; +} + +void RelativeCoordinatesDialogModel::computeCoordinates() +{ + if (_originIndex < 0 || _satelliteIndex < 0 || (_originIndex == _satelliteIndex)) { + _distance = 0.0f; + _orientation_p = 0.0f; + _orientation_b = 0.0f; + _orientation_h = 0.0f; + + return; + } + + auto origin_pos = Objects[_originIndex].pos; + auto satellite_pos = Objects[_satelliteIndex].pos; + + // distance + _distance = vm_vec_dist(&origin_pos, &satellite_pos); + + // transform the coordinate frame + vec3d delta_vec, local_vec; + vm_vec_sub(&delta_vec, &satellite_pos, &origin_pos); + if (Objects[_originIndex].type != OBJ_WAYPOINT) + vm_vec_rotate(&local_vec, &delta_vec, &Objects[_originIndex].orient); + + // find the orientation + matrix m; + vm_vector_2_matrix(&m, &local_vec); + + // find the angles + angles ang; + vm_extract_angles_matrix(&ang, &m); + _orientation_p = to_degrees(ang.p); + _orientation_b = to_degrees(ang.b); + _orientation_h = to_degrees(ang.h); +} + +float RelativeCoordinatesDialogModel::to_degrees(float rad) +{ + float deg = fl_degrees(rad); + return normalize_degrees(deg); +} + +float RelativeCoordinatesDialogModel::normalize_degrees(float deg) +{ + while (deg < -180.0f) + deg += 180.0f; + while (deg > 180.0f) + deg -= 180.0f; + // check for negative zero... + if (deg == -0.0f) + return 0.0f; + return deg; +} + +} // namespace fso::fred::dialogs diff --git a/qtfred/src/mission/dialogs/RelativeCoordinatesDialogModel.h b/qtfred/src/mission/dialogs/RelativeCoordinatesDialogModel.h new file mode 100644 index 00000000000..74ca89cb324 --- /dev/null +++ b/qtfred/src/mission/dialogs/RelativeCoordinatesDialogModel.h @@ -0,0 +1,44 @@ +#pragma once + +#include "mission/dialogs/AbstractDialogModel.h" + +namespace fso::fred::dialogs { + +class RelativeCoordinatesDialogModel : public AbstractDialogModel { + Q_OBJECT + public: + RelativeCoordinatesDialogModel(QObject* parent, EditorViewport* viewport); + + bool apply() override; + void reject() override; + + float getDistance() const; + float getPitch() const; + float getBank() const; + float getHeading() const; + + int getOrigin() const; + void setOrigin(int index); + int getSatellite() const; + void setSatellite(int index); + + SCP_vector> getObjectsList() const; + + private: + void computeCoordinates(); + static float to_degrees(float rad); + static float normalize_degrees(float deg); + + int _originIndex = -1; + int _satelliteIndex = -1; + + float _distance = 0.0f; + float _orientation_p = 0.0f; + float _orientation_b = 0.0f; + float _orientation_h = 0.0f; + + SCP_vector> _objects; // (name, obj index) + +}; + +} // namespace fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 42cda670e8a..3a9ecfd1e2a 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include #include "mission/Editor.h" @@ -1235,6 +1236,12 @@ void FredView::on_actionMusic_Player_triggered(bool) dialog->show(); } +void FredView::on_actionCalculate_Relative_Coordinates_triggered(bool) { + auto dialog = new dialogs::RelativeCoordinatesDialog(this, _viewport); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); +} + void FredView::on_actionFiction_Viewer_triggered(bool) { auto dialog = new dialogs::FictionViewerDialog(this, _viewport); dialog->setAttribute(Qt::WA_DeleteOnClose); diff --git a/qtfred/src/ui/FredView.h b/qtfred/src/ui/FredView.h index a2c2743ae02..59be40a9661 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -153,6 +153,7 @@ class FredView: public QMainWindow, public IDialogProvider { void on_actionFiction_Viewer_triggered(bool); void on_actionMission_Goals_triggered(bool); void on_actionMusic_Player_triggered(bool); + void on_actionCalculate_Relative_Coordinates_triggered(bool); signals: /** * @brief Special version of FredApplication::onIdle which is limited to the lifetime of this object diff --git a/qtfred/src/ui/dialogs/RelativeCoordinatesDialog.cpp b/qtfred/src/ui/dialogs/RelativeCoordinatesDialog.cpp new file mode 100644 index 00000000000..b2d373c635f --- /dev/null +++ b/qtfred/src/ui/dialogs/RelativeCoordinatesDialog.cpp @@ -0,0 +1,87 @@ +#include "ui/dialogs/RelativeCoordinatesDialog.h" +#include "ui_RelativeCoordinatesDialog.h" +#include "ui/util/SignalBlockers.h" + +#include + +namespace fso::fred::dialogs { + +RelativeCoordinatesDialog::RelativeCoordinatesDialog(FredView* parent, EditorViewport* viewport) + : QDialog(parent), _viewport(viewport), ui(new Ui::RelativeCoordinatesDialog()), + _model(new RelativeCoordinatesDialogModel(this, viewport)) +{ + ui->setupUi(this); + + initializeUi(); +} + +RelativeCoordinatesDialog::~RelativeCoordinatesDialog() = default; + +void RelativeCoordinatesDialog::initializeUi() +{ + util::SignalBlockers blockers(this); + + ui->originListWidget->clear(); + ui->satelliteListWidget->clear(); + + const auto objects = _model->getObjectsList(); + + auto addWithData = [](QListWidget* list, const std::string& name, int objnum) { + auto* item = new QListWidgetItem(QString::fromStdString(name)); + item->setData(Qt::UserRole, objnum); + list->addItem(item); + }; + + for (const auto& [name, objnum] : objects) { + addWithData(ui->originListWidget, name, objnum); + addWithData(ui->satelliteListWidget, name, objnum); + } + + updateUi(); +} + +void RelativeCoordinatesDialog::updateUi() +{ + util::SignalBlockers blockers(this); + + // Restore selections based on model's obj indices + auto selectByObjnum = [](QListWidget* list, int wantObj) { + if (wantObj < 0) + return; + for (int r = 0; r < list->count(); ++r) { + if (list->item(r)->data(Qt::UserRole).toInt() == wantObj) { + list->setCurrentRow(r); + break; + } + } + }; + selectByObjnum(ui->originListWidget, _model->getOrigin()); + selectByObjnum(ui->satelliteListWidget, _model->getSatellite()); + + ui->distanceDoubleSpinBox->setValue(static_cast(_model->getDistance())); + ui->pitchDoubleSpinBox->setValue(static_cast(_model->getPitch())); + ui->bankDoubleSpinBox->setValue(static_cast(_model->getBank())); + ui->headingDoubleSpinBox->setValue(static_cast(_model->getHeading())); +} + +void RelativeCoordinatesDialog::on_originListWidget_currentRowChanged(int row) +{ + auto* item = ui->originListWidget->item(row); + if (!item) + return; + const int objnum = item->data(Qt::UserRole).toInt(); + _model->setOrigin(objnum); + updateUi(); +} + +void RelativeCoordinatesDialog::on_satelliteListWidget_currentRowChanged(int row) +{ + auto* item = ui->satelliteListWidget->item(row); + if (!item) + return; + const int objnum = item->data(Qt::UserRole).toInt(); + _model->setSatellite(objnum); + updateUi(); +} + +} // namespace fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/RelativeCoordinatesDialog.h b/qtfred/src/ui/dialogs/RelativeCoordinatesDialog.h new file mode 100644 index 00000000000..e307e94d2ef --- /dev/null +++ b/qtfred/src/ui/dialogs/RelativeCoordinatesDialog.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "mission/dialogs/RelativeCoordinatesDialogModel.h" +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class RelativeCoordinatesDialog; +} + +class RelativeCoordinatesDialog final : public QDialog { + Q_OBJECT + public: + explicit RelativeCoordinatesDialog(FredView* parent, EditorViewport* viewport); + ~RelativeCoordinatesDialog() override; + + private slots: + void on_originListWidget_currentRowChanged(int row); + void on_satelliteListWidget_currentRowChanged(int row); + + private: // NOLINT(readability-redundant-access-specifiers) + void initializeUi(); + void updateUi(); + + // Boilerplate + EditorViewport* _viewport = nullptr; + std::unique_ptr ui; + std::unique_ptr _model; +}; + +} // namespace fred::dialogs diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index 0348e11c87c..af613091eef 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -20,7 +20,7 @@ 0 0 926 - 26 + 22 @@ -239,6 +239,7 @@ + @@ -1552,6 +1553,11 @@ Volumetric Nebula + + + Calculate Relative Coordinates + + diff --git a/qtfred/ui/RelativeCoordinatesDialog.ui b/qtfred/ui/RelativeCoordinatesDialog.ui new file mode 100644 index 00000000000..f3876932ade --- /dev/null +++ b/qtfred/ui/RelativeCoordinatesDialog.ui @@ -0,0 +1,195 @@ + + + fso::fred::dialogs::RelativeCoordinatesDialog + + + Qt::WindowModal + + + + 0 + 0 + 650 + 335 + + + + Calculate Relative Coordinates + + + true + + + true + + + + 6 + + + 10 + + + 10 + + + 10 + + + 10 + + + + + + + Origin + + + + + + + + + + + + + + Satellite + + + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + Distance + + + + + + + false + + + QAbstractSpinBox::NoButtons + + + 16777215.000000000000000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + P: + + + + + + + B: + + + + + + + H: + + + + + + + false + + + QAbstractSpinBox::NoButtons + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + + + + + false + + + QAbstractSpinBox::NoButtons + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + + + + + false + + + QAbstractSpinBox::NoButtons + + + -16777215.000000000000000 + + + 16777215.000000000000000 + + + + + + + + + + From 8057b52a04e5bd998900e7c8a967d7d779fda642 Mon Sep 17 00:00:00 2001 From: Shivansps Date: Tue, 16 Sep 2025 01:38:02 -0300 Subject: [PATCH 464/466] Do not set IS_X86 when target cpu is armv7 (#7033) --- cmake/globals.cmake | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cmake/globals.cmake b/cmake/globals.cmake index 93775cae71c..be918c9e9e1 100644 --- a/cmake/globals.cmake +++ b/cmake/globals.cmake @@ -9,14 +9,19 @@ else() endif() set(IS_ARM64 FALSE) +set(IS_ARMV7A FALSE) if (NOT "${CMAKE_GENERATOR_PLATFORM}" STREQUAL "") # needed to cover Visual Studio generator if(CMAKE_GENERATOR_PLATFORM MATCHES "^(aarch64|arm64|ARM64)") set(IS_ARM64 TRUE) + elseif(CMAKE_GENERATOR_PLATFORM MATCHES "^(armv7)") + set(IS_ARMV7A TRUE) endif() else() if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64|ARM64)") set(IS_ARM64 TRUE) + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "^(armv7)") + set(IS_ARMV7A TRUE) endif() endif() @@ -33,6 +38,6 @@ else() endif() set(IS_X86 FALSE) -if (NOT IS_ARM64 AND NOT IS_RISCV) +if (NOT IS_ARM64 AND NOT IS_RISCV AND NOT IS_ARMV7A) set(IS_X86 TRUE) endif() From 7875e5f7c72fd14d27ac868e75dc5f0661630c79 Mon Sep 17 00:00:00 2001 From: BMagnu <6238428+BMagnu@users.noreply.github.com> Date: Tue, 16 Sep 2025 13:45:12 +0900 Subject: [PATCH 465/466] Supply normals for edge hits (#7039) * Pass out normals for edge hits * Remove now-obsolete comments * Also add third instance of disabled particle normal --- code/model/modelcollide.cpp | 1 + code/weapon/beam.cpp | 10 +++------- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/code/model/modelcollide.cpp b/code/model/modelcollide.cpp index ad7dfed84f3..c6fe18afa20 100644 --- a/code/model/modelcollide.cpp +++ b/code/model/modelcollide.cpp @@ -323,6 +323,7 @@ static void mc_check_sphereline_face( int nv, vec3d ** verts, vec3d * plane_pnt, // This is closer than best so far Mc->hit_dist = sphere_time; Mc->hit_point = hit_point; + Mc->hit_normal = *plane_norm; Mc->hit_submodel = Mc_submodel; Mc->edge_hit = true; diff --git a/code/weapon/beam.cpp b/code/weapon/beam.cpp index f2e05b477cf..4a0a25e13f6 100644 --- a/code/weapon/beam.cpp +++ b/code/weapon/beam.cpp @@ -3962,9 +3962,7 @@ void beam_handle_collisions(beam *b) if (wi->flash_impact_weapon_expl_effect.isValid()) { auto particleSource = particle::ParticleManager::get()->createSource(wi->flash_impact_weapon_expl_effect); particleSource->setHost(beam_hit_make_effect_host(b, &Objects[target], b->f_collisions[idx].cinfo.hit_submodel, &b->f_collisions[idx].cinfo.hit_point_world, &b->f_collisions[idx].cinfo.hit_point)); -// TODO: Commenting out until the collision code can be enhanced to return a valid normal when a beam collides with an edge. -// (This can happen when a slash beam moves off the edge of a model; edge_hit will be true and hit_normal will be 0,0,0.) -// particleSource->setNormal(worldNormal); + particleSource->setNormal(worldNormal); particleSource->setTriggerRadius(width); particleSource->finishCreation(); } @@ -3972,8 +3970,7 @@ void beam_handle_collisions(beam *b) if(do_expl){ auto particleSource = particle::ParticleManager::get()->createSource(wi->impact_weapon_expl_effect); particleSource->setHost(beam_hit_make_effect_host(b, &Objects[target], b->f_collisions[idx].cinfo.hit_submodel, &b->f_collisions[idx].cinfo.hit_point_world, &b->f_collisions[idx].cinfo.hit_point)); -// TODO: see comment above -// particleSource->setNormal(worldNormal); + particleSource->setNormal(worldNormal); particleSource->setTriggerRadius(width); particleSource->finishCreation(); } @@ -4029,8 +4026,7 @@ void beam_handle_collisions(beam *b) auto particleSource = particle::ParticleManager::get()->createSource(wi->piercing_impact_effect); particleSource->setHost(beam_hit_make_effect_host(b, &Objects[target], b->f_collisions[idx].cinfo.hit_submodel, &b->f_collisions[idx].cinfo.hit_point_world, &b->f_collisions[idx].cinfo.hit_point)); -// TODO: see comment above -// particleSource->setNormal(worldNormal); + particleSource->setNormal(worldNormal); particleSource->setTriggerRadius(width); particleSource->finishCreation(); } From cda542dae6d2ca7c8a87333a18843403ade5ee7a Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Tue, 16 Sep 2025 21:40:42 -0400 Subject: [PATCH 466/466] Prepare to sync 2025-09-16 Get the codebase into a state where it can be automatically synced with the FSO code. This commit will be rolled back after the sync. --- code/source_groups.cmake | 9 --------- 1 file changed, 9 deletions(-) diff --git a/code/source_groups.cmake b/code/source_groups.cmake index abf56b9b2e3..601ae93841a 100644 --- a/code/source_groups.cmake +++ b/code/source_groups.cmake @@ -836,15 +836,6 @@ add_file_folder("Mission" mission/mission_flags.h ) -add_file_folder("Mission\\\\Import" - mission/import/xwingbrflib.cpp - mission/import/xwingbrflib.h - mission/import/xwinglib.cpp - mission/import/xwinglib.h - mission/import/xwingmissionparse.cpp - mission/import/xwingmissionparse.h -) - # MissionUI files add_file_folder("MissionUI" missionui/chatbox.cpp