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/.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/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/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() 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/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/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 ebb6f15d221..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:")) { @@ -699,6 +701,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 +821,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/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/ai/aicode.cpp b/code/ai/aicode.cpp index df09fee6c14..818f1a8e7d0 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)) { @@ -7697,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); } } @@ -7723,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]; @@ -7734,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; } @@ -8964,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; } @@ -10882,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; } @@ -12125,9 +12141,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); @@ -13228,8 +13246,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; @@ -13250,11 +13266,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; @@ -14032,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 @@ -14168,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 { 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 12470cd6f11..d929e727d35 100644 --- a/code/ai/aigoals.h +++ b/code/ai/aigoals.h @@ -80,7 +80,8 @@ 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_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, AI_GOAL_DISABLE_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 4c2f85c6449..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; } } @@ -1559,7 +1561,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 +1663,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); @@ -2043,9 +2047,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 +2167,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..6fedf4daa0c 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; @@ -824,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) { @@ -1705,37 +1693,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 = 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); + + 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 { @@ -1751,8 +1759,10 @@ 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()) { - particle::create( hitpos, &vmd_zero_vector, 0.0f, Asteroid_impact_explosion_radius, Asteroid_impact_explosion_ani ); + 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(); } } } @@ -2330,13 +2340,25 @@ 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); + } + } + + 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:")){ @@ -2461,18 +2483,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:")) { @@ -2507,24 +2563,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); - } } } } @@ -2532,6 +2585,7 @@ SCP_vector get_list_valid_asteroid_subtypes() return list; } + static void verify_asteroid_splits() { @@ -2622,8 +2676,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,8 +2697,11 @@ 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) { - Error(LOCATION, "Missing valid asteroid impact explosion definition in asteroid.tbl!"); + if (!Asteroid_impact_explosion_ani.isValid()) { + // 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') diff --git a/code/asteroid/asteroid.h b/code/asteroid/asteroid.h index 7b7faf40e59..31781b5c978 100644 --- a/code/asteroid/asteroid.h +++ b/code/asteroid/asteroid.h @@ -16,6 +16,8 @@ #include "globalincs/pstypes.h" #include "object/object_flags.h" #include "io/timer.h" +#include "particle/ParticleSource.h" +#include "math/vecmat.h" class object; class polymodel; @@ -79,7 +81,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 +94,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 = ""; @@ -130,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; @@ -147,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]; @@ -179,7 +200,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/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/cmdline/cmdline.cpp b/code/cmdline/cmdline.cpp index e56a4f736a8..7dae2532cab 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,10 @@ bool SetCmdlineParams() } } + if (multithreading.found()) { + Cmdline_multithreading = abs(multithreading.get_int()); + } + 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/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()); 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/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; } } } diff --git a/code/debris/debris.cpp b/code/debris/debris.cpp index 56bf35e714f..f458137b69f 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 @@ -67,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()) { @@ -121,6 +143,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 +160,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 +449,32 @@ 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]; + 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)); + source->finishCreation(); + } } return obj; @@ -550,22 +607,10 @@ 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) { + // 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; @@ -580,6 +625,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 @@ -681,13 +742,15 @@ 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); } } + 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/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/decals/decals.cpp b/code/decals/decals.cpp index 7e386e595b5..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) { @@ -370,7 +356,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(const 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 +395,62 @@ 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; } +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) { + if (!Decal_system_active || !Decal_option_active || !gr_is_capable(gr_capability::CAPABILITY_INSTANCED_RENDERING)) { return; } @@ -432,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(active_decals.size()); - 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) { @@ -521,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 26ec0952d65..138caae0ec5 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 +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; @@ -23,24 +29,15 @@ layout (std140) uniform decalGlobalData { mat4 invViewMatrix; mat4 invProjMatrix; - vec3 ambientLight; - 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 +74,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 +88,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 +108,7 @@ void main() { vec3 frag_position = computeViewPosition(gl_FragCoord.xy); float alpha = alpha_scale; + vec2 tex_coord = getDecalTexCoord(frag_position, alpha); vec3 binormal; @@ -134,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 4cfa3b36b87..bd3511e0c7e 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; + +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; @@ -7,27 +14,29 @@ layout (std140) uniform decalGlobalData { mat4 invViewMatrix; mat4 invProjMatrix; - vec3 ambientLight; - 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) * 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..6932430d65d 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 @@ -483,10 +413,22 @@ 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 - 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/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/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/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/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/fireball/fireballs.cpp b/code/fireball/fireballs.cpp index 7783a612fe4..10b6c7aceed 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; } } @@ -1150,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)); 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; } diff --git a/code/globalincs/pstypes.h b/code/globalincs/pstypes.h index 8c28441f28a..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 @@ -397,15 +411,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/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/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..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, @@ -271,14 +273,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 +296,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 +306,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 +338,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 +897,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..b0f81848da6 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 @@ -114,29 +82,34 @@ decal_draw_list::decal_draw_list(size_t num_decals) header->viewportSize.x = (float) gr_screen.max_w; header->viewportSize.y = (float) gr_screen.max_h; - gr_get_ambient_light(&header->ambientLight); + 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); - // 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; + draw_info.first.uniform_offset = _buffer.getBufferOffset(aligner.getCurrentOffset()); - header->ambientLight.xyz.x += gr_light_emission[0]; - header->ambientLight.xyz.y += gr_light_emission[1]; - header->ambientLight.xyz.z += gr_light_emission[2]; -} -decal_draw_list::~decal_draw_list() { + 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; + } } + 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 +122,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 +137,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/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/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/gropengl.cpp b/code/graphics/opengl/gropengl.cpp index 4756d276874..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) { @@ -1489,8 +1500,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/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp index cce19f69bfe..b3acf8faf84 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" @@ -189,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; @@ -223,7 +227,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 +251,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 +268,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 +300,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,38 +328,54 @@ 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; - } + auto light_data = prepare_light_uniforms(l, light_uniform_aligner, lp); - // 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; + } - 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; + // Global light direction should match shadow light direction + if (first_directional) { + global_light = &l; + global_light_diffuse = light_data->diffuseLightColor; - vm_vec_transform(&view_dir, &light_dir, &gr_view_matrix); + first_directional = false; + } + + 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); + 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; @@ -355,6 +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; + // 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; @@ -362,11 +395,12 @@ 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; + light_data->lightType = LT_TUBE; vec3d a; @@ -389,7 +423,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(); @@ -494,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)); @@ -521,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); @@ -602,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/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); 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/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/opengl/gropenglshader.cpp b/code/graphics/opengl/gropenglshader.cpp index daed462a172..f17cd83ee45 100644 --- a/code/graphics/opengl/gropenglshader.cpp +++ b/code/graphics/opengl/gropenglshader.cpp @@ -57,7 +57,7 @@ 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 }}} }, }; struct opengl_uniform_block_binding { @@ -141,7 +141,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 }, @@ -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"}, @@ -884,7 +886,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) { @@ -904,7 +906,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)); @@ -1076,19 +1078,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 151f0d43e4f..6df7875f554 100644 --- a/code/graphics/opengl/gropenglshader.h +++ b/code/graphics/opengl/gropenglshader.h @@ -35,7 +35,7 @@ struct opengl_vert_attrib { MODEL_ID, RADIUS, UVEC, - WORLD_MATRIX, + MODEL_MATRIX, NUM_ATTRIBS, }; @@ -151,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/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 diff --git a/code/graphics/opengl/gropengltnl.cpp b/code/graphics/opengl/gropengltnl.cpp index fe680cd189e..70f97fd3654 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 { @@ -855,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)) @@ -911,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, @@ -1189,7 +1180,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); @@ -1233,15 +1224,23 @@ void opengl_bind_vertex_array(const vertex_layout& layout) { auto attribIndex = 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 +1266,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/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/software/font.cpp b/code/graphics/software/font.cpp index 5b4b5e5ec15..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) { - WarningEx(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"); } 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 4bf861d44ab..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,30 +180,18 @@ struct decal_globals { matrix4 invViewMatrix; matrix4 invProjMatrix; - vec3d ambientLight; - float pad0; - vec2d viewportSize; float pad1[2]; }; 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 { @@ -407,6 +394,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 { 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); 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; 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_ */ 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/hud/hudconfig.cpp b/code/hud/hudconfig.cpp index e1855e614d1..739f473af53 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; @@ -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) { @@ -2006,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 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/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/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/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; 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/code/lab/dialogs/lab_ui.cpp b/code/lab/dialogs/lab_ui.cpp index aa0eca46a41..2770deab4c4 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++; @@ -1035,37 +1029,10 @@ 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() { - 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, @@ -1076,10 +1043,35 @@ 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())) { auto& scripted_triggers = manual_animation_triggers[trigger_type]; auto direction = scripted_triggers[anim_trigger.name]; do_triggered_anim(trigger_type, @@ -1094,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") @@ -1101,7 +1269,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) { @@ -1141,6 +1309,9 @@ void LabUi::build_animation_options(ship* shipp, ship_info* sip) const maybe_show_animation_category(anim_triggers, animation::ModelAnimationTriggerType::Docking_Stage3, "Docking stage 3##anims"); + maybe_show_animation_category(anim_triggers, + animation::ModelAnimationTriggerType::Docked, + "Docked animations##anims"); } } @@ -1233,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]; @@ -1243,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 79aaf2f2313..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; @@ -60,7 +67,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, 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 dec81cc5255..c72a9c4aa56 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" @@ -48,7 +49,6 @@ LabManager::LabManager() { shockwave_level_init(); ship_level_init(); shipfx_flash_init(); - mflash_page_in(true); weapon_level_init(); beam_level_init(); particle::init(); @@ -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,13 +427,19 @@ void LabManager::cleanup() { CurrentClass = -1; DockerDockPoint.clear(); DockeeDockPoint.clear(); + BayPathMask = 0; CurrentPosition = vmd_zero_vector; CurrentOrientation = vmd_identity_matrix; ModelFilename = ""; Player_ship = nullptr; + + Lab_object_detail_level = -1; } +} - Cmdline_dis_collisions = Saved_cmdline_collisions_value; +void LabManager::deleteTestObjects() { + deleteDockerObject(); + deleteBayObject(); } void LabManager::deleteDockerObject() { @@ -439,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; @@ -479,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(); @@ -488,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); @@ -572,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 @@ -607,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); @@ -615,6 +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. 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 ea3bcc363b4..6d0418a2a66 100644 --- a/code/lab/manager/lab_manager.h +++ b/code/lab/manager/lab_manager.h @@ -36,23 +36,37 @@ 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(); cleanup(); + Cmdline_dis_collisions = Saved_cmdline_collisions_value; + LabRenderer::close(); // Unload any asteroids that were loaded @@ -76,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; 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/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/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); 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/math/vecmat.cpp b/code/math/vecmat.cpp index 526baf08693..d356c2e00ce 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 @@ -2975,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 } 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/mission/import/xwingbrflib.cpp b/code/mission/import/xwingbrflib.cpp new file mode 100644 index 00000000000..86aec8a5643 --- /dev/null +++ b/code/mission/import/xwingbrflib.cpp @@ -0,0 +1,40 @@ + +#include +#include +#include "xwingbrflib.h" + + +bool XWingBriefing::load(XWingBriefing *b, const char *data) +{ + // parse header + xwi_brf_header *h = (xwi_brf_header *)data; + if (h->version != 2) + return false; + + // h->icon_count == numShips + b->ships.resize(h->icon_count); + + 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/import/xwingbrflib.h b/code/mission/import/xwingbrflib.h new file mode 100644 index 00000000000..a60c3e96c5b --- /dev/null +++ b/code/mission/import/xwingbrflib.h @@ -0,0 +1,44 @@ +#pragma once +#include +#include + +std::map > 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 { + float coordinates_x, coordinates_y, coordinates_z; + 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: + + static bool load(XWingBriefing *b, const char *data); + + std::string message1; + xwi_brf_header header; + std::vector ships; +}; + diff --git a/code/mission/import/xwinglib.cpp b/code/mission/import/xwinglib.cpp new file mode 100644 index 00000000000..0c9c39bdeec --- /dev/null +++ b/code/mission/import/xwinglib.cpp @@ -0,0 +1,791 @@ + +#include +#include +#include "xwinglib.h" + +#pragma pack(push, 1) +struct xwi_header { + short version; + short mission_time_limit; + short end_event; + short rnd_seed; + 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 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_markings1; + short craft_markings2; + short objective; + short primary_target; + short secondary_target; +}; + +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 number_of_objects; + short object_x; + short object_y; + short object_z; + short object_yaw; + short object_pitch; + short object_roll; +}; +#pragma pack(pop) + +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 - 20) * 6; +} + +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 = XWMEndEvent::ev_rescued; + break; + case 1: + m->endEvent = XWMEndEvent::ev_captured; + break; + case 5: + m->endEvent = XWMEndEvent::ev_hit_exhaust_port; + break; + default: + return false; + } + + m->rnd_seed = h->rnd_seed; + + switch(h->mission_location) { + case 0: + m->missionLocation = XWMMissionLocation::ml_deep_space; + break; + case 1: + m->missionLocation = XWMMissionLocation::ml_death_star; + if (m->endEvent == XWMEndEvent::ev_captured) + m->endEvent = XWMEndEvent::ev_cleared_laser_turrets; + break; + default: + return 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++) { + 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; + nfg->specialCargo = fg->special_cargo; + nfg->specialShipNumber = fg->special_ship_number; + + switch(fg->flight_group_type) { + case 0: + nfg->flightGroupType = XWMFlightGroupType::fg_None; + break; + case 1: + nfg->flightGroupType = XWMFlightGroupType::fg_X_Wing; + 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; + break; + case 4: + nfg->flightGroupType = XWMFlightGroupType::fg_TIE_Fighter; + break; + case 5: + nfg->flightGroupType = XWMFlightGroupType::fg_TIE_Interceptor; + break; + case 6: + nfg->flightGroupType = XWMFlightGroupType::fg_TIE_Bomber; + break; + case 7: + nfg->flightGroupType = XWMFlightGroupType::fg_Gunboat; + break; + case 8: + nfg->flightGroupType = XWMFlightGroupType::fg_Transport; + break; + case 9: + nfg->flightGroupType = XWMFlightGroupType::fg_Shuttle; + break; + case 10: + nfg->flightGroupType = XWMFlightGroupType::fg_Tug; + break; + case 11: + nfg->flightGroupType = XWMFlightGroupType::fg_Container; + break; + case 12: + nfg->flightGroupType = XWMFlightGroupType::fg_Freighter; + break; + case 13: + nfg->flightGroupType = XWMFlightGroupType::fg_Calamari_Cruiser; + break; + case 14: + nfg->flightGroupType = XWMFlightGroupType::fg_Nebulon_B_Frigate; + break; + case 15: + nfg->flightGroupType = XWMFlightGroupType::fg_Corellian_Corvette; + break; + case 16: + nfg->flightGroupType = XWMFlightGroupType::fg_Imperial_Star_Destroyer; + break; + case 17: + nfg->flightGroupType = XWMFlightGroupType::fg_TIE_Advanced; + break; + case 18: + nfg->flightGroupType = XWMFlightGroupType::fg_B_Wing; + break; + default: + return false; + } + + switch(fg->craft_iff) { + case 0: + nfg->craftIFF = XWMCraftIFF::iff_default; + break; + case 1: + nfg->craftIFF = XWMCraftIFF::iff_rebel; + break; + case 2: + nfg->craftIFF = XWMCraftIFF::iff_imperial; + break; + case 3: + nfg->craftIFF = XWMCraftIFF::iff_neutral; + break; + } + + switch(fg->craft_status) { + case 0: + nfg->craftStatus = XWMCraftStatus::cs_normal; + break; + case 1: + nfg->craftStatus = XWMCraftStatus::cs_no_missiles; + break; + case 2: + nfg->craftStatus = XWMCraftStatus::cs_half_missiles; + break; + case 3: + nfg->craftStatus = XWMCraftStatus::cs_no_shields; + break; + case 4: + // XXX Used by CENTURY 1 in CONVOY2.XWI + nfg->craftStatus = XWMCraftStatus::cs_normal; + break; + case 10: + // XXX Used by Unnammed B-Wing group in DESUPPLY.XWI + nfg->craftStatus = XWMCraftStatus::cs_normal; + break; + case 11: + // XXX Used by PROTOTYPE 2 in T5H1WB.XWI + nfg->craftStatus = XWMCraftStatus::cs_no_missiles; + break; + case 12: + // XXX Used by RED 1 in T5M19MB.XWI + nfg->craftStatus = XWMCraftStatus::cs_half_missiles; + break; + default: + return false; + } + + nfg->numberInWave = fg->number_in_wave; + nfg->numberOfWaves = fg->number_of_waves + 1; + + switch(fg->arrival_event) { + case 0: + nfg->arrivalEvent = XWMArrivalEvent::ae_mission_start; + break; + case 1: + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_arrived; + break; + case 2: + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_destroyed; + break; + case 3: + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_attacked; + break; + case 4: + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_captured; + break; + case 5: + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_identified; + break; + case 6: + nfg->arrivalEvent = XWMArrivalEvent::ae_afg_disabled; + break; + default: + return false; + } + + nfg->arrivalDelay = arrival_delay_to_seconds(fg->arrival_delay); + + nfg->arrivalFlightGroup = fg->arrival_flight_group; + nfg->mothership = fg->mothership; + 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; + 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->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->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 = XWMFormation::f_Vic; + break; + case 1: + nfg->formation = XWMFormation::f_Finger_Four; + break; + case 2: + nfg->formation = XWMFormation::f_Line_Astern; + break; + case 3: + nfg->formation = XWMFormation::f_Line_Abreast; + break; + case 4: + nfg->formation = XWMFormation::f_Echelon_Right; + break; + case 5: + nfg->formation = XWMFormation::f_Echelon_Left; + break; + case 6: + nfg->formation = XWMFormation::f_Double_Astern; + break; + case 7: + nfg->formation = XWMFormation::f_Diamond; + break; + case 8: + nfg->formation = XWMFormation::f_Stacked; + break; + case 9: + nfg->formation = XWMFormation::f_Spread; + break; + case 10: + nfg->formation = XWMFormation::f_Hi_Lo; + break; + case 11: + nfg->formation = XWMFormation::f_Spiral; + break; + default: + return false; + } + + nfg->playerPos = fg->player_pos; + + switch(fg->craft_ai) { + case 0: + nfg->craftAI = XWMCraftAI::ai_Rookie; + break; + case 1: + nfg->craftAI = XWMCraftAI::ai_Officer; + break; + case 2: + nfg->craftAI = XWMCraftAI::ai_Veteran; + break; + case 3: + nfg->craftAI = XWMCraftAI::ai_Ace; + break; + case 4: + nfg->craftAI = XWMCraftAI::ai_Top_Ace; + break; + default: + return false; + } + + switch(fg->order) { + case 0: + nfg->craftOrder = XWMCraftOrder::o_Hold_Steady; + break; + case 1: + nfg->craftOrder = XWMCraftOrder::o_Fly_Home; + break; + case 2: + nfg->craftOrder = XWMCraftOrder::o_Circle_And_Ignore; + break; + case 3: + nfg->craftOrder = XWMCraftOrder::o_Fly_Once_And_Ignore; + break; + case 4: + nfg->craftOrder = XWMCraftOrder::o_Circle_And_Evade; + break; + case 5: + nfg->craftOrder = XWMCraftOrder::o_Fly_Once_And_Evade; + break; + case 6: + nfg->craftOrder = XWMCraftOrder::o_Close_Escort; + break; + case 7: + nfg->craftOrder = XWMCraftOrder::o_Loose_Escort; + break; + case 8: + nfg->craftOrder = XWMCraftOrder::o_Attack_Escorts; + break; + case 9: + nfg->craftOrder = XWMCraftOrder::o_Attack_Pri_And_Sec_Targets; + break; + case 10: + nfg->craftOrder = XWMCraftOrder::o_Attack_Enemies; + break; + case 11: + nfg->craftOrder = XWMCraftOrder::o_Rendezvous; + break; + case 12: + nfg->craftOrder = XWMCraftOrder::o_Disabled; + break; + case 13: + nfg->craftOrder = XWMCraftOrder::o_Board_To_Deliver; + break; + case 14: + nfg->craftOrder = XWMCraftOrder::o_Board_To_Take; + break; + case 15: + nfg->craftOrder = XWMCraftOrder::o_Board_To_Exchange; + break; + case 16: + nfg->craftOrder = XWMCraftOrder::o_Board_To_Capture; + break; + case 17: + nfg->craftOrder = XWMCraftOrder::o_Board_To_Destroy; + break; + case 18: + nfg->craftOrder = XWMCraftOrder::o_Disable_Pri_And_Sec_Targets; + break; + case 19: + nfg->craftOrder = XWMCraftOrder::o_Disable_All; + break; + case 20: + nfg->craftOrder = XWMCraftOrder::o_Attack_Transports; + break; + case 21: + nfg->craftOrder = XWMCraftOrder::o_Attack_Freighters; + break; + case 22: + nfg->craftOrder = XWMCraftOrder::o_Attack_Starships; + break; + case 23: + nfg->craftOrder = XWMCraftOrder::o_Attack_Satellites_And_Mines; + break; + case 24: + nfg->craftOrder = XWMCraftOrder::o_Disable_Freighters; + break; + case 25: + nfg->craftOrder = XWMCraftOrder::o_Disable_Starships; + break; + case 26: + nfg->craftOrder = XWMCraftOrder::o_Starship_Sit_And_Fire; + break; + case 27: + nfg->craftOrder = XWMCraftOrder::o_Starship_Fly_Dance; + break; + case 28: + nfg->craftOrder = XWMCraftOrder::o_Starship_Circle; + break; + case 29: + nfg->craftOrder = XWMCraftOrder::o_Starship_Await_Return; + break; + case 30: + nfg->craftOrder = XWMCraftOrder::o_Starship_Await_Launch; + break; + case 31: + nfg->craftOrder = XWMCraftOrder::o_Starship_Await_Boarding; + break; + case 32: + // XXX Used by T-FORCE 1 in LEIA.XWI + nfg->craftOrder = XWMCraftOrder::o_Attack_Enemies; + break; + default: + return false; + } + + nfg->dockTime = fg->dock_time_or_throttle; + nfg->Throttle = fg->dock_time_or_throttle; + + switch(fg->craft_markings1) { + case 0: + nfg->craftColor = XWMCraftColor::c_Red; + break; + case 1: + nfg->craftColor = XWMCraftColor::c_Gold; + break; + case 2: + nfg->craftColor = XWMCraftColor::c_Blue; + break; + case 3: + nfg->craftColor = XWMCraftColor::c_Green; + break; + default: + return false; + } + + nfg->craftMarkings = fg->craft_markings2; + + switch(fg->objective) { + case 0: + nfg->objective = XWMObjective::o_None; + break; + case 1: + nfg->objective = XWMObjective::o_All_Destroyed; + break; + case 2: + nfg->objective = XWMObjective::o_All_Survive; + break; + case 3: + nfg->objective = XWMObjective::o_All_Captured; + break; + case 4: + nfg->objective = XWMObjective::o_All_Docked; + break; + case 5: + nfg->objective = XWMObjective::o_Special_Craft_Destroyed; + break; + case 6: + nfg->objective = XWMObjective::o_Special_Craft_Survive; + break; + case 7: + nfg->objective = XWMObjective::o_Special_Craft_Captured; + break; + case 8: + nfg->objective = XWMObjective::o_Special_Craft_Docked; + break; + case 9: + nfg->objective = XWMObjective::o_50_Percent_Destroyed; + break; + case 10: + nfg->objective = XWMObjective::o_50_Percent_Survive; + break; + case 11: + nfg->objective = XWMObjective::o_50_Percent_Captured; + break; + case 12: + nfg->objective = XWMObjective::o_50_Percent_Docked; + break; + case 13: + nfg->objective = XWMObjective::o_All_Identified; + break; + case 14: + nfg->objective = XWMObjective::o_Special_Craft_Identifed; + break; + case 15: + nfg->objective = XWMObjective::o_50_Percent_Identified; + break; + case 16: + nfg->objective = XWMObjective::o_Arrive; + break; + default: + return false; + } + + // XXX LEVEL1.XWI seems to set primaryTarget to junk + if (nfg->objective == XWMObjective::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); + } + + for (int n = 0; n < h->number_of_objects; n++) { + xwi_objectgroup *oj = + (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; + + switch (oj->object_type) { + case 18: + noj->objectType = XWMObjectType::oj_Mine1; + break; + case 19: + noj->objectType = XWMObjectType::oj_Mine2; + break; + case 20: + noj->objectType = XWMObjectType::oj_Mine3; + break; + case 21: + noj->objectType = XWMObjectType::oj_Mine4; + break; + case 22: + noj->objectType = XWMObjectType::oj_Satellite; + break; + case 23: + noj->objectType = XWMObjectType::oj_Nav_Buoy; + break; + case 24: + noj->objectType = XWMObjectType::oj_Probe; + break; + case 26: + noj->objectType = XWMObjectType::oj_Asteroid1; + break; + case 27: + noj->objectType = XWMObjectType::oj_Asteroid2; + break; + case 28: + noj->objectType = XWMObjectType::oj_Asteroid3; + break; + case 29: + noj->objectType = XWMObjectType::oj_Asteroid4; + break; + case 30: + noj->objectType = XWMObjectType::oj_Asteroid5; + break; + case 31: + noj->objectType = XWMObjectType::oj_Asteroid6; + break; + case 32: + noj->objectType = XWMObjectType::oj_Asteroid7; + break; + case 33: + noj->objectType = XWMObjectType::oj_Asteroid8; + break; + case 34: + noj->objectType = XWMObjectType::oj_Rock_World; + break; + case 35: + noj->objectType = XWMObjectType::oj_Gray_Ring_World; + break; + case 36: + noj->objectType = XWMObjectType::oj_Gray_World; + break; + case 37: + noj->objectType = XWMObjectType::oj_Brown_World; + break; + case 38: + noj->objectType = XWMObjectType::oj_Gray_World2; + break; + case 39: + noj->objectType = XWMObjectType::oj_Planet_and_Moon; + break; + case 40: + noj->objectType = XWMObjectType::oj_Gray_Crescent; + break; + case 41: + noj->objectType = XWMObjectType::oj_Orange_Crescent1; + break; + case 42: + noj->objectType = XWMObjectType::oj_Orange_Crescent2; + break; + case 43: + noj->objectType = XWMObjectType::oj_Orange_Crescent3; + break; + case 44: + noj->objectType = XWMObjectType::oj_Orange_Crescent4; + break; + case 45: + noj->objectType = XWMObjectType::oj_Orange_Crescent5; + break; + case 46: + noj->objectType = XWMObjectType::oj_Orange_Crescent6; + break; + case 47: + noj->objectType = XWMObjectType::oj_Orange_Crescent7; + break; + case 48: + noj->objectType = XWMObjectType::oj_Orange_Crescent8; + break; + 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; + } + + 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 + // 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) { + 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; + } + } + + 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; + 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/xwinglib.h b/code/mission/import/xwinglib.h new file mode 100644 index 00000000000..a642cccbd22 --- /dev/null +++ b/code/mission/import/xwinglib.h @@ -0,0 +1,320 @@ + +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_arrived, + ae_afg_destroyed, + ae_afg_attacked, + ae_afg_captured, + 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, + c_Green +}; + +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 +}; + +enum class XWMObjectType : short +{ + oj_Mine1 = 0x12, + oj_Mine2, + oj_Mine3, + oj_Mine4, + oj_Satellite, + oj_Nav_Buoy, + oj_Probe, + oj_Asteroid1 = 0x1A, + 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, + oj_Training_Platform1 = 0x3A, + 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_FloorXY = 0, + ojf_SideYZ, + ojf_FrontXZ, + ojf_Scattered // may be buggy - undefined locations +}; + +enum class XWMObjectGoal : short +{ + ojg_Neither = 0, + ojg_Destroyed, + ojg_Survive +}; + +class XWMFlightGroup +{ +public: + + std::string designation; + std::string cargo; + std::string specialCargo; + + int specialShipNumber; + + XWMFlightGroupType flightGroupType; + + XWMCraftIFF craftIFF; + + XWMCraftStatus craftStatus; + + int numberInWave; + int numberOfWaves; + + XWMArrivalEvent arrivalEvent; + + int arrivalDelay; + + int arrivalFlightGroup; + + int mothership; + + bool arriveByHyperspace; + bool departByHyperspace; + + float start1_x, start1_y, start1_z; + float start2_x, start2_y, start2_z; + float start3_x, start3_y, start3_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; + + 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; + + XWMCraftAI craftAI; + + XWMCraftOrder craftOrder; + + int dockTime; + int Throttle; + + XWMCraftColor craftColor; + + short craftMarkings; + + XWMObjective objective; + + int primaryTarget; + int secondaryTarget; +}; + +class XWMObject +{ +public: + + XWMObjectType objectType; + + XWMObjectFormation formation; + + XWMObjectGoal objectGoal; + + int numberOfObjects; + + float object_x, object_y, object_z; + float object_yaw, object_pitch, object_roll; +}; + + +enum class XWMEndEvent : short +{ + 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 int arrival_delay_to_seconds(int delay); + static bool load(XWingMission *xwim, const char *data); + + int missionTimeLimit; + XWMEndEvent endEvent; + short rnd_seed; // Not used + XWMMissionLocation missionLocation; + + std::string completionMsg1; + std::string completionMsg2; + std::string completionMsg3; + + std::vector flightgroups; + std::vector objects; +}; diff --git a/code/mission/import/xwingmissionparse.cpp b/code/mission/import/xwingmissionparse.cpp new file mode 100644 index 00000000000..0ac24dd566e --- /dev/null +++ b/code/mission/import/xwingmissionparse.cpp @@ -0,0 +1,1049 @@ +#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" +#include "ship/ship.h" +#include "species_defs/species_defs.h" +#include "starfield/starfield.h" +#include "weapon/weapon.h" + +#include "xwingbrflib.h" +#include "xwinglib.h" +#include "xwingmissionparse.h" + +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) +{ + 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_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); +} + +bool is_fighter_or_bomber(const XWMFlightGroup *fg) +{ + switch (fg->flightGroupType) + { + 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; + default: + break; + } + return false; +} + +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[NAME_LENGTH + 50]; + + 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", fg_index); + + if (mission_event_lookup(event_name) >= 0) + return; + + 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); + 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) +{ + const XWMFlightGroup *arrival_fg = nullptr; + char arrival_fg_name[NAME_LENGTH] = ""; + char sexp_buf[NAME_LENGTH * 2 + 80]; + + bool check_wing = false; + if (fg->arrivalFlightGroup >= 0) + { + 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_mission_start) + return Locked_sexp_true; + + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_arrived) + { + 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\" )", arrival_fg_name); + else + sprintf(sexp_buf, "( fotg-is-ship-attacked \"%s\" )", arrival_fg_name); + Mp = sexp_buf; + return get_sexp_main(); + } + + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_captured) + { + if (check_wing) + sprintf(sexp_buf, "( fotg-is-wing-captured \"%s\" )", arrival_fg_name); + else + sprintf(sexp_buf, "( fotg-is-ship-captured \"%s\" )", arrival_fg_name); + Mp = sexp_buf; + return get_sexp_main(); + } + + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_destroyed) + { + // 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(); + } + + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_disabled) + { + if (check_wing) + sprintf(sexp_buf, "( fotg-is-wing-disabled \"%s\" )", arrival_fg_name); + else + sprintf(sexp_buf, "( fotg-is-ship-disabled \"%s\" )", arrival_fg_name); + Mp = sexp_buf; + return get_sexp_main(); + } + + if (fg->arrivalEvent == XWMArrivalEvent::ae_afg_identified) + { + if (check_wing) + sprintf(sexp_buf, "( fotg-is-wing-identified \"%s\" )", arrival_fg_name); + else + sprintf(sexp_buf, "( fotg-is-ship-identified \"%s\" )", arrival_fg_name); + Mp = sexp_buf; + return get_sexp_main(); + } + + return Locked_sexp_true; +} + +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()); + 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_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) + { + case XWMFlightGroupType::fg_X_Wing: + return "T-65c X-wing"; + case XWMFlightGroupType::fg_Y_Wing: + return "BTL-A4 Y-wing"; + case XWMFlightGroupType::fg_A_Wing: + return "RZ-1 A-wing"; + case XWMFlightGroupType::fg_TIE_Fighter: + return "TIE/ln Fighter"; + case XWMFlightGroupType::fg_TIE_Interceptor: + return "TIE/In Interceptor"; + case XWMFlightGroupType::fg_TIE_Bomber: + return "TIE/sa Bomber"; + case XWMFlightGroupType::fg_Gunboat: + return "XG-1 Star Wing"; + case XWMFlightGroupType::fg_Transport: + return "DX-9 Stormtrooper Transport"; + case XWMFlightGroupType::fg_Shuttle: + return "Lambda T-4a Shuttle"; + case XWMFlightGroupType::fg_Tug: + return "DV-3 Freighter"; + case XWMFlightGroupType::fg_Container: + return "BFF-1 Container"; + case XWMFlightGroupType::fg_Freighter: + return "BFF-1 Freighter"; + case XWMFlightGroupType::fg_Calamari_Cruiser: + return "Liberty Type Star Cruiser"; + case XWMFlightGroupType::fg_Nebulon_B_Frigate: + return "Nebulon-B Frigate"; + case XWMFlightGroupType::fg_Corellian_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 "ASF-01 B-wing"; + default: + break; + } + + return nullptr; +} + +int xwi_determine_ship_class(const XWMFlightGroup *fg) +{ + // base ship class must exist + auto class_name = xwi_determine_base_ship_class(fg); + if (class_name == nullptr) + return -1; + + // 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()) + { + SCP_string variant_name = class_name; + bool variant = false; + + // 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; + } + else if (fg->craftColor == XWMCraftColor::c_Green) + { + variant_name += "#green"; + 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_name.c_str(), fg->designation.c_str()); + } + } + + // no variant, or we're just going with the base class + return base_class; +} + +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) + { + if (player_iff == XWMCraftIFF::iff_imperial) + return "Friendly"; + if (player_iff == XWMCraftIFF::iff_rebel) + return "Hostile"; + } + + if (fg->craftIFF == XWMCraftIFF::iff_rebel) + { + if (player_iff == XWMCraftIFF::iff_imperial) + return "Hostile"; + if (player_iff == XWMCraftIFF::iff_rebel) + return "Friendly"; + } + + if (fg->craftIFF == XWMCraftIFF::iff_neutral) + return "Civilian"; + + return nullptr; +} + +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) + { + if (Num_cargo == MAX_CARGO) + { + Warning(LOCATION, "Can't add any more cargo!"); + return 0; + } + + index = Num_cargo++; + strcpy(Cargo_names[index], cargo_name); + SCP_totitle(Cargo_names[index]); + } + return index; +} + +const char *xwi_determine_ai_class(const XWMFlightGroup *fg) +{ + // Rookie = Cadet + // Officer = Officer + // Veteran = Captain + // Ace = Commander + // Top Ace = General + + switch (fg->craftAI) + { + case XWMCraftAI::ai_Rookie: + return "Cadet"; + case XWMCraftAI::ai_Officer: + return "Officer"; + case XWMCraftAI::ai_Veteran: + return "Captain"; + case XWMCraftAI::ai_Ace: + return "Commander"; + case XWMCraftAI::ai_Top_Ace: + return "General"; + } + + return nullptr; +} + +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: + // 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); +} + +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; + 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; + 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 ? 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 ? 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 = ArrivalLocation::AT_LOCATION; + if (wingp->departure_anchor < 0) + wingp->departure_location = DepartureLocation::AT_LOCATION; + + wingp->wave_count = number_in_wave; + } + + // 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 the AI + int ai_index = sip->ai_class; + auto ai_name = xwi_determine_ai_class(fg); + if (ai_name) + { + int index = string_lookup(ai_name, Ai_class_names, Num_ai_classes); + if (index >= 0) + ai_index = index; + else + Warning(LOCATION, "Could not find AI class %s", ai_name); + } + + // similarly for any waypoints + // 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); + 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(&hyperspace, 1000); + + matrix orient; + xwi_determine_orientation(&orient, fg, &start1, &start2, &start3, &waypoint1, &waypoint2, &waypoint3, &hyperspace); + + // now configure each ship in the flight group + 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; + pobj.departure_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 ? 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 ? 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 = ArrivalLocation::AT_LOCATION; + if (pobj.departure_anchor < 0) + pobj.departure_location = DepartureLocation::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()); + + if (fg->craftOrder != XWMCraftOrder::o_Hold_Steady && fg->craftOrder != XWMCraftOrder::o_Starship_Sit_And_Fire) + pobj.initial_velocity = 100; + + if (fg->playerPos == wing_index + 1) + { + // undo any previously set player + if (Player_starts > 0) + { + 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); + 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 + 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); + pobj.flags.set(Mission::Parse_Object_Flags::SF_Cannot_perform_scan); + Player_starts++; + } + + if (fg->craftStatus == XWMCraftStatus::cs_no_shields) + pobj.flags.set(Mission::Parse_Object_Flags::OF_No_shields); + + 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; + int this_subsys = allocate_subsys_status(); + pobj.subsys_count++; + strcpy_s(Subsys_status[this_subsys].name, NOX("Pilot")); + + 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 == XWMCraftStatus::cs_no_missiles) ? 0 : 50; + } + } + + Parse_objects.push_back(pobj); + } +} + +const char *xwi_determine_object_class(const XWMObject *oj) +{ + switch (oj->objectType) { + case XWMObjectType::oj_Mine1: + case XWMObjectType::oj_Mine2: + case XWMObjectType::oj_Mine3: + 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 "Asteroid#Small01"; + case XWMObjectType::oj_Asteroid2: + return "Asteroid#Small02"; + case XWMObjectType::oj_Asteroid3: + return "Asteroid#Medium01"; + case XWMObjectType::oj_Asteroid4: + return "Asteroid#Medium02"; + case XWMObjectType::oj_Asteroid5: + return "Asteroid#Medium03"; + case XWMObjectType::oj_Asteroid6: + return "Asteroid#Big01"; + case XWMObjectType::oj_Asteroid7: + return "Asteroid#Big02"; + case XWMObjectType::oj_Asteroid8: + return "Asteroid#Big03"; + default: + break; + } + return nullptr; +} + +vec3d xwi_determine_mine_formation_position(const XWMObject* oj, float objectPosX, float objectPosY, float objectPosZ, + 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 + offsetAxisA), objectPosZ, (objectPosY + offsetAxisB)); + case XWMObjectFormation::ojf_SideYZ: + return vm_vec_new(objectPosX, (objectPosZ + offsetAxisA), (objectPosY + offsetAxisB)); + case XWMObjectFormation::ojf_FrontXZ: + return vm_vec_new((objectPosX + offsetAxisA), (objectPosZ + offsetAxisB), 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_orient(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; +} + +// 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 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]; + 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, int &object_count) +{ + SCP_UNUSED(pm); + + 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; + + // 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 + 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 + 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); + + 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; + + 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); + + 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; + } + + // 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); + + // 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 < (int)Parse_objects.size(); n++) { + objectNameSet.insert(Parse_objects[n].name); + } + + // 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 + 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 + (mine_dist * a), offsetAxisB + (mine_dist * b)); + + p_object pobj; + strcpy_s(pobj.name, xwi_determine_space_object_name(objectNameSet, class_name, og_index)); + pobj.ship_class = ship_class; + + pobj.arrival_cue = Locked_sexp_true; + pobj.arrival_location = ArrivalLocation::AT_LOCATION; + pobj.departure_cue = Locked_sexp_false; + pobj.departure_location = DepartureLocation::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; + + 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")); + + 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: + break; + } + + 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 = 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); + } + } +} + +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 < (int)xwim->flightgroups.size(); i++) + { + if (xwim->flightgroups[i].playerPos > 0) + { + index = i; + // don't break in case multiple FGs set a player - we will use the last one assigned + } + } + if (index >= 0) + Player_flight_group = index; + else + { + Warning(LOCATION, "Player flight group not found?"); + 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]); + + // indicate we are using X-Wing options + 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(); + + // 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); + + // load object groups + int object_count = 0; + for (const auto& obj : xwim->objects) + parse_xwi_objectgroup(pm, xwim, &obj, object_count); +} + +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]; + 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]; + + 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; + } + } + } +} + +/** +* 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, const XWingBriefing *xwBrief) +{ + SCP_UNUSED(pm); + SCP_UNUSED(xwBrief); + + auto bp = &Briefings[0]; + bp->num_stages = 1; // xwing briefings only have one stage + auto 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 X-Wing mission!"; + strcpy_s(bs->voice, "none.wav"); + vm_vec_zero(&bs->camera_pos); + bs->camera_orient = IDENTITY_MATRIX; + 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/import/xwingmissionparse.h b/code/mission/import/xwingmissionparse.h new file mode 100644 index 00000000000..7e15b017f4d --- /dev/null +++ b/code/mission/import/xwingmissionparse.h @@ -0,0 +1,12 @@ +#ifndef _XWI_PARSE_H +#define _XWI_PARSE_H + +struct mission; +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 diff --git a/code/mission/missioncampaign.cpp b/code/mission/missioncampaign.cpp index 27942cab0c7..e754f2b2105 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; @@ -504,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(); @@ -1245,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 f8af4fbde96..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) @@ -213,7 +214,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/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/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..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; @@ -239,6 +241,7 @@ struct Persona { char name[NAME_LENGTH]; int flags; int species_bitfield; + SCP_map custom_data; }; extern SCP_vector Personas; diff --git a/code/mission/missionparse.cpp b/code/mission/missionparse.cpp index 97c40da847e..e8174c7633b 100644 --- a/code/mission/missionparse.cpp +++ b/code/mission/missionparse.cpp @@ -46,6 +46,9 @@ #include "mission/missionlog.h" #include "mission/missionmessage.h" #include "mission/missionparse.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" @@ -267,6 +270,156 @@ 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 +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 }, @@ -401,6 +554,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 }, @@ -625,15 +807,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:") ) { @@ -2036,6 +2218,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; @@ -2792,9 +2975,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); @@ -3360,6 +3540,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 @@ -4580,7 +4769,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 +4950,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); + } } } @@ -6264,7 +6442,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; @@ -6279,34 +6457,44 @@ 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); - parse_custom_data(pm); + 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); + parse_custom_data(pm); + } // if we couldn't load some mod data if ((Num_unknown_ship_classes > 0) || ( Num_unknown_loadout_classes > 0 )) { @@ -6363,6 +6551,8 @@ bool parse_mission(mission *pm, 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]; @@ -6661,7 +6851,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); } } } @@ -6674,7 +6865,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 { @@ -6907,7 +7102,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. @@ -6932,12 +7127,48 @@ bool parse_main(const char *mission_name, int flags) 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); + XWingMission xwim; + if (!XWingMission::load(&xwim, Parse_text_raw)) + throw parse::ParseException("Could not parse XWI mission!"); + rval = parse_mission(&The_mission, &xwim, flags); + + if (rval) + { + strcpy(ch, ".BRF"); + 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 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 4b56a3c97ce..e4ad27895c0 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 @@ -64,7 +67,8 @@ extern bool check_for_24_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 @@ -86,6 +90,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 { @@ -118,6 +131,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; @@ -137,6 +166,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 { @@ -271,10 +310,22 @@ 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_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; +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[]; @@ -336,11 +387,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 @@ -389,6 +462,7 @@ class p_object flagset flags; // mission savable flags int escort_priority = 0; // priority in escort list + 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; 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/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 2013236cff3..a2d35cb603b 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; @@ -935,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); } @@ -1009,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); @@ -2807,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, @@ -2820,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/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/mod_table/mod_table.cpp b/code/mod_table/mod_table.cpp index bce433f2ced..917dbec1fc6 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; @@ -45,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; @@ -171,6 +176,9 @@ 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; +float Shield_percent_skips_damage; +float Min_radius_for_persistent_debris; #ifdef WITH_DISCORD @@ -1134,6 +1142,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:")) { @@ -1209,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) @@ -1534,6 +1584,25 @@ 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); + } + + 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; + } + } + + 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 @@ -1618,6 +1687,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; @@ -1630,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; @@ -1769,6 +1843,9 @@ 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; + Shield_percent_skips_damage = 0.1f; + Min_radius_for_persistent_debris = 50.0f; } void mod_table_set_version_flags() @@ -1794,5 +1871,7 @@ 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; + Skybox_internal_depth_consistency = true; } } diff --git a/code/mod_table/mod_table.h b/code/mod_table/mod_table.h index 85878b6f1b8..f1172e0ec76 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; @@ -58,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; @@ -186,6 +191,9 @@ 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; +extern float Shield_percent_skips_damage; +extern float Min_radius_for_persistent_debris; void mod_table_init(); void mod_table_post_process(); diff --git a/code/model/model.h b/code/model/model.h index fb5d0efe99f..d0471f08c22 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,10 @@ class model_subsystem { /* contains rotation rate info */ float density; + particle::ParticleEffectHandle death_effect; + particle::ParticleEffectHandle debris_flame_particles; + particle::ParticleEffectHandle shrapnel_flame_particles; + void reset(); model_subsystem(); @@ -724,13 +729,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 +807,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 +820,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 +894,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 @@ -1012,6 +1011,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 @@ -1365,7 +1365,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/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/model/modelcollide.cpp b/code/model/modelcollide.cpp index 7984135de2f..c6fe18afa20 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() @@ -324,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; @@ -577,11 +577,11 @@ void model_collide_parse_bsp_flatpoly(bsp_collision_leaf *leaf, SCP_vectororient; 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/model/modelinterp.cpp b/code/model/modelinterp.cpp index 9896a548851..1b200060457 100644 --- a/code/model/modelinterp.cpp +++ b/code/model/modelinterp.cpp @@ -97,6 +97,8 @@ class bsp_polygon_data int Num_flat_polies; int Num_flat_verts; + int bsp_data_size; + void process_bsp(int offset, ubyte* bsp_data); void process_defpoints(int off, ubyte* bsp_data); void process_sortnorm(int offset, ubyte* bsp_data); @@ -105,7 +107,7 @@ class bsp_polygon_data void process_tmap2(int offset, ubyte* bsp_data); void process_flat(int offset, ubyte* bsp_data); public: - bsp_polygon_data(ubyte* bsp_data); + bsp_polygon_data(ubyte* bsp_data, int bsp_data_size); int get_num_triangles(int texture); int get_num_lines(int texture); @@ -1322,6 +1324,8 @@ int submodel_get_num_polys_sub( ubyte *p ) Int3(); // Bad chunk type! return 0; } + if (end) break; + p += chunk_size; chunk_type = w(p); chunk_size = w(p+4); @@ -1810,6 +1814,7 @@ void parse_bsp(int offset, ubyte *bsp_data) default: return; } + if (end) break; offset += size; id = w(bsp_data+offset); @@ -1922,6 +1927,7 @@ void find_tri_counts(int offset, ubyte *bsp_data) default: return; } + if (end) break; offset += size; id = w(bsp_data+offset); @@ -2173,7 +2179,7 @@ void interp_configure_vertex_buffers(polymodel *pm, int mn, const model_read_def int milliseconds = timer_get_milliseconds(); - bsp_polygon_data *bsp_polies = new bsp_polygon_data(model->bsp_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 f16aa16461d..11f671cb817 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 @@ -488,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 @@ -1063,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 ) @@ -1655,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; @@ -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); @@ -2804,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; + for(int idx3 = 0; idx3 < 3; idx3++){ + faces[idx3] = cfread_int(fp); - //get three points (rotated) and compute normal - - 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 @@ -3014,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); } } } @@ -3555,7 +3576,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 @@ -3895,6 +3919,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 ); @@ -4359,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 @@ -5720,6 +5749,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/modelrender.cpp b/code/model/modelrender.cpp index 6ba9f0866e8..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); @@ -639,51 +641,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; @@ -2094,17 +2051,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; } @@ -2118,16 +2075,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; @@ -2425,74 +2382,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 +2541,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); @@ -2781,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) { @@ -2970,8 +2863,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) ) { @@ -3099,7 +3013,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 +3032,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..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, @@ -254,7 +256,6 @@ class model_draw_list SCP_vector Render_keys; SCP_vector Arcs; - SCP_vector Insignias; SCP_vector Outlines; graphics::util::UniformBuffer _dataBuffer; @@ -287,9 +288,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,11 +310,10 @@ 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); +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/model/modelsinc.h b/code/model/modelsinc.h index 5d6c68c9a54..80bc4a4ee63 100644 --- a/code/model/modelsinc.h +++ b/code/model/modelsinc.h @@ -60,6 +60,20 @@ 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 +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)) +#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)) +#else #define us(p) (*reinterpret_cast(p)) #define cus(p) (*reinterpret_cast(p)) #define uw(p) (*reinterpret_cast(p)) @@ -70,6 +84,7 @@ class polymodel; #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); diff --git a/code/nebula/neb.cpp b/code/nebula/neb.cpp index 86cdd219e69..4d49ee0a6a0 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; @@ -1138,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 -------------------------------------------------------------------- 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..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; + } } } @@ -34,6 +37,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; @@ -85,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(); @@ -95,6 +102,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/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/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/multi_interpolate.h b/code/network/multi_interpolate.h index e600f02fb9a..8870b28fc42 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? @@ -59,8 +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_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) { @@ -82,10 +84,11 @@ class interpolation_manager { } - 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; } + 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_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/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; } 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/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/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) { diff --git a/code/network/multimsgs.cpp b/code/network/multimsgs.cpp index e7792bac095..ef4596c5983 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]); } } @@ -3140,6 +3137,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); @@ -3433,7 +3435,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. @@ -3949,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) { @@ -3974,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 @@ -6997,7 +6984,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; } @@ -8739,7 +8727,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. @@ -8770,15 +8759,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/network/multimsgs.h b/code/network/multimsgs.h index 836c090be89..91852e9b6a4 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 ); @@ -359,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/network/multiui.cpp b/code/network/multiui.cpp index b64039381eb..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; @@ -4535,7 +4543,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 +4725,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 +4746,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 +4827,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/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/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/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/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<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/collideshipship.cpp b/code/object/collideshipship.cpp index 14896f6d350..5fcaf880d5f 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 } @@ -747,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); + 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); + 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 @@ -867,11 +873,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 +910,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 +921,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 +1108,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 / 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); + + 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 +1454,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 +1493,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 +1506,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 +1519,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 +1556,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 5e500635958..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; @@ -82,7 +84,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 +137,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); @@ -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,26 +565,26 @@ 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) { 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); @@ -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/collideweaponweapon.cpp b/code/object/collideweaponweapon.cpp index cb31acfb1b3..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)) { @@ -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/object/objcollide.cpp b/code/object/objcollide.cpp index 854cd0e76d5..5d65219fa04 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,118 @@ 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]; + + 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); + } + 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 (queue_length == 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 +861,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 +897,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 +1086,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 +1144,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 +1206,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 +1239,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/object/object.cpp b/code/object/object.cpp index c5aea41ff9a..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 ( Detail.lighting > 3 ) { + if ( (Detail.lighting > 3) && light_deferred_enabled() ) { // Weapons cast light int group_id = Weapons[objp->instance].group_id; 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/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/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) { diff --git a/code/parse/parselo.cpp b/code/parse/parselo.cpp index b50fde1445e..42e7f60c2f1 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) @@ -972,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 @@ -1006,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"); } @@ -1015,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"); } @@ -1032,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"); } @@ -1129,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 @@ -4445,7 +4432,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 +4443,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..653c74f7284 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); @@ -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); diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 5c069ade1e5..8049687a894 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, }, @@ -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 }, @@ -4426,6 +4428,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 +4874,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); @@ -14059,17 +14111,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); @@ -17010,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 @@ -26764,27 +26822,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 +26893,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 { @@ -27567,8 +27633,8 @@ 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); + 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))) { @@ -30616,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 @@ -31550,6 +31620,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: @@ -32567,6 +32638,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; @@ -36732,6 +36811,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: @@ -37459,7 +37539,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" @@ -37522,12 +37602,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" @@ -37632,7 +37712,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" @@ -37642,40 +37722,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" }, @@ -37718,7 +37798,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" @@ -37742,7 +37822,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" @@ -37958,8 +38038,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."}, @@ -38001,19 +38081,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" }, @@ -38808,7 +38888,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" }, @@ -38840,7 +38920,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" @@ -38878,8 +38958,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" @@ -38887,8 +38967,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" @@ -38898,9 +38978,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" @@ -38909,7 +38989,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"}, @@ -38931,7 +39011,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" @@ -39215,6 +39295,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" @@ -39729,7 +39818,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" @@ -39825,13 +39914,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!)"}, @@ -40021,15 +40110,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)" }, @@ -40054,7 +40143,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" }, @@ -40073,7 +40162,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 @@ -40423,11 +40512,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" @@ -40541,7 +40630,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" @@ -41260,10 +41349,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" @@ -41310,8 +41399,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" }, @@ -41328,9 +41417,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." }, @@ -41384,7 +41474,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" }, @@ -41594,9 +41684,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" @@ -41939,15 +42029,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" @@ -42164,8 +42254,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" }, 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/particle/ParticleEffect.cpp b/code/particle/ParticleEffect.cpp index 0077fd7075a..fec51b3a5f1 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,15 +46,17 @@ 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), m_particleChance(1.f), m_distanceCulled(-1.f) {} 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 +71,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 +89,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 +114,26 @@ 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::getApproximatePixelSize(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, + g3_get_hfov(Eye_fov), + gr_screen.max_w); +} + +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 +169,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 +183,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,10 +262,13 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s } for (uint i = 0; i < num_spawn; ++i) { - particle_info info; + float particleFraction = static_cast(i) / static_cast(num_spawn); + + particle info; + info.reverse = m_reverseAnimation; info.pos = pos; - info.vel = velParent; + info.velocity = velParent; if (m_parent_local) { info.attached_objnum = parent; @@ -225,19 +280,23 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s } if (m_vel_inherit_absolute) - vm_vec_normalize_safe(&info.vel, true); + vm_vec_normalize_safe(&info.velocity, true); - info.vel *= 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; 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,28 +321,43 @@ 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.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; + + 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_parentLifetime) - // if we were spawned by a particle, parentLifetime is the parent's remaining liftime 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; + 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.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: @@ -300,7 +374,10 @@ void ParticleEffect::processSource(float interp, const ParticleSource& source, s } if (m_particleTrail.isValid()) { - auto part = createPersistent(&info); + auto part = createPersistent(std::move(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. @@ -310,10 +387,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(std::move(info)); + createdParticles.push_back(part); + } + else { + // We don't have a trail so we don't need a persistent particle + create(std::move(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..fb1a0a044e3 100644 --- a/code/particle/ParticleEffect.h +++ b/code/particle/ParticleEffect.h @@ -3,11 +3,13 @@ #pragma once #include "globalincs/pstypes.h" +#include "globalincs/systemvars.h" #include "particle/ParticleVolume.h" #include "particle/ParticleSource.h" #include "utils/RandomRange.h" #include "utils/id.h" #include "utils/modular_curves.h" +#include "graphics/2d.h" #include @@ -15,6 +17,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; @@ -79,13 +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; @@ -99,6 +116,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,18 +144,18 @@ class ParticleEffect { std::shared_ptr> m_spawnNoise; std::optional m_manual_offset; + std::optional m_manual_velocity_offset; 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. 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 +168,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 +185,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 +209,8 @@ class ParticleEffect { bool isOnetime() const { return m_duration == Duration::ONETIME; } + float getApproximatePixelSize(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}, @@ -206,13 +237,53 @@ 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>{}}, + 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( + .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 {"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>{}} + ); + + 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; + 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/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 19de33f8b68..6a7e982d763 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 @@ -15,7 +17,18 @@ 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); + } + } + } + + static void parseBitmapReversed(ParticleEffect &effect) { + if (optional_string("+Animation Reversed:")) { + stuff_boolean(&effect.m_reverseAnimation); } } @@ -73,6 +86,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 +125,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 +137,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 +160,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) { @@ -254,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) { @@ -302,6 +318,7 @@ namespace particle { //Particle Settings parseBitmaps(effect); + parseBitmapReversed(effect); parseRotationType(effect); parseRadius(effect); parseLength(effect); @@ -338,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.cpp b/code/particle/ParticleSource.cpp index 7d400303e89..1023b315440 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)) { @@ -62,14 +65,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; @@ -83,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; } @@ -99,10 +102,20 @@ 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::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 9b3d47c7e13..df5c2c2c7f8 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" @@ -18,23 +19,6 @@ 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 { -}; -using ParticleEffectHandle = ::util::ID; /** * @brief The orientation of a particle source @@ -90,6 +74,10 @@ class ParticleSource { static float getEffectRemainingTime(const std::tuple& source); static float getEffectRunningTime(const std::tuple& source); + + static float getEffectPixelSize(const std::tuple& source); + + static float getEffectApparentSize(const std::tuple& source); public: ParticleSource(); @@ -98,7 +86,7 @@ class ParticleSource { const SCP_vector& getEffect() const; inline void setEffect(ParticleEffectHandle eff) { - Assert(eff.isValid()); + Assert(eff.isValid() || Is_standalone); m_effect = eff; } 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/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 3163e95fd19..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() { @@ -113,6 +118,10 @@ namespace particle Particles.clear(); } + size_t get_particle_count() { + return Particles.size() + Persistent_particles.size(); + } + void page_in() { if (!Particles_enabled) @@ -130,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; - } - - int fps = 1; - - part->pos = info->pos; - part->velocity = info->vel; - part->age = 0.0f; - 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); - } + return true; } - 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(); - } + ParticlePtr new_particle_ptr = std::make_shared(new_particle); - Persistent_particles.push_back(new_particle); + Persistent_particles.push_back(new_particle_ptr); - return WeakParticlePtr(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; - - // lower level function - create(&pinfo); + return {new_particle_ptr}; } /** @@ -288,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; @@ -402,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 { @@ -418,18 +370,18 @@ 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 = part->pos; + vec3d p0 = p_pos; vec3d p1; vm_vec_copy_normalize_safe(&p1, &part->velocity); - p1 *= part->length; - p1 += part->pos; + if (part->attached_objnum >= 0) { + vm_vec_unrotate(&p1, &p1, &Objects[part->attached_objnum].orient); + } + 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 871d85370a5..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 ================== //============================================================================ @@ -40,6 +53,8 @@ namespace particle // kill all active particles void kill_all(); + size_t get_particle_count(); + //============================================================================ //=============== LOW-LEVEL SINGLE PARTICLE CREATION CODE ==================== @@ -54,28 +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 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 @@ -94,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; @@ -110,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 @@ -137,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/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..95d64cfdce0 100644 --- a/code/particle/volumes/SpheroidVolume.cpp +++ b/code/particle/volumes/SpheroidVolume.cpp @@ -6,32 +6,45 @@ 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; + + 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 - 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 +57,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/physics/physics.cpp b/code/physics/physics.cpp index ce2bf662463..639313f7ea1 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) ); @@ -1242,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; 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/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); } 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/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; } 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; 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); 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)); diff --git a/code/scripting/api/libs/graphics.cpp b/code/scripting/api/libs/graphics.cpp index 8982215f5f2..ec82897307e 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,80 +2194,174 @@ 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(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 + 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; + if (Is_standalone) { + return persistent ? ADE_RETURN_NIL : ADE_RETURN_FALSE; + } + 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_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); + } + 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 +2374,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/mission.cpp b/code/scripting/api/libs/mission.cpp index b9b3b1ad264..dc89fb9a190 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)) @@ -3033,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/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 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/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/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/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))) 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 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)); } diff --git a/code/scripting/api/objs/model.cpp b/code/scripting/api/objs/model.cpp index 67d3f2e747b..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,13 +56,41 @@ 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; } +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/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; }; 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/parse_object.cpp b/code/scripting/api/objs/parse_object.cpp index c6468e87008..9ed1f8b5eb9 100644 --- a/code/scripting/api/objs/parse_object.cpp +++ b/code/scripting/api/objs/parse_object.cpp @@ -7,6 +7,8 @@ #include "vecmath.h" #include "weaponclass.h" #include "wing.h" +#include "team_colors.h" +#include "globalincs/alphacolors.h" //Needed for team colors #include "mission/missionparse.h" @@ -332,6 +334,37 @@ 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, "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; + 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_RETURN_NIL; + + // 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", poh->getObject()->name)); + } else { + poh->getObject()->team_color_setting = Team_Names[idx]; + } + } + + // 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_RETURN_NIL; +} + 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/ship.cpp b/code/scripting/api/objs/ship.cpp index 285a04abda6..dbea8871766 100644 --- a/code/scripting/api/objs/ship.cpp +++ b/code/scripting/api/objs/ship.cpp @@ -13,8 +13,10 @@ #include "ship.h" #include "ship_bank.h" #include "shipclass.h" +#include "shiptype.h" #include "subsystem.h" #include "team.h" +#include "team_colors.h" #include "texture.h" #include "vecmath.h" #include "weaponclass.h" @@ -94,6 +96,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; @@ -837,6 +869,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; @@ -1582,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()) @@ -1838,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.cpp b/code/scripting/api/objs/shipclass.cpp index 674d8b5f218..38df49ddd34 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]", + "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,7 +1124,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)) + 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()) @@ -1132,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); @@ -1146,17 +1150,26 @@ 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 = 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]", "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 = 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; + 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()) @@ -1165,15 +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(); - return ade_set_args(L, "b", render_tech_model(TECH_SHIP, x1, y1, x2, y2, zoom, true, idx, orient)); + 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]", + "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.", @@ -1188,7 +1211,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)) + 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()) @@ -1223,13 +1247,28 @@ 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 = 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); } if (sip->replacement_textures.size() > 0) { 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, @@ -1242,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); } @@ -1252,7 +1291,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, 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 " @@ -1293,10 +1332,12 @@ ADE_FUNC(renderOverheadModel, int weapon_list[MAX_SHIP_WEAPONS] = {-1, -1, -1, -1, -1, -1, -1}; + int tc_idx = -1; + if (lua_isnumber(L, 6)) { if (!ade_get_args(L, - "oii|iiiiiiiiiiiiiiiiiiii", + "oii|iiiiiiiiiiiiiiiiiiiio", l_Shipclass.Get(&idx), &x1, &y1, @@ -1319,7 +1360,8 @@ ADE_FUNC(renderOverheadModel, &bank6_y, &bank7_x, &bank7_y, - &style)) + &style, + l_TeamColor.Get(&tc_idx))) return ADE_RETURN_NIL; // Convert this from the Lua index @@ -1333,7 +1375,7 @@ ADE_FUNC(renderOverheadModel, } } else { if (!ade_get_args(L, - "oii|iitiiiiiiiiiiiiiiiii", + "oii|iitiiiiiiiiiiiiiiiiio", l_Shipclass.Get(&idx), &x1, &y1, @@ -1356,7 +1398,8 @@ ADE_FUNC(renderOverheadModel, &bank6_y, &bank7_x, &bank7_y, - &style)) + &style, + l_TeamColor.Get(&tc_idx))) return ADE_RETURN_NIL; int count = 0; @@ -1403,6 +1446,14 @@ ADE_FUNC(renderOverheadModel, ship_info* sip = &Ship_info[idx]; + 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); static float ShipRot = 0.0f; @@ -1436,7 +1487,8 @@ ADE_FUNC(renderOverheadModel, 0, 0, 0, - (overhead_style)style); + (overhead_style)style, + tcolor); return ade_set_args(L, "b", true); } 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/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/scripting/api/objs/team_colors.cpp b/code/scripting/api/objs/team_colors.cpp new file mode 100644 index 00000000000..da21ac50223 --- /dev/null +++ b/code/scripting/api/objs/team_colors.cpp @@ -0,0 +1,132 @@ +// +// + +#include "team_colors.h" +#include "globalincs/alphacolors.h" +#include "scripting/api/objs/color.h" + +namespace scripting::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..0790106215e --- /dev/null +++ b/code/scripting/api/objs/team_colors.h @@ -0,0 +1,12 @@ +#pragma once + +#include "globalincs/pstypes.h" + +#include "scripting/ade.h" +#include "scripting/ade_api.h" + +namespace scripting::api { + +DECLARE_ADE_OBJ(l_TeamColor, int); + +} // namespace scripting::api 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(); 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/shield.cpp b/code/ship/shield.cpp index 67f21de13b9..7006b720e47 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; @@ -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, 0.1f * 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, 0.1f * 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.cpp b/code/ship/ship.cpp index f5ff68c9c85..b4f9dbc9cdc 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); @@ -1072,9 +1073,18 @@ 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; + 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; @@ -1207,6 +1217,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; @@ -1341,6 +1355,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) @@ -1428,9 +1444,18 @@ 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); + 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; @@ -1546,6 +1571,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; @@ -1677,6 +1706,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 @@ -1754,6 +1785,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(); @@ -1786,6 +1818,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; @@ -1795,6 +1831,13 @@ 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(); + 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; debris_min_speed = -1.0f; @@ -1920,6 +1963,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; @@ -2046,7 +2093,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; @@ -2058,6 +2105,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() @@ -2444,6 +2493,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; @@ -2529,6 +2583,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 +2600,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 @@ -3016,6 +3079,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; @@ -3318,6 +3419,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); } @@ -3746,7 +3851,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 @@ -3765,7 +3872,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; @@ -3777,7 +3886,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; @@ -3838,6 +3949,26 @@ 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); + } + + 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; @@ -4016,12 +4147,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:")){ @@ -4801,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:") ) { @@ -4842,6 +5017,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 +5034,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 @@ -5326,6 +5510,20 @@ 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); + } + + 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; @@ -5423,6 +5621,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) @@ -5609,10 +5812,23 @@ 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); } + 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; @@ -8113,8 +8329,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) || !(Viewer_mode & VM_EXTERNAL)); Cockpit_active = renderCockpitModel; //Nothing to do @@ -8248,6 +8463,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; @@ -9276,19 +9492,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 ); - float rad = objp->radius*0.1f; + if (sip->death_roll_exp_particles.isValid()) { + 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 { + 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; @@ -9379,24 +9603,33 @@ 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 ); + model_instance_local_to_global_point(&outpnt, &avgpnt, pm, pmi, pm->detail[0], &objp->orient, &objp->pos ); - float rad = objp->radius*0.40f; + if (sip->pre_death_exp_particles.isValid()) { + 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 { + float rad = objp->radius*0.40f; - rad *= sip->death_fx_r_mult; + rad *= sip->death_fx_r_mult; - int fireball_type = fireball_ship_explosion_type(sip); - if(fireball_type < 0) { - fireball_type = FIREBALL_EXPLOSION_MEDIUM; + 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 ); } } @@ -12126,8 +12359,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)); @@ -13322,7 +13559,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; @@ -13349,12 +13586,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 @@ -13750,7 +13987,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; @@ -13760,6 +13997,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 ); @@ -13846,14 +14084,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. @@ -14026,10 +14263,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() ) { @@ -14046,39 +14279,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 { @@ -14195,12 +14424,12 @@ 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 // 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 @@ -14212,7 +14441,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 @@ -14241,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); + } } } } @@ -14276,7 +14513,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 @@ -15446,6 +15683,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]) @@ -16240,6 +16480,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)) { @@ -17189,38 +17432,9 @@ const char *ship_subsys_get_name_on_hud(const ship_subsys *ss) return ship_subsys_get_name(ss); } -/** - * 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) +const char *ship_subsys_get_canonical_name(const ship_subsys *ss) { - 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; + return ss->system_info->subobj_name; } // Determine if a ship is threatened by any dumbfire projectiles (laser or missile) @@ -17372,6 +17586,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: @@ -17405,7 +17620,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; @@ -17429,6 +17644,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; @@ -17437,6 +17653,7 @@ SCP_string ship_return_orders(ship* sp) } break; + case AI_GOAL_CHASE: case AI_GOAL_DOCK: case AI_GOAL_UNDOCK: @@ -17474,7 +17691,7 @@ SCP_string ship_return_orders(ship* sp) break; default: - return SCP_string(); + return {}; } return outbuf; @@ -18545,6 +18762,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 */ @@ -19941,9 +20183,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)) { @@ -20638,7 +20882,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 db458ee83b9..ef052b122df 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 @@ -1224,9 +1225,18 @@ 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; + 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; @@ -1255,6 +1265,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) @@ -1352,6 +1363,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 @@ -1471,7 +1486,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; @@ -1765,16 +1780,13 @@ 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(); -// 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); @@ -1803,6 +1815,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); @@ -1866,9 +1879,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); @@ -1906,6 +1916,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/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/code/ship/shipfx.cpp b/code/ship/shipfx.cpp index 8610874e68f..86f1352c0b6 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; @@ -282,18 +285,20 @@ 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!! - 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 ); } } @@ -343,7 +348,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 +379,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 ); } } @@ -1083,9 +1088,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); } } @@ -1847,7 +1849,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; @@ -1862,8 +1870,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 { @@ -1871,18 +1880,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; diff --git a/code/ship/shipfx.h b/code/ship/shipfx.h index 25aadea6d7e..06db056443b 100644 --- a/code/ship/shipfx.h +++ b/code/ship/shipfx.h @@ -32,11 +32,11 @@ 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. -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); // ================================================= diff --git a/code/ship/shiphit.cpp b/code/ship/shiphit.cpp index 39bf78bbd60..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; } @@ -418,7 +446,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 +462,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 +632,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 +666,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 @@ -662,7 +703,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, bool shield_hit) { vec3d g_subobj_pos; float damage_left, damage_if_hull; @@ -687,10 +728,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 +743,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 +762,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 +774,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; @@ -784,7 +827,7 @@ float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3 } } 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; @@ -919,6 +962,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 +1044,16 @@ float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3 damage_to_apply *= ss_dif_scale; } + if (j == 0 && !shield_hit) { + 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; @@ -1019,7 +1073,7 @@ float do_subobj_hit_stuff(object *ship_objp, const object *other_obj, const vec3 // 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 @@ -1037,7 +1091,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 +2346,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")); @@ -2348,9 +2402,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; } } @@ -2400,11 +2459,36 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi // 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; } // 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 X percent of a shield doesn't matter + 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 { + ImpactCondition(shipp->armor_type_idx), + HitType::HULL, + 0.0f, + ship_objp->hull_strength, + shipp->ship_max_hull_strength, + }; + } + 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; } @@ -2417,6 +2501,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 X percent of a shield doesn't matter + 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 ) { float piercing_pct = 0.0f; @@ -2435,28 +2528,37 @@ 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. 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 - 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, shield_hit); + + 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 +2613,16 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi } } + 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)) { } else { @@ -2623,6 +2735,10 @@ static void ship_do_damage(object *ship_objp, object *other_obj, const vec3d *hi } } + 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) { Assert(weapon_info_index >= 0); @@ -2645,7 +2761,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) @@ -2672,8 +2788,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; } @@ -2689,22 +2810,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) @@ -2715,11 +2858,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 ) @@ -2788,7 +2941,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 +2958,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 X percent of a shield doesn't matter + 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 { + 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; } } @@ -2860,11 +3035,11 @@ 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); } - 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 @@ -2933,6 +3108,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); @@ -2951,7 +3131,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, 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) @@ -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/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; } 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/code/source_groups.cmake b/code/source_groups.cmake index af16749c9ee..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 @@ -1140,6 +1146,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 ) @@ -1495,6 +1505,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 @@ -1700,6 +1712,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/starfield/starfield.cpp b/code/starfield/starfield.cpp index 0f1a7168639..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; @@ -2322,7 +2326,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) 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/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; } } diff --git a/code/utils/RandomRange.h b/code/utils/RandomRange.h index ed13709c08f..c574b5ce1ac 100644 --- a/code/utils/RandomRange.h +++ b/code/utils/RandomRange.h @@ -77,6 +77,10 @@ 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 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) @@ -140,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/utils/modular_curves.h b/code/utils/modular_curves.h index 4def11b9b5e..683f50e5a61 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 @@ -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) { @@ -145,6 +159,77 @@ 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_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: @@ -170,10 +255,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)); } }; @@ -181,7 +279,7 @@ enum class ModularCurvesMathOperators { addition, subtraction, multiplication, - division, + division }; template @@ -241,7 +339,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 +453,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); @@ -390,6 +488,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 +559,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/utils/threading.cpp b/code/utils/threading.cpp new file mode 100644 index 00000000000..5c71228235e --- /dev/null +++ b/code/utils/threading.cpp @@ -0,0 +1,196 @@ +#include "threading.h" + +#include "cmdline/cmdline.h" +#include "object/objcollide.h" +#include "globalincs/pstypes.h" + +#include +#include +#include +#include + +#ifdef WIN32 +#include +#elif defined(__APPLE__) +#include +#endif + +namespace threading { + 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; + + static 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!"); + } + } + } + + 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 __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 { + 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) { + 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 (Cmdline_multithreading == 0) { + //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; + } + + if (!is_threading()) + return; + + mprintf(("Spinning up threadpool with %d threads...\n", static_cast(num_threads))); + + for (size_t i = 0; i < num_threads; 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 num_threads > 0; + } + + size_t get_num_workers() { + return worker_threads.size(); + } +} 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/code/weapon/beam.cpp b/code/weapon/beam.cpp index 6864d2757f9..4a0a25e13f6 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); @@ -2287,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: @@ -2964,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]); } } @@ -4120,12 +4077,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); } } @@ -4137,7 +4096,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/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..3d9e8534ed1 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.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) { + 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("Pixel 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/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/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 ) { diff --git a/code/weapon/weapon.h b/code/weapon/weapon.h index edc1392f486..9e01545bdc0 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 @@ -90,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; @@ -144,10 +135,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 +225,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 +250,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 @@ -324,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 { @@ -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 @@ -546,7 +550,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; @@ -572,9 +576,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 +620,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 +934,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; @@ -978,6 +942,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); @@ -1030,15 +1001,18 @@ 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); -// 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 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 @@ -1082,7 +1056,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/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 e606986c867..c5c90ecccef 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" @@ -56,8 +57,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" @@ -121,15 +125,12 @@ 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); -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 +318,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() @@ -850,6 +619,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); @@ -866,6 +640,145 @@ 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 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) { + 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); + + 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), powf(pspew_buffer.particle_spew_scale, 3.0f)); + 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_either(pspew_buffer.particle_spew_anim.c_str()) : particle::Anim_bitmap_id_smoke)); //Bitmap or Anim +} + /** * Parse the information for a specific ship type. * Return weapon index if successful, otherwise return -1 @@ -1084,6 +997,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]; @@ -1096,6 +1013,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); @@ -2222,6 +2177,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 +2194,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 +2267,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 +2284,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 +2366,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 +2383,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 +2398,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 +2415,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 @@ -2438,41 +2429,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; } } @@ -2503,14 +2532,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) @@ -2701,9 +2727,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)}); } @@ -2900,25 +2926,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 +3169,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 +3186,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 +3251,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 +3268,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 +3282,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 +3299,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 +3641,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 +3668,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); } } @@ -4377,12 +4524,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 +4548,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 +4637,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 +4682,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); @@ -4800,26 +4902,13 @@ void weapon_do_post_parse() } } - // translate all spawn type weapons to referrnce the appropriate spawned weapon entry - 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 ); + 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."); } - // done - LOD_checker.clear(); + // translate all spawn type weapons to referrnce the appropriate spawned weapon entry + translate_spawn_types(); } /** @@ -4828,9 +4917,6 @@ void weapon_expl_info_init() void weapon_init() { if ( !Weapons_inited ) { - //Init weapon explosion info - weapon_expl_info_init(); - Num_spawn_types = 0; // parse weapons.tbl @@ -6038,6 +6124,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) @@ -6215,10 +6314,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 +6874,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 +7148,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); @@ -7215,7 +7313,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 @@ -7292,8 +7390,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 ) { @@ -7330,14 +7426,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 ); @@ -7735,7 +7833,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; } @@ -7757,133 +7856,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); + 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) { @@ -7892,9 +8024,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) { @@ -7903,18 +8035,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; @@ -7935,9 +8065,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) { @@ -7947,9 +8077,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) { @@ -7960,6 +8090,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; @@ -8008,7 +8193,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) ) { @@ -8059,6 +8244,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) @@ -8080,9 +8266,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); } } @@ -8235,23 +8423,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 +8449,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 +8596,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 +8665,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 +8793,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 @@ -9217,8 +8939,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); @@ -9824,6 +9551,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; @@ -9855,8 +9586,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 +9625,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 +9664,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 +9686,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; @@ -10551,7 +10265,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 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; 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/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/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/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/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 6d8bfdeb3a8..2d5a8da4546 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...", ID_IMPORT_XWIMISSION END MENUITEM SEPARATOR MENUITEM "&Run FreeSpace", 32985 @@ -324,24 +325,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 @@ -389,6 +395,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 @@ -601,40 +608,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 +668,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 @@ -991,7 +998,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" @@ -1000,83 +1007,85 @@ 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 - COMBOBOX IDC_SHIP_CLASS,47,23,94,207,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - COMBOBOX IDC_AI_CLASS,47,37,94,185,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - COMBOBOX IDC_SHIP_TEAM,47,51,94,196,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - COMBOBOX IDC_SHIP_CARGO1,47,65,94,258,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP - COMBOBOX IDC_SHIP_ALT,47,79,94,125,CBS_DROPDOWN | CBS_AUTOHSCROLL | WS_VSCROLL | WS_TABSTOP - 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 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 - CONTROL "Hide Cues",IDC_HIDE_CUES,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,266,133,49,12 - COMBOBOX IDC_ARRIVAL_LOCATION,46,162,103,127,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - COMBOBOX IDC_ARRIVAL_TARGET,46,176,103,262,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - EDITTEXT IDC_ARRIVAL_DISTANCE,46,190,40,14,ES_AUTOHSCROLL | ES_NUMBER - 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 - CONTROL "Update Cue",IDC_UPDATE_ARRIVAL,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,94,263,54,10 - 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 - COMBOBOX IDC_DEPARTURE_LOCATION,202,162,103,127,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - COMBOBOX IDC_DEPARTURE_TARGET,202,176,103,262,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP - 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 - CONTROL "Update Cue",IDC_UPDATE_DEPARTURE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,251,263,54,10 - 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_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 + "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 @@ -1602,7 +1611,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 @@ -1752,6 +1761,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 @@ -2424,7 +2434,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 @@ -2467,15 +2477,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 @@ -2485,33 +2498,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 @@ -3679,6 +3692,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/freddoc.cpp b/fred2/freddoc.cpp index 4e61c451cf4..be0daf6952f 100644 --- a/fred2/freddoc.cpp +++ b/fred2/freddoc.cpp @@ -62,6 +62,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() @@ -231,7 +232,7 @@ bool CFREDDoc::load_mission(const 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)) { @@ -342,7 +343,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; @@ -575,6 +579,155 @@ 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 + BROWSEINFO bi; + bi.hwndOwner = theApp.GetMainWnd()->GetSafeHwnd(); + bi.pidlRoot = NULL; + 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); + + if (*dest_directory == '\0') + return; + + // clean things up first + if (Briefing_dialog) + Briefing_dialog->icon_select(-1); + + clear_mission(); + + int num_files = 0; + int successes = 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]; + + CString xwi_path_mfc(dlgFile.GetNextPathName(pos)); + num_files++; + 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 | MPF_FAST_RELOAD)) + 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'; + + // assign this as the mission name + strcpy_s(The_mission.name, filename); + + // 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 + successes++; + } + + if (num_files > 1) + { + create_new_mission(); + 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) + { + if (successes == 1) + SetModifiedFlag(FALSE); + + if (Briefing_dialog) { + Briefing_dialog->restore_editor_state(); + Briefing_dialog->update_data(1); + } + + if (successes == 1) + { + // these aren't done automatically for imports + theApp.AddToRecentFileList((LPCTSTR)dest_path); + SetTitle((LPCTSTR)Mission_filename); + } + } + + recreate_dialogs(); +} + BOOL CFREDDoc::OnNewDocument() { if (!CDocument::OnNewDocument()) return FALSE; diff --git a/fred2/freddoc.h b/fred2/freddoc.h index 58a81b2270d..c01368b0db0 100644 --- a/fred2/freddoc.h +++ b/fred2/freddoc.h @@ -173,6 +173,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/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..8249e1ade3c 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) @@ -3482,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: @@ -3829,6 +3832,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/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 84d31102f97..e0b1a41c833 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); @@ -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()); UpdateData(FALSE); } - + + lcl_fred_replace_stuff(m_display); + strcpy_s(old_name, jnp->GetName()); 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 +389,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 +398,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_ALT_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() }; diff --git a/fred2/management.cpp b/fred2/management.cpp index 13436ae1564..534a7dc22a4 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 }, @@ -234,6 +235,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) @@ -421,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 de315b1fe00..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; @@ -68,6 +58,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/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/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 753c13fc8c8..3aa01720525 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; } @@ -659,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; @@ -1343,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); @@ -2787,6 +2816,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); @@ -2824,6 +2854,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:"); @@ -3554,15 +3585,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()); } } @@ -3589,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:"); @@ -4849,13 +4894,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 { @@ -5159,30 +5204,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/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/fred2/resource.h b/fred2/resource.h index 5edfb7ac085..304b671ac4b 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 @@ -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 @@ -1462,6 +1464,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 @@ -1556,6 +1559,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 @@ -1567,7 +1571,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 diff --git a/fred2/shipeditordlg.cpp b/fred2/shipeditordlg.cpp index 718e91feac6..a398fb5a9db 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,24 @@ 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) + { + 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); @@ -1296,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) { @@ -1618,6 +1657,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; @@ -1923,7 +1974,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/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/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(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/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/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); diff --git a/fred2/wing_editor.cpp b/fred2/wing_editor.cpp index 058f23f68c3..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); } @@ -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/freespace2/freespace.cpp b/freespace2/freespace.cpp index a04782ae390..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" @@ -941,7 +942,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 +1094,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(); @@ -1759,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)); @@ -2008,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(); @@ -7025,6 +7027,8 @@ void game_shutdown(void) } lcl_xstr_close(); + + threading::shut_down_task_pool(); } // game_stop_looped_sounds() 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) { 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 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*/ 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; diff --git a/qtfred/resources/images/arrow_down.png b/qtfred/resources/images/arrow_down.png new file mode 100644 index 00000000000..98ab8f99022 Binary files /dev/null and b/qtfred/resources/images/arrow_down.png differ diff --git a/qtfred/resources/images/arrow_up.png b/qtfred/resources/images/arrow_up.png new file mode 100644 index 00000000000..f8c7b54476e Binary files /dev/null and b/qtfred/resources/images/arrow_up.png differ diff --git a/qtfred/resources/images/comment.png b/qtfred/resources/images/comment.png index e832c7bebb9..1729f7c8abd 100644 Binary files a/qtfred/resources/images/comment.png and b/qtfred/resources/images/comment.png differ diff --git a/qtfred/resources/images/next.bmp b/qtfred/resources/images/next.bmp new file mode 100644 index 00000000000..cfc4592eb5f Binary files /dev/null and b/qtfred/resources/images/next.bmp differ diff --git a/qtfred/resources/images/next.png b/qtfred/resources/images/next.png new file mode 100644 index 00000000000..42efffd2bfe Binary files /dev/null and b/qtfred/resources/images/next.png differ diff --git a/qtfred/resources/images/play.bmp b/qtfred/resources/images/play.bmp new file mode 100644 index 00000000000..45ad6fc1e74 Binary files /dev/null and b/qtfred/resources/images/play.bmp differ diff --git a/qtfred/resources/images/prev.bmp b/qtfred/resources/images/prev.bmp new file mode 100644 index 00000000000..877144d0db4 Binary files /dev/null and b/qtfred/resources/images/prev.bmp differ diff --git a/qtfred/resources/images/prev.png b/qtfred/resources/images/prev.png new file mode 100644 index 00000000000..bd6fc446a28 Binary files /dev/null and b/qtfred/resources/images/prev.png differ diff --git a/qtfred/resources/images/stop.bmp b/qtfred/resources/images/stop.bmp new file mode 100644 index 00000000000..0263040745f Binary files /dev/null and b/qtfred/resources/images/stop.bmp differ diff --git a/qtfred/resources/images/stop.png b/qtfred/resources/images/stop.png new file mode 100644 index 00000000000..86d42b9ebbf Binary files /dev/null and b/qtfred/resources/images/stop.png differ diff --git a/qtfred/resources/resources.qrc b/qtfred/resources/resources.qrc index 0c5036915d9..632b40f9a9c 100644 --- a/qtfred/resources/resources.qrc +++ b/qtfred/resources/resources.qrc @@ -1,66 +1,73 @@ - - 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/arrow_down.png + images/arrow_up.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/comment.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 a4bd7250f6a..941ba377551 100644 --- a/qtfred/source_groups.cmake +++ b/qtfred/source_groups.cmake @@ -42,32 +42,64 @@ 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 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 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 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 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 - 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 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/VolumetricNebulaDialogModel.cpp + src/mission/dialogs/VolumetricNebulaDialogModel.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/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 @@ -86,10 +118,16 @@ 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/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 src/mission/dialogs/ShipEditor/ShipCustomWarpDialogModel.cpp + src/mission/dialogs/ShipEditor/ShipAltShipClassModel.h + src/mission/dialogs/ShipEditor/ShipAltShipClassModel.cpp ) add_file_folder("Source/UI" @@ -112,32 +150,58 @@ 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 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 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 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 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 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/VolumetricNebulaDialog.h + src/ui/dialogs/VolumetricNebulaDialog.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/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 @@ -156,13 +220,29 @@ 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 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 +) +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 @@ -172,12 +252,20 @@ 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 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 + src/ui/widgets/PersonaColorComboBox.cpp + src/ui/widgets/PersonaColorComboBox.h ) add_file_folder("UI" @@ -186,20 +274,30 @@ add_file_folder("UI" ui/BackgroundEditor.ui ui/BriefingEditorDialog.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 ui/FormWingDialog.ui ui/FredView.ui + ui/GlobalShipFlagsDialog.ui + ui/JumpNodeEditorDialog.ui ui/LoadoutDialog.ui + ui/MissionCutscenesDialog.ui + ui/MissionEventsDialog.ui ui/MissionGoalsDialog.ui ui/MissionSpecDialog.ui + ui/MusicPlayerDialog.ui ui/ObjectOrientationDialog.ui ui/ReinforcementsDialog.ui + ui/RelativeCoordinatesDialog.ui ui/SelectionDialog.ui ui/ShieldSystemDialog.ui + ui/SoundEnvironmentDialog.ui ui/VoiceActingManager.ui + ui/VolumetricNebulaDialog.ui ui/WaypointEditorDialog.ui ui/ShipEditorDialog.ui ui/ShipInitialStatus.ui @@ -211,6 +309,10 @@ add_file_folder("UI" ui/ShipTBLViewer.ui ui/ShipPathsDialog.ui ui/ShipCustomWarpDialog.ui + ui/ShipAltShipClass.ui + ui/ShipWeaponsDialog.ui + ui/VariableDialog.ui + ui/WingEditorDialog.ui ) add_file_folder("Resources" @@ -265,8 +367,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 @@ -277,6 +381,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/main.cpp b/qtfred/src/main.cpp index d089bd12ad9..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); @@ -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); } diff --git a/qtfred/src/mission/Editor.cpp b/qtfred/src/mission/Editor.cpp index 85c5fe7d091..f39cc6852ed 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 @@ -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,16 +179,13 @@ 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) { char name[512], * old_name; int i, j, k, ob; - int used_pool[MAX_WEAPON_TYPES]; object* objp; // activate the localizer hash table @@ -335,7 +332,7 @@ bool Editor::loadMission(const std::string& mission_name, int flags) { } } } - } + } for (i = 0; i < Num_teams; i++) { generate_team_weaponry_usage_list(i, _weapon_usage[i]); @@ -344,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; } @@ -363,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++], ""); } @@ -491,10 +488,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); @@ -695,7 +692,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; @@ -720,9 +717,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); } @@ -3242,59 +3239,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]++; @@ -3325,6 +3321,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..8b0cea4504c 100644 --- a/qtfred/src/mission/Editor.h +++ b/qtfred/src/mission/Editor.h @@ -21,6 +21,29 @@ 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 +}; + +enum class GlobalShieldStatus { + HasShields, + NoShields, + MixedShields +}; + /*! Game editor. * Handles everything needed to edit the game, * without any knowledge of the actual GUI framework stack. @@ -39,7 +62,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); @@ -159,7 +182,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. @@ -180,14 +209,15 @@ 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); 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(); @@ -207,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/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/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/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/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/mission/dialogs/BackgroundEditorDialogModel.cpp b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp new file mode 100644 index 00000000000..eb9739258c4 --- /dev/null +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.cpp @@ -0,0 +1,1348 @@ +#include "FredApplication.h" +#include "BackgroundEditorDialogModel.h" + +#include "graphics/light.h" +#include "math/bitarray.h" +#include "mission/missionparse.h" +#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. +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) +{ + auto& bg = getActiveBackground(); + 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() +{ + // 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 +} + +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(); +} + +background_t& BackgroundEditorDialogModel::getActiveBackground() +{ + 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]; +} + +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::getBackgroundNames() +{ + + 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() +{ + 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); + + // Ensure the swap index is still valid + if (!SCP_vector_inbounds(Backgrounds, _swapIndex)) { + _swapIndex = 0; + } +} + +int BackgroundEditorDialogModel::getImportableBackgroundCount(const SCP_string& fs2Path) +{ + // 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(); +} + +int BackgroundEditorDialogModel::getSwapWithIndex() const +{ + return _swapIndex; +} + +void BackgroundEditorDialogModel::setSwapWithIndex(int idx) +{ + if (!SCP_vector_inbounds(Backgrounds, idx)) { + return; + } + + _swapIndex = idx; +} + +bool BackgroundEditorDialogModel::getSaveAnglesCorrectFlag() +{ + 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() +{ + 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() +{ + 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::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 0; + + 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 0; + + 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 0; + + 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 0; + + return bm->scale_x; +} + +void BackgroundEditorDialogModel::setBitmapScaleX(float v) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(v, getBitmapScaleLimit().first, getBitmapScaleLimit().second); + modify(bm->scale_x, v); + + refreshBackgroundPreview(); +} + +float BackgroundEditorDialogModel::getBitmapScaleY() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return 0; + + return bm->scale_y; +} + +void BackgroundEditorDialogModel::setBitmapScaleY(float v) +{ + auto* bm = getActiveBitmap(); + if (!bm) + return; + + CLAMP(v, getBitmapScaleLimit().first, getBitmapScaleLimit().second); + modify(bm->scale_y, v); + + refreshBackgroundPreview(); +} + +int BackgroundEditorDialogModel::getBitmapDivX() const +{ + auto* bm = getActiveBitmap(); + if (!bm) + return 0; + + 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 0; + + 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(); +} + +SCP_vector BackgroundEditorDialogModel::getAvailableSunNames() +{ + 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() +{ + 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::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(); +} + +SCP_vector BackgroundEditorDialogModel::getLightningNames() +{ + 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() +{ + 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() +{ + SCP_vector out; + out.reserve(Poof_info.size()); + for (const auto& p : Poof_info) { + out.emplace_back(p.name); + } + return out; +} + +bool BackgroundEditorDialogModel::getFullNebulaEnabled() +{ + 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() +{ + // May be -1 if full nebula is disabled + return Neb2_awacs; +} + +void BackgroundEditorDialogModel::setFullNebulaRange(float range) +{ + modify(Neb2_awacs, range); +} + +SCP_string BackgroundEditorDialogModel::getNebulaFullPattern() +{ + 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() +{ + // 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() +{ + 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() +{ + 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() +{ + return Neb2_fog_near_mult; +} + +void BackgroundEditorDialogModel::setFogNearMultiplier(float v) +{ + modify(Neb2_fog_near_mult, v); +} + +float BackgroundEditorDialogModel::getFogFarMultiplier() +{ + return Neb2_fog_far_mult; +} + +void BackgroundEditorDialogModel::setFogFarMultiplier(float v) +{ + modify(Neb2_fog_far_mult, v); +} + +bool BackgroundEditorDialogModel::getDisplayBackgroundBitmaps() +{ + 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() +{ + 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() +{ + return Neb2_fog_color[0]; +} + +void BackgroundEditorDialogModel::setFogR(int r) +{ + CLAMP(r, 0, 255) + const auto v = static_cast(r); + modify(Neb2_fog_color[0], v); +} + +int BackgroundEditorDialogModel::getFogG() +{ + return Neb2_fog_color[1]; +} + +void BackgroundEditorDialogModel::setFogG(int g) +{ + CLAMP(g, 0, 255) + const auto v = static_cast(g); + modify(Neb2_fog_color[1], v); +} + +int BackgroundEditorDialogModel::getFogB() +{ + return Neb2_fog_color[2]; +} + +void BackgroundEditorDialogModel::setFogB(int b) +{ + CLAMP(b, 0, 255) + const auto v = static_cast(b); + modify(Neb2_fog_color[2], v); +} + +SCP_vector BackgroundEditorDialogModel::getOldNebulaPatternOptions() +{ + SCP_vector out; + out.emplace_back(""); + for (auto& neb : Nebula_filenames) { + out.emplace_back(neb); + } + return out; +} + +SCP_vector BackgroundEditorDialogModel::getOldNebulaColorOptions() +{ + SCP_vector out; + out.reserve(NUM_NEBULA_COLORS); + for (auto& color : Nebula_colors) { + out.emplace_back(color); + } + return out; +} + +SCP_string BackgroundEditorDialogModel::getOldNebulaPattern() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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); + } +} + +int BackgroundEditorDialogModel::getAmbientR() +{ + 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() +{ + 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() +{ + 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(); +} + +SCP_string BackgroundEditorDialogModel::getSkyboxModelName() +{ + return The_mission.skybox_model; +} +void BackgroundEditorDialogModel::setSkyboxModelName(const SCP_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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + 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() +{ + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + int d = static_cast(fl2ir(fl_degrees(a.p))); + d = (d % 360 + 360) % 360; // wrap to [0, 359] + 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() +{ + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + int d = static_cast(fl2ir(fl_degrees(a.b))); + d = (d % 360 + 360) % 360; // wrap to [0, 359] + 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() +{ + angles a; + vm_extract_angles_matrix(&a, &The_mission.skybox_orientation); + int d = static_cast(fl2ir(fl_degrees(a.h))); + d = (d % 360 + 360) % 360; // wrap to [0, 359] + 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(); + } +} + +SCP_vector BackgroundEditorDialogModel::getLightingProfileOptions() +{ + 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() +{ + return Num_stars; +} + +void BackgroundEditorDialogModel::setNumStars(int n) +{ + CLAMP(n, getStarsLimit().first, getStarsLimit().second); + modify(Num_stars, n); + refreshBackgroundPreview(); +} + +bool BackgroundEditorDialogModel::getTakesPlaceInSubspace() +{ + 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() +{ + return {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() +{ + 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 new file mode 100644 index 00000000000..8d450ae654b --- /dev/null +++ b/qtfred/src/mission/dialogs/BackgroundEditorDialogModel.h @@ -0,0 +1,182 @@ +#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 + 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 + static SCP_vector getBackgroundNames(); + void setActiveBackgroundIndex(int idx); + static int getActiveBackgroundIndex(); + void addBackground(); + void removeActiveBackground(); + 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); + static bool getSaveAnglesCorrectFlag(); + + // bitmap group + static SCP_vector getAvailableBitmapNames(); + static SCP_vector getMissionBitmapNames(); + 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); + + // sun group + static SCP_vector getAvailableSunNames(); + static SCP_vector getMissionSunNames(); + 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 getSunHeading() const; + void setSunHeading(int deg); + float getSunScale() const; // uses scale_x for both x and y + void setSunScale(float v); + + // nebula group + static SCP_vector getLightningNames(); + static SCP_vector getNebulaPatternNames(); + static SCP_vector getPoofNames(); + static bool getFullNebulaEnabled(); + void setFullNebulaEnabled(bool enabled); + static float getFullNebulaRange(); + void setFullNebulaRange(float range); + static SCP_string getNebulaFullPattern(); + void setNebulaFullPattern(const SCP_string& name); + static SCP_string getLightning(); + void setLightning(const SCP_string& name); + static SCP_vector getSelectedPoofs(); + void setSelectedPoofs(const SCP_vector& names); + static bool getShipTrailsToggled(); + void setShipTrailsToggled(bool on); + static float getFogNearMultiplier(); + void setFogNearMultiplier(float v); + static float getFogFarMultiplier(); + void setFogFarMultiplier(float v); + static bool getDisplayBackgroundBitmaps(); + void setDisplayBackgroundBitmaps(bool on); + static bool getFogPaletteOverride(); + void setFogPaletteOverride(bool on); + static int getFogR(); + void setFogR(int r); + static int getFogG(); + void setFogG(int g); + static int getFogB(); + void setFogB(int b); + + // old nebula group + static SCP_vector getOldNebulaPatternOptions(); + static SCP_vector getOldNebulaColorOptions(); + static SCP_string getOldNebulaColorName(); + void setOldNebulaColorName(const SCP_string& name); + static SCP_string getOldNebulaPattern(); + void setOldNebulaPattern(const SCP_string& name); + static int getOldNebulaPitch(); + void setOldNebulaPitch(int deg); + static int getOldNebulaBank(); + void setOldNebulaBank(int deg); + static int getOldNebulaHeading(); + void setOldNebulaHeading(int deg); + + // ambient light group + static int getAmbientR(); + void setAmbientR(int r); + static int getAmbientG(); + void setAmbientG(int g); + static int getAmbientB(); + void setAmbientB(int b); + + // skybox group + static SCP_string getSkyboxModelName(); + void setSkyboxModelName(const SCP_string& name); + static bool getSkyboxNoLighting(); + void setSkyboxNoLighting(bool on); + static bool getSkyboxAllTransparent(); + void setSkyboxAllTransparent(bool on); + static bool getSkyboxNoZbuffer(); + void setSkyboxNoZbuffer(bool on); + static bool getSkyboxNoCull(); + void setSkyboxNoCull(bool on); + static bool getSkyboxNoGlowmaps(); + void setSkyboxNoGlowmaps(bool on); + static bool getSkyboxForceClamp(); + void setSkyboxForceClamp(bool on); + static int getSkyboxPitch(); + void setSkyboxPitch(int deg); + static int getSkyboxBank(); + void setSkyboxBank(int deg); + static int getSkyboxHeading(); + void setSkyboxHeading(int deg); + + // misc group + static SCP_vector getLightingProfileOptions(); + static int getNumStars(); + void setNumStars(int n); + static bool getTakesPlaceInSubspace(); + void setTakesPlaceInSubspace(bool on); + static SCP_string getEnvironmentMapName(); + void setEnvironmentMapName(const SCP_string& name); + static SCP_string getLightingProfileName(); + void setLightingProfileName(const SCP_string& name); + + private: + void refreshBackgroundPreview(); + static background_t& getActiveBackground(); + 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 + int _swapIndex = 0; // index of background to swap with + +}; +} // namespace fso::fred::dialogs \ No newline at end of file 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/mission/dialogs/CustomWingNamesDialogModel.cpp b/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.cpp deleted file mode 100644 index 4bbeb119389..00000000000 --- a/qtfred/src/mission/dialogs/CustomWingNamesDialogModel.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include "CustomWingNamesDialogModel.h" - -#include "ship/ship.h" - -namespace fso { -namespace fred { -namespace dialogs { - -CustomWingNamesDialogModel::CustomWingNamesDialogModel(QObject * parent, EditorViewport * viewport) : - AbstractDialogModel(parent, viewport) { - initializeData(); -} - -bool CustomWingNamesDialogModel::apply() { - int i; - - for (auto wing : _m_starting) { - Editor::strip_quotation_marks(wing); - } - - for (auto wing : _m_squadron) { - Editor::strip_quotation_marks(wing); - } - - for (auto wing : _m_tvt) { - Editor::strip_quotation_marks(wing); - } - - if (strcmp(_m_starting[0].c_str(), _m_tvt[0].c_str()) != 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 }); - if (button == DialogButton::Ok) { - return false; - } - } - - 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())) - { - auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Custom Wing Error", "Duplicate wing names in starting wing list.", - { DialogButton::Ok }); - if (button == DialogButton::Ok) { - return false; - } - } - - 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())) - { - auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Custom Wing Error", "Duplicate wing names in squadron wing list.", - { DialogButton::Ok }); - if (button == DialogButton::Ok) { - return false; - } - } - - if (!stricmp(_m_tvt[0].c_str(), _m_tvt[1].c_str())) - { - auto button = _viewport->dialogProvider->showButtonDialog(DialogType::Error, "Custom Wing Error", "Duplicate wing names in team-versus-team wing list.", - { DialogButton::Ok }); - if (button == DialogButton::Ok) { - return false; - } - } - - - // 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(); - - return true; -} - -void CustomWingNamesDialogModel::reject() { -} - -void CustomWingNamesDialogModel::initializeData() { - int i; - - // init starting wings - for (i = 0; i < MAX_STARTING_WINGS; i++) { - _m_starting[i] = Starting_wing_names[i]; - } - - // init squadron wings - for (i = 0; i < MAX_SQUADRON_WINGS; i++) { - _m_squadron[i] = Squadron_wing_names[i]; - } - - // init tvt wings - for (i = 0; i < MAX_TVT_WINGS; i++) { - _m_tvt[i] = TVT_wing_names[i]; - } -} - -void CustomWingNamesDialogModel::setStartingWing(SCP_string str, int index) { - modify(_m_starting[index], str); -} - -void CustomWingNamesDialogModel::setSquadronWing(SCP_string str, int index) { - modify(_m_squadron[index], str); -} - -void CustomWingNamesDialogModel::setTvTWing(SCP_string str, int index) { - modify(_m_tvt[index], str); -} - -SCP_string CustomWingNamesDialogModel::getStartingWing(int index) { - return _m_starting[index]; -} - -SCP_string CustomWingNamesDialogModel::getSquadronWing(int index) { - return _m_squadron[index]; -} - -SCP_string CustomWingNamesDialogModel::getTvTWing(int index) { - return _m_tvt[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;; -} - -} -} -} \ No newline at end of file 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/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/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/mission/dialogs/JumpNodeEditorDialogModel.cpp b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp new file mode 100644 index 00000000000..2034ef5484b --- /dev/null +++ b/qtfred/src/mission/dialogs/JumpNodeEditorDialogModel.cpp @@ -0,0 +1,379 @@ +#include "JumpNodeEditorDialogModel.h" + +#include "globalincs/linklist.h" +#include +#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'; + + lcl_fred_replace_stuff(_display); + + 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/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/mission/dialogs/MissionEventsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp new file mode 100644 index 00000000000..022bad69e72 --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionEventsDialogModel.cpp @@ -0,0 +1,1500 @@ +#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); + lcl_fred_replace_stuff(event.objective_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); + lcl_fred_replace_stuff(event.objective_key_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/mission/dialogs/MissionGoalsDialogModel.cpp b/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp index b1328360e26..ed9d5d605c0 100644 --- a/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp +++ b/qtfred/src/mission/dialogs/MissionGoalsDialogModel.cpp @@ -1,12 +1,8 @@ -// -// - +#include #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 +10,6 @@ MissionGoalsDialogModel::MissionGoalsDialogModel(QObject* parent, fso::fred::Edi bool MissionGoalsDialogModel::apply() { SCP_vector> names; - int i; auto changes_detected = query_modified(); @@ -24,7 +19,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 +45,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,19 +65,19 @@ 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(i)); if (m_goals[i].name.empty()) m_goals[i].name = ""; @@ -103,18 +98,17 @@ bool MissionGoalsDialogModel::isGoalVisible(const mission_goal& goal) const { return (goal.type & GOAL_TYPE_MASK) == m_display_goal_types; } void MissionGoalsDialogModel::setGoalDisplayType(int type) { - m_display_goal_types = type; + modify(m_display_goal_types, type); } -bool MissionGoalsDialogModel::query_modified() { - int i; - +bool MissionGoalsDialogModel::query_modified() +{ if (modified) return true; if (Mission_goals.size() != m_goals.size()) return true; - for (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/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/MissionSpecs/CustomWingNamesDialogModel.cpp b/qtfred/src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.cpp new file mode 100644 index 00000000000..ffcf8a2ddee --- /dev/null +++ b/qtfred/src/mission/dialogs/MissionSpecs/CustomWingNamesDialogModel.cpp @@ -0,0 +1,132 @@ +#include "CustomWingNamesDialogModel.h" + +#include "ship/ship.h" + +namespace fso::fred::dialogs { + +CustomWingNamesDialogModel::CustomWingNamesDialogModel(QObject * parent, EditorViewport * viewport) : + AbstractDialogModel(parent, viewport) { +} + +bool CustomWingNamesDialogModel::apply() { + for (auto& wing : _m_starting) { + Editor::strip_quotation_marks(wing); + } + + for (auto& wing : _m_squadron) { + Editor::strip_quotation_marks(wing); + } + + for (auto& wing : _m_tvt) { + Editor::strip_quotation_marks(wing); + } + + 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 }); + if (button == DialogButton::Ok) { + return false; + } + } + + 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 }); + if (button == DialogButton::Ok) { + return false; + } + } + + 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 }); + if (button == DialogButton::Ok) { + return false; + } + } + + 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 }); + if (button == DialogButton::Ok) { + return false; + } + } + + + // 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::setInitialStartingWings(const std::array& startingWings) +{ + _m_starting = startingWings; +} + +void CustomWingNamesDialogModel::setInitialSquadronWings(const std::array& squadronWings) +{ + _m_squadron = squadronWings; +} + +void CustomWingNamesDialogModel::setInitialTvTWings(const std::array& tvtWings) +{ + _m_tvt = tvtWings; +} + +const std::array& CustomWingNamesDialogModel::getStartingWings() const { + return _m_starting; +} + +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(const SCP_string& str, int index) +{ + modify(_m_squadron[index], str); +} + +void CustomWingNamesDialogModel::setTvTWing(const SCP_string& str, int index) +{ + modify(_m_tvt[index], str); +} + +const SCP_string& CustomWingNamesDialogModel::getStartingWing(int index) +{ + return _m_starting[index]; +} + +const SCP_string& CustomWingNamesDialogModel::getSquadronWing(int index) +{ + return _m_squadron[index]; +} + +const SCP_string& CustomWingNamesDialogModel::getTvTWing(int index) +{ + return _m_tvt[index]; +} + +} // 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/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/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/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/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/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/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/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 68f28a2d20e..b7774467521 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 @@ -91,6 +92,7 @@ namespace fso { int type, wing = -1; int cargo = 0, base_ship, base_player, pship = -1; int escort_count; + respawn_priority = 0; texenable = true; std::set current_orders; pship_count = 0; // a total count of the player ships not marked @@ -169,9 +171,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; @@ -274,8 +278,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; @@ -334,7 +339,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++) { @@ -348,6 +352,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 +380,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 +388,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 +435,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 +600,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,9 +638,30 @@ 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) + { + 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); - + 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); } @@ -641,6 +678,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) { @@ -870,7 +908,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 +918,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; @@ -1008,6 +1056,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); @@ -1321,12 +1378,12 @@ 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); } - 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 5a2ac86f0a9..f521f28ebe5 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; @@ -70,15 +71,20 @@ class ShipEditorDialogModel : public AbstractDialogModel { bool texenable = true; + int respawn_priority; + public: ShipEditorDialogModel(QObject* parent, EditorViewport* viewport); void initializeData(); 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; @@ -118,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); @@ -177,8 +186,8 @@ 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); - std::set getShipOrders() const; + bool wing_is_player_wing(const int) const; + const std::set &getShipOrders() const; bool getTexEditEnable() const; /** 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..c894bd8b34a 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 @@ -140,7 +141,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 +595,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 +615,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); @@ -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) { @@ -883,6 +885,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/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/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp new file mode 100644 index 00000000000..bbd03657db4 --- /dev/null +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.cpp @@ -0,0 +1,374 @@ +#include "ShipWeaponsDialogModel.h" +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; +} +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(); +} +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 = 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])) { + 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") { + Ships[ship].weapons.ai_class = newClass; + } else { + subsys->weapons.ai_class = newClass; + } + } +} +int Banks::getInitalAI() const +{ + 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(); + + m_ship = _editor->cur_ship; + if (m_ship == -1) + m_ship = Objects[_editor->currentObject].instance; + + if (m_isMultiEdit) { + 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])) { + int inst = ptr->instance; + if (!(Ship_info[Ships[inst].ship_info_index].is_big_or_huge())) + 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 = false; + 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 < 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); + } + 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; + 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 < static_cast(banks->getBanks().size()); i++) { + if (banks->getBanks()[i]->getWeaponId() != pss->weapons.primary_bank_weapons[i]) { + banks->getBanks()[i]->setWeapon(-2); + } + if (banks->getBanks()[i]->getAmmo() != pss->weapons.primary_bank_ammo[i]) { + banks->getBanks()[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 < 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); + } + 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; + 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 < static_cast(banks->getBanks().size()); 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 = 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])) { + int inst = ptr->instance; + saveShip(inst); + } + ptr = GET_NEXT(ptr); + } + } 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 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 new file mode 100644 index 00000000000..d63908e1c6b --- /dev/null +++ b/qtfred/src/mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h @@ -0,0 +1,88 @@ +#pragma once + +#include "../AbstractDialogModel.h" + +#include + +namespace fso::fred { +struct Bank; +struct Banks { + Banks(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; + SCP_vector getBanks() const; + int getAiClass() const; + void setAiClass(int); + bool m_isMultiEdit; + int getInitalAI() const; + + 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 { +/** + * @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); + + 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); + bool m_isMultiEdit; + int m_ship; + bool big = true; + SCP_vector PrimaryBanks; + SCP_vector SecondaryBanks; + // SCP_vector TertiaryBanks; +}; +} // namespace dialogs +} // 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 new file mode 100644 index 00000000000..54f5a292eca --- /dev/null +++ b/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.cpp @@ -0,0 +1,150 @@ +#include "WeaponsTBLViewerModel.h" + +#include +namespace fso::fred::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 fso::fred::dialogs \ 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..97a0e3eee5e --- /dev/null +++ b/qtfred/src/mission/dialogs/ShipEditor/WeaponsTBLViewerModel.h @@ -0,0 +1,18 @@ +#pragma once + +#include "../AbstractDialogModel.h" + +namespace fso::fred::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 fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.cpp b/qtfred/src/mission/dialogs/VariableDialogModel.cpp new file mode 100644 index 00000000000..d9ed801ad0d --- /dev/null +++ b/qtfred/src/mission/dialogs/VariableDialogModel.cpp @@ -0,0 +1,2386 @@ +#include "VariableDialogModel.h" +#include "parse/sexp.h" +#include +#include + +namespace fso::fred::dialogs { + static int _textMode = 0; + +VariableDialogModel::VariableDialogModel(QObject* parent, EditorViewport* viewport) + : AbstractDialogModel(parent, viewport) +{ + _deleteWarningCount = 0; + initializeData(); +} + +void VariableDialogModel::reject() +{ + _variableItems.clear(); + _containerItems.clear(); +} + +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; + } + } + + SCP_string messageOut; + SCP_string messageBuffer; + + if (!duplicates.empty()){ + for (const auto& item : duplicates){ + if (messageBuffer.empty()){ + messageBuffer = "\"" + item + "\""; + } else { + messageBuffer += ", ""\"" + item + "\""; + } + } + + 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 + "\", "; + duplicateKeys.push_back(temp); + } + + if (key.empty()){ + ++emptyKeys; + } else if (!container.stringKeys){ + if (key != trimIntegerString(key)){ + ++notNumberKeys; + } + + } + } + } + } + + messageBuffer.clear(); + + if (!duplicates.empty()){ + for (const auto& item : duplicates){ + if (messageBuffer.empty()){ + messageBuffer = "\"" + item + "\""; + } else { + messageBuffer += ", ""\"" + item + "\""; + } + } + + SCP_string temp; + + sprintf(temp, "There are %zu duplicate containers:\n\n", duplicates.size()); + messageOut += temp + messageBuffer + "\n"; + } + + messageBuffer.clear(); + + if (!duplicateKeys.empty()){ + for (const auto& key : duplicateKeys){ + messageBuffer += key; + } + + SCP_string temp; + + sprintf(temp, "There are %zu duplicate map keys:\n\n", duplicateKeys.size()); + messageOut += temp + 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 (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"; + } + + + if (messageOut.empty()){ + return true; + } else { + messageOut = "Please correct these issues. The editor cannot apply your changes until they are fixed:\n\n" + messageOut; + + QMessageBox msgBox; + msgBox.setText(messageOut.c_str()); + msgBox.setStandardButtons(QMessageBox::Ok); + msgBox.exec(); + + return false; + } +} + +sexp_container VariableDialogModel::createContainer(const containerInfo& infoIn) +{ + sexp_container containerOut; + + containerOut.container_name = infoIn.name; + + // handle type info, which defaults to List + if (!infoIn.list) { + containerOut.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; + } + + + // 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; + } + + + // Handle contained data + if (infoIn.list){ + + 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 < static_cast(infoIn.keys.size()); ++x){ + if (infoIn.string){ + containerOut.map_data[infoIn.keys[x]] = infoIn.stringValues[x]; + } else { + containerOut.map_data[infoIn.keys[x]] = std::to_string(infoIn.numberValues[x]); + } + } + } + + return containerOut; +} + +bool VariableDialogModel::apply() +{ + // what did we delete from the original list? We need to check these references and clean them. + SCP_vector> nameChangedVariables; + bool found; + + // first we have to edit known variables. + for (auto& variable : _variableItems){ + found = false; + + // set of instructions for updating variables + if (!variable.originalName.empty()) { + 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); + } else { + if (variable.name != variable.originalName) { + nameChangedVariables.emplace_back(i, variable.originalName); + } + + strcpy_s(Sexp_variables[i].variable_name, variable.name.c_str()); + Sexp_variables[i].type = variable.flags; + + if (variable.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].type |= SEXP_VARIABLE_NUMBER; + } + } + + found = true; + break; + } + } + + // 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); + } else { + variable.flags |= SEXP_VARIABLE_NUMBER; + sexp_add_variable(std::to_string(variable.numberValue).c_str(), variable.name.c_str(), variable.flags, -1); + } + } + } + + + SCP_vector newContainers; + SCP_unordered_map renamedContainers; + + for (const auto& container : _containerItems){ + newContainers.push_back(createContainer(container)); + + if (!container.originalName.empty() && container.name != container.originalName){ + renamedContainers[container.originalName] = container.name; + } + } + + update_sexp_containers(newContainers, renamedContainers); + + return true; +} + +void VariableDialogModel::initializeData() +{ + _variableItems.clear(); + _containerItems.clear(); + + 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; + 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; + + try { + item.numberValue = std::stoi(Sexp_variables[i].text); + } + catch (...) { + item.numberValue = 0; + } + + item.stringValue = ""; + } + } + } + + const auto& containers = get_all_sexp_containers(); + + for (const auto& container : containers) { + _containerItems.emplace_back(); + auto& newContainer = _containerItems.back(); + + newContainer.name = container.container_name; + newContainer.originalName = newContainer.name; + newContainer.deleted = false; + + if (any(container.type & ContainerType::STRING_DATA)) { + newContainer.string = true; + } else if (any(container.type & ContainerType::NUMBER_DATA)) { + newContainer.string = false; + } + + // using the SEXP variable version of these values here makes things easier + if (any(container.type & ContainerType::SAVE_TO_PLAYER_FILE)) { + newContainer.flags |= SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } + + if (any(container.type & ContainerType::SAVE_ON_MISSION_CLOSE)) { + newContainer.flags |= SEXP_VARIABLE_SAVE_ON_MISSION_CLOSE; + } + + if (any(container.type & ContainerType::SAVE_ON_MISSION_PROGRESS)) { + newContainer.flags |= SEXP_VARIABLE_SAVE_ON_MISSION_PROGRESS; + } + + if (any(container.type & ContainerType::NETWORK)) { + newContainer.flags |= SEXP_VARIABLE_NETWORK; + } + + 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 { + newContainer.stringKeys = any(container.type & ContainerType::STRING_KEYS); + + 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.emplace_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) +{ + auto variable = lookupVariable(index); + return (variable) ? (variable->string) : true; +} + +bool VariableDialogModel::getVariableNetworkStatus(int index) +{ + 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(int index) +{ + auto variable = lookupVariable(index); + + 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(int index) +{ + auto variable = lookupVariable(index); + return (variable) ? ((variable->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) != 0) : false; +} + + +SCP_string VariableDialogModel::getVariableStringValue(int index) +{ + auto variable = lookupVariable(index); + return (variable && variable->string) ? (variable->stringValue) : ""; +} + +int VariableDialogModel::getVariableNumberValue(int index) +{ + auto variable = lookupVariable(index); + return (variable && !variable->string) ? (variable->numberValue) : 0; +} + + + +// true on string, false on number +bool VariableDialogModel::setVariableType(int index, bool string) +{ + auto variable = lookupVariable(index); + + // 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; + } + + if (!safeToAlterVariable(index)){ + return variable->string; + } + + // Here we change the variable type! + // this variable is currently a string + if (variable->string) { + // no risk change, because no string was specified. + if (variable->stringValue.empty()) { + variable->string = string; + return variable->string; + } else { + // 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 and we have to use the old value. + catch (...) {} + + } + + variable->string = string; + return variable->string; + } + + // this variable is currently a number + } else { + // safe change because there was no number value specified + if (variable->numberValue == 0){ + variable->string = string; + return variable->string; + } else { + // if there was no previous string value + if (variable->stringValue.empty()){ + sprintf(variable->stringValue, "%i", variable->numberValue); + } + + variable->string = string; + return variable->string; + } + } +} + +bool VariableDialogModel::setVariableNetworkStatus(int index, bool network) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable){ + return false; + } + + if (!(variable->flags & SEXP_VARIABLE_NETWORK) && network){ + variable->flags |= SEXP_VARIABLE_NETWORK; + } else { + variable->flags &= ~SEXP_VARIABLE_NETWORK; + } + return network; +} + +int VariableDialogModel::setVariableOnMissionCloseOrCompleteFlag(int index, int flags) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable || flags < 0 || flags > 2){ + return 0; + } + + 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; +} + +bool VariableDialogModel::setVariableEternalFlag(int index, bool eternal) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable){ + return false; + } + + if (eternal) { + variable->flags |= SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } else { + variable->flags &= ~SEXP_VARIABLE_SAVE_TO_PLAYER_FILE; + } + + return eternal; +} + +SCP_string VariableDialogModel::setVariableStringValue(int index, const SCP_string& value) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable || !variable->string){ + return ""; + } + + if (!safeToAlterVariable(index)){ + return variable->stringValue; + } + + variable->stringValue = value; + return value; +} + +int VariableDialogModel::setVariableNumberValue(int index, int value) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable || variable->string){ + return 0; + } + + if (!safeToAlterVariable(index)){ + return variable->numberValue; + } + + variable->numberValue = value; + + return value; +} + +SCP_string VariableDialogModel::addNewVariable() +{ + variableInfo* variable = nullptr; + int count = 1; + SCP_string name; + + if (atMaxVariables()){ + return ""; + } + + do { + name = ""; + sprintf(name, "newVar%i", count); + variable = lookupVariableByName(name); + ++count; + } while (variable != nullptr && count < MAX_SEXP_VARIABLES); + + if (variable){ + return ""; + } + + _variableItems.emplace_back(); + _variableItems.back().name = name; + return name; +} + +SCP_string VariableDialogModel::addNewVariable(SCP_string nameIn) +{ + if (atMaxVariables()){ + return ""; + } + + _variableItems.emplace_back(); + _variableItems.back().name.substr(0, TOKEN_LENGTH - 1) = std::move(nameIn); + return _variableItems.back().name; +} + +SCP_string VariableDialogModel::changeVariableName(int index, SCP_string newName) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable){ + return ""; + } + + if (!safeToAlterVariable(index)){ + return variable->name; + } + + // Truncate name if needed + 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). + variable->name = newName; + return newName; +} + +SCP_string VariableDialogModel::copyVariable(int index) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable){ + return ""; + } + + if (atMaxVariables()){ + return ""; + } + + int count = 1; + variableInfo* variableSearch; + SCP_string newName; + + do { + sprintf(newName, "%s_%i", variable->name.substr(0, TOKEN_LENGTH - 4).c_str(), count); + variableSearch = lookupVariableByName(newName); + + // open slot found! + if (!variableSearch){ + // create the new entry in the model + variableInfo newInfo; + + // and set everything as a copy from the original, except original name and deleted. + newInfo.name = newName; + newInfo.flags = variable->flags; + newInfo.string = variable->string; + + if (newInfo.string) { + newInfo.stringValue = variable->stringValue; + } else { + newInfo.numberValue = variable->numberValue; + } + + _variableItems.push_back(std::move(newInfo)); + + return newName; + } + + ++count; + } while (variableSearch != nullptr && count < MAX_SEXP_VARIABLES); + + return ""; +} + +// returns whether it succeeded +bool VariableDialogModel::removeVariable(int index, bool toDelete) +{ + auto variable = lookupVariable(index); + + // nothing to change, or invalid entry + if (!variable){ + return false; + } + + if (variable->deleted == toDelete || !safeToAlterVariable(index)){ + return variable->deleted; + } + + if (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; + + 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. No one expects Cybog the Careless + ++_deleteWarningCount; + } + + variable->deleted = toDelete; + return variable->deleted; + + } else { + variable->deleted = toDelete; + return variable->deleted; + } +} + +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->originalName.empty()){ + return true; + } + + return true; +} + +bool VariableDialogModel::safeToAlterVariable(const variableInfo& variableItem) +{ + // again, FIXME! Needs actally reference count. + return variableItem.originalName.empty(); +} + + +// Container Section + +// true on string, false on number +bool VariableDialogModel::getContainerValueType(int index) +{ + auto container = lookupContainer(index); + return (container) ? container->string : true; +} + +bool VariableDialogModel::getContainerKeyType(int index) +{ + auto container = lookupContainer(index); + return (container) ? container->stringKeys : true; +} + +// true on list, false on map +bool VariableDialogModel::getContainerListOrMap(int index) +{ + auto container = lookupContainer(index); + return (container) ? container->list : true; +} + +bool VariableDialogModel::getContainerNetworkStatus(int index) +{ + 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(int index) +{ + auto container = lookupContainer(index); + + 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(int index) +{ + auto container = lookupContainer(index); + return (container) ? ((container->flags & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) != 0) : false; +} + + +bool VariableDialogModel::setContainerValueType(int index, bool type) +{ + auto container = lookupContainer(index); + + if (!container){ + return true; + } + + if (container->string == type || !safeToAlterContainer(index)){ + return container->string; + } + + if ((container->string && container->stringValues.empty()) || (!container->string && container->numberValues.empty())){ + container->string = type; + return container->string; + } + + // 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(std::to_string(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; +} + +bool VariableDialogModel::setContainerKeyType(int index, bool string) +{ + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container){ + return false; + } + + if (container->stringKeys == string || !safeToAlterContainer(index)){ + 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) { + if(key != trimIntegerString(key)){ + quickConvert = false; + break; + } + } + + // 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 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 + case QMessageBox::ActionRole: + { + int current = 0; + for (auto& key : container->keys){ + sprintf(key, "%i", current); + ++current; + } + + container->stringKeys = string; + return container->stringKeys; + } + + // filter out current keys + case QMessageBox::RejectRole: + for (auto& key: container->keys){ + key = trimIntegerString(key); + } + + container->stringKeys = string; + return container->stringKeys; + + // 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; +} + +// 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) +{ + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container){ + return !list; + } + + if (container->list == list || !safeToAlterContainer(index)){ + return container->list; + } + + if (container->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 (!container->keys.empty()){ + container->stringValues.resize(container->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 (!container->keys.empty()){ + container->numberValues.resize(container->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 editor, please report!"); + return false; + break; + } + + // 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::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); + msgBoxListToMapRetainData.exec(); + ret = msgBoxListToMapRetainData.buttonRole(msgBoxListToMapRetainData.clickedButton()); + + switch (ret) { + 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 = container->stringValues; + container->stringValues.clear(); + container->stringValues.resize(container->keys.size(), ""); + container->list = list; + return container->list; + } + + // The hard version ...... I guess it's not that bad, actually + container->keys.clear(); + + for (auto& number : container->numberValues){ + SCP_string temp; + sprintf(temp, "%i", number); + container->keys.push_back(temp); + } + + container->numberValues.clear(); + container->numberValues.resize(container->keys.size(), 0); + container->list = list; + return container->list; + break; + + 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; + + // 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. 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. + // So let's add a few keys to make them level. + int keyIndex = 0; + + while (currentSize > container->keys.size()) { + 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; + } + break; + + case QMessageBox::ApplyRole: + + container->list = list; + container->stringValues.clear(); + container->numberValues.clear(); + container->keys.clear(); + return container->list; + break; + + case QMessageBox::HelpRole: + return !list; + break; + + default: + UNREACHABLE("Bad button value from confirmation message box in the Variable editor, please report!"); + return false; + 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; + } + +} + +bool VariableDialogModel::setContainerNetworkStatus(int index, bool network) +{ + auto container = lookupContainer(index); + + // 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(int index, int flags) +{ + auto container = lookupContainer(index); + + // 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(int index, bool eternal) +{ + auto container = lookupContainer(index); + + // 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() +{ + containerInfo* container = nullptr; + int count = 1; + SCP_string name; + + do { + name = ""; + sprintf(name, "newCont%i", count); + container = lookupContainerByName(name); + ++count; + } while (container != nullptr && count < 51); + + if (container){ + return ""; + } + + _containerItems.emplace_back(); + _containerItems.back().name = name; + return _containerItems.back().name; +} + +SCP_string VariableDialogModel::addContainer(const SCP_string& nameIn) +{ + _containerItems.emplace_back(); + _containerItems.back().name = nameIn.substr(0, TOKEN_LENGTH - 1); + return _containerItems.back().name; +} + +SCP_string VariableDialogModel::copyContainer(int index) +{ + auto container = lookupContainer(index); + + // nothing to copy, invalid entry + if (!container){ + return ""; + } + + // searching for a duplicate is not that hard. + _containerItems.push_back(*container); + container = &_containerItems.back(); + + SCP_string newName; + int count = 0; + + while(true) { + sprintf(newName, "%s_%i", container->name.substr(0, TOKEN_LENGTH - 4).c_str(), count); + auto containerSearch = lookupContainerByName(newName); + + // open slot found! + if (!containerSearch){ + break; + } + ++count; + } + + _containerItems.back().name = newName; + return _containerItems.back().name; +} + +SCP_string VariableDialogModel::changeContainerName(int index, const SCP_string& newName) +{ + auto container = lookupContainer(index); + + // nothing to change, or invalid entry + if (!container || !safeToAlterContainer(index)){ + return ""; + } + + // 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; +} + +bool VariableDialogModel::removeContainer(int index, bool toDelete) +{ + auto container = lookupContainer(index); + + if (!container){ + return false; + } + + if (container->deleted == toDelete || !safeToAlterContainer(index)){ + 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 container->deleted; + } + + // adjust to the user's actions. If they are deleting container after container, allow after a while. + ++_deleteWarningCount; + } + + container->deleted = toDelete; + return container->deleted; + + } else { + container->deleted = toDelete; + return container->deleted; + } +} + +SCP_string VariableDialogModel::addListItem(int index) +{ + auto container = lookupContainer(index); + + 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::addListItem(int index, const SCP_string& item) +{ + auto container = lookupContainer(index); + + if (!container){ + return ""; + } + + if (container->string) { + container->stringValues.push_back(item.substr(0, TOKEN_LENGTH - 1)); + 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); + + std::pair ret = {"", ""}; + + // no container available + if (!container){ + return ret; + } + + bool conflict; + int count = 0; + SCP_string newKey; + + do { + conflict = false; + + if (container->stringKeys){ + sprintf(newKey, "key%i", count); + } else { + sprintf(newKey, "%i", count); + } + + for (const auto& key : container->keys) { + if (key == newKey){ + conflict = true; + break; + } + } + + ++count; + } while (conflict && count < 101); + + if (conflict) { + return ret; + } + + ret.first = newKey; + container->keys.push_back(newKey); + + if (container->string){ + ret.second = ""; + } else { + ret.second = "0"; + } + + container->stringValues.emplace_back(); + container->numberValues.push_back(0); + + sortMap(index); + + return ret; +} + +// Overload for specified key and/or Value +std::pair VariableDialogModel::addMapItem(int index, const SCP_string& key, const SCP_string& value) +{ + auto container = lookupContainer(index); + + std::pair ret = { "", "" }; + + // no container available + if (!container) { + return ret; + } + + bool conflict = false; + 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 (const auto& current_key : container->keys) { + if (current_key == newKey){ + conflict = true; + break; + } + } + + ++count; + } while (conflict && count < 101); + } else { + if (container->stringKeys){ + newKey = key.substr(0, TOKEN_LENGTH - 1); + } else { + newKey = trimIntegerString(key); + } + } + + if (conflict) { + return ret; + } + + ret.first = newKey; + container->keys.push_back(ret.first); + + if (container->string) { + ret.second = value.substr(0, TOKEN_LENGTH - 1); + container->stringValues.push_back(ret.second); + container->numberValues.push_back(0); + } else { + try { + ret.second = trimIntegerString(value); + container->numberValues.push_back(std::stoi(ret.second)); + ret.second = value; + } + catch (...) { + ret.second = "0"; + container->numberValues.push_back(0); + } + + container->stringValues.emplace_back(""); + } + + sortMap(index); + return ret; +} + +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()))){ + return ""; + } + + if (container->string) { + container->stringValues.push_back(container->stringValues[index]); + return container->stringValues.back(); + } else { + container->numberValues.push_back(container->numberValues[index]); + return "0"; + } + +} + +SCP_string VariableDialogModel::changeListItem(int containerIndex, int index, const SCP_string& newString) +{ + auto container = lookupContainer(containerIndex); + + if (!container){ + return ""; + } + + if (container->string){ + auto listItem = lookupContainerStringItem(containerIndex, index); + + if (!listItem){ + return ""; + } + + *listItem = newString.substr(0, TOKEN_LENGTH - 1); + + } else { + auto listItem = lookupContainerNumberItem(containerIndex, index); + + if (!listItem){ + return ""; + } + + try{ + *listItem = std::stoi(trimIntegerString(newString)); + } + catch(...){ + SCP_string temp; + sprintf(temp, "%i", *listItem); + return temp; + } + } + + return ""; +} + +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()))){ + 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 container->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); + } else { + container->numberValues.erase(container->numberValues.begin() + index); + } + + return true; +} + +std::pair VariableDialogModel::copyMapItem(int index, int mapIndex) +{ + auto container = lookupContainer(index); + + // any invalid case, early return + if (!container) { + return std::make_pair("", ""); + } + + auto key = lookupContainerKey(index, mapIndex); + + if (key == nullptr) { + return std::make_pair("", ""); + } + + + + if (container->string){ + auto value = lookupContainerStringItem(index, mapIndex); + + // not a valid value. + if (value == nullptr){ + return std::make_pair("", ""); + } + + SCP_string copyValue = *value; + SCP_string baseNewKey = key->substr(0, TOKEN_LENGTH - 4); + SCP_string newKey = baseNewKey + "0"; + int count = 0; + + bool found; + + do { + found = false; + for (const auto& current_key : container->keys){ + if (current_key == newKey) { + found = true; + break; + } + } + + // attempt did not work, try next number + if (found) { + sprintf(newKey, "%s%i", baseNewKey.c_str(), ++count); + } + + } while (found && count < 999); + + // we could not generate a new key .... somehow. + if (found){ + return std::make_pair("", ""); + } + + container->keys.push_back(newKey); + container->stringValues.push_back(copyValue); + container->numberValues.push_back(0); + + sortMap(index); + return std::make_pair(newKey, copyValue); + + } else { + auto value = lookupContainerNumberItem(index, mapIndex); + + // no valid value. + if (!value){ + return std::make_pair("", ""); + } + + int copyValue = *value; + SCP_string baseNewKey = key->substr(0, TOKEN_LENGTH - 4); + SCP_string newKey = baseNewKey + "0"; + int count = 0; + + bool found; + + do { + found = false; + for (const auto& current_key : container->keys){ + if (current_key == newKey) { + found = true; + break; + } + } + + // attempt did not work, try next number + if (found) { + sprintf(newKey, "%s%i", baseNewKey.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); + container->stringValues.emplace_back(); + + SCP_string temp; + sprintf(temp, "%i", copyValue); + sortMap(index); + + return std::make_pair(newKey, temp); + } + +} + +// 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 +// 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, int itemIndex) +{ + auto container = lookupContainer(index); + + if (!container){ + return false; + } + // container is valid. + + auto item = lookupContainerKey(index, itemIndex); + + if (!item){ + return false; + } + // 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 container->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); + } else if (!container->string && lookupContainerNumberItem(index, itemIndex)){ + container->numberValues.erase(container->numberValues.begin() + itemIndex); + } else { + 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::changeMapItemKey(int index, int keyRow, const SCP_string& newKey) +{ + auto container = lookupContainer(index); + + if (!container || container->list){ + return ""; + } + + if (container->stringKeys){ + container->keys[keyRow] = newKey.substr(0, TOKEN_LENGTH - 1); + } else { + container->keys[keyRow] = trimIntegerString(newKey); + } + + sortMap(index); + return container->keys[keyRow]; +} + +SCP_string VariableDialogModel::changeMapItemStringValue(int index, int itemIndex, const SCP_string& newValue) +{ + auto item = lookupContainerStringItem(index, itemIndex); + + if (!item){ + return ""; + } + + *item = newValue.substr(0, TOKEN_LENGTH - 1); + + return *item; +} + +void VariableDialogModel::swapKeyAndValues(int index) +{ + auto container = lookupContainer(index); + + // bogus cases + if (!container || container->list || !safeToAlterContainer(index)){ + 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(keysCopy.size()); ++x) { + if (keysCopy[x].empty()){ + 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(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(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(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; + } + } + + sortMap(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.empty()){ + return true; + } + + return true; +} + +bool VariableDialogModel::safeToAlterContainer(const containerInfo& containerItem) +{ + // again, FIXME! Needs actual reference count. + return containerItem.originalName.empty(); +} + +SCP_string VariableDialogModel::changeMapItemNumberValue(int index, int itemIndex, int newValue) +{ + auto mapItem = lookupContainerNumberItem(index, itemIndex); + + if (!mapItem){ + return ""; + } + + *mapItem = newValue; + + SCP_string ret; + sprintf(ret, "%i", newValue); + return ret; +} + + +// These functions should only be called when the container is guaranteed to exist! +const SCP_vector& VariableDialogModel::getMapKeys(int index) +{ + auto container = lookupContainer(index); + + if (!container) { + SCP_string temp; + 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(temp, "getMapKeys() found that container %s is not a map.", container->name.c_str()); + throw std::invalid_argument(temp); + } + + return container->keys; +} + +// Only call when the container is guaranteed to exist! +const SCP_vector& VariableDialogModel::getStringValues(int index) +{ + auto container = lookupContainer(index); + + if (!container) { + SCP_string temp; + 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(temp, "getStringValues() found that container %s does not store strings.", container->name.c_str()); + throw std::invalid_argument(temp); + } + + return container->stringValues; +} + +// Only call when the container is guaranteed to exist! +const SCP_vector& VariableDialogModel::getNumberValues(int index) +{ + auto container = lookupContainer(index); + + if (!container) { + SCP_string temp; + 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(temp, "getNumberValues() found that container %s does not store numbers.", container->name.c_str()); + throw std::invalid_argument(temp); + } + + return container->numberValues; +} + +SCP_vector> VariableDialogModel::getVariableValues() +{ + SCP_vector> outStrings; + + for (const auto& item : _variableItems){ + SCP_string notes; + + if (!safeToAlterVariable(item)){ + notes = "Referenced"; + } else if (item.deleted){ + notes = "To Be Deleted"; + } else if (item.originalName.empty()){ + notes = "New"; + } else if ((item.string && item.stringValue.empty()) || (!item.string && item.numberValue == 0)){ + notes = "Default Value"; + } else if (item.name != item.originalName){ + notes = "Renamed"; + } else { + notes = "Unreferenced"; + } + + SCP_string temp; + sprintf(temp, "%i", item.numberValue); + outStrings.push_back(std::array{item.name, (item.string) ? item.stringValue : temp, notes}); + } + + return outStrings; +} + +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; + SCP_string listPostscript; + + SCP_string mapPrefix; + SCP_string mapMidScript; + SCP_string mapPostscript; + + switch (_textMode) { + 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. + _textMode = 0; + listPrefix = "List of "; + listPostscript = "s"; + break; + } + + switch (_textMode) { + 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; + + default: + _textMode = 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.append(listPrefix.append(type.append(listPostscript))); + + } else { + + type = mapPrefix; + + if (item.stringKeys){ + type += "String"; + } else { + type += "Number"; + } + + type += mapMidScript; + + if (item.string){ + type += "String"; + } else { + type += "Number"; + } + + type += mapPostscript; + } + + + if (!safeToAlterContainer(item)){ + notes = "Referenced"; + } else if (item.deleted) { + notes = "To Be Deleted"; + } else if (item.originalName.empty()) { + 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"; + } else { + notes = "Unreferenced"; + } + + outStrings.push_back(std::array{item.name, type, notes}); + } + + return outStrings; +} + +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(...){ + // we're up the creek if this happens anyway. + return true; + } + } + ); + } + + int y = 0; + + 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. + 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; + } else { + ++y; + } + } + + 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); +} + +bool VariableDialogModel::atMaxVariables() +{ + if (_variableItems.size() < MAX_SEXP_VARIABLES){ + return false; + } + + int count = 0; + + for (const auto& item : _variableItems){ + if (!item.deleted){ + ++count; + } + } + + return count < MAX_SEXP_VARIABLES; +} + +// 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; + // 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), + [&foundNonZero, &ret](char c) -> bool { + switch (c) { + // ignore leading zeros. If all digits are zero, this will be handled elsewhere + case '0': + return foundNonZero; + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + foundNonZero = true; + return true; + break; + // only copy the '-' char if it is the first thing to be copied. + case '-': + return ret.empty(); + default: + return false; + break; + } + } + ); + + // -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"; + } + + // here we deal with overflow values. + try { + // some OS's will deal with this properly. + long test = std::stol(ret); + + if (test > INT_MAX) { + return "2147483647"; + } else if (test < INT_MIN) { + return "-2147483648"; + } + + return ret; + } + // 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 (...){ + if (ret.size() > 10 && ret[0] == '-'){ + return "-2147483648"; + } else if (ret.size() > 9) { + return "2147483647"; + } + + // emergency return value + return "0"; + } +} + + +} diff --git a/qtfred/src/mission/dialogs/VariableDialogModel.h b/qtfred/src/mission/dialogs/VariableDialogModel.h new file mode 100644 index 00000000000..4bf7ccb8527 --- /dev/null +++ b/qtfred/src/mission/dialogs/VariableDialogModel.h @@ -0,0 +1,261 @@ +#pragma once + +#include "globalincs/pstypes.h" + +#include "AbstractDialogModel.h" +#include "parse/sexp_container.h" +#include + +namespace fso::fred::dialogs { + +struct variableInfo { + SCP_string name = ""; + SCP_string originalName; + bool deleted = false; + bool string = true; + int flags = 0; + int numberValue = 0; + SCP_string stringValue; +}; + + +struct containerInfo { + SCP_string name = ""; + bool deleted = false; + bool list = true; + bool string = true; + bool stringKeys = true; + int flags = 0; + + // 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. + // 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; +}; + +class VariableDialogModel : public AbstractDialogModel { +public: + VariableDialogModel(QObject* parent, EditorViewport* viewport); + + // true on string, false on number + 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(int index); + bool getVariableEternalFlag(int index); + + 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 + // 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(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(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 copyVariable(int index); + // returns whether it succeeded + bool removeVariable(int index, bool toDelete); + bool safeToAlterVariable(int index); + static bool safeToAlterVariable(const variableInfo& variableItem); + + // Container Section + + // 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); + // 0 neither, 1 on mission complete, 2 on mission close (higher number saves more often) + int getContainerOnMissionCloseOrCompleteFlag(int index); + 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); + bool setContainerEternalFlag(int index, bool eternal); + + SCP_string addContainer(); + 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); + + SCP_string addListItem(int index); + 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, const SCP_string& key, const SCP_string& value); + std::pair copyMapItem(int index, int itemIndex); + 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, 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); + const SCP_vector& getStringValues(int index); + const SCP_vector& getNumberValues(int index); + + void swapKeyAndValues(int index); + + bool safeToAlterContainer(int index); + static bool safeToAlterContainer(const containerInfo& containerItem); + + SCP_vector> getVariableValues(); + SCP_vector> getContainerNames(); + static void setTextMode(int modeIn); + + bool checkValidModel(); + + bool apply() override; + void reject() override; + + void initializeData(); + + static SCP_string trimIntegerString(SCP_string source); + +private: + SCP_vector _variableItems; + SCP_vector _containerItems; + + int _deleteWarningCount; + + static sexp_container createContainer(const containerInfo& infoIn); + + void sortMap(int index); + bool atMaxVariables(); + + variableInfo* lookupVariable(int index){ + if(index > -1 && index < static_cast(_variableItems.size()) ){ + return &_variableItems[index]; + } + + return nullptr; + } + + variableInfo* lookupVariableByName(const SCP_string& name){ + for (auto& variableItem : _variableItems) { + if (variableItem.name == name) { + return &variableItem; + } + } + + return nullptr; + } + + containerInfo* lookupContainer(int index){ + if(index > -1 && index < static_cast(_containerItems.size()) ){ + return &_containerItems[index]; + } + + return nullptr; + } + + containerInfo* lookupContainerByName(const SCP_string& name){ + for (auto& container : _containerItems) { + if (container.name == name) { + return &container; + } + } + + 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[containerIndex].keys[itemIndex]; + } + } + + return nullptr; + } + + SCP_string* lookupContainerKeyByName(int containerIndex, const SCP_string& keyIn){ + if(containerIndex > -1 && containerIndex < static_cast(_containerItems.size()) ){ + for (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())){ + return &_containerItems[containerIndex].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[containerIndex].numberValues[itemIndex]; + } + } + + return nullptr; + } + + + // many of the controls in this editor can lead to drastic actions, so this will be very useful. + static bool confirmAction(const SCP_string& question, const 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."); + return false; + break; + } + } + +}; + +} // namespace dialogs 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/mission/dialogs/VolumetricNebulaDialogModel.cpp b/qtfred/src/mission/dialogs/VolumetricNebulaDialogModel.cpp new file mode 100644 index 00000000000..6e336321380 --- /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) +{ + // 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..6515e5d9852 --- /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); + + static void makeVolumetricsCopy(volumetric_nebula& dest, const volumetric_nebula& src); + + // boilerplate + bool _bypass_errors; + + volumetric_nebula _volumetrics; +}; + +} // namespace fso::fred::dialogs 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/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/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 94e25e37b99..59ce2f4cefd 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; } @@ -671,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; @@ -2340,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); @@ -2677,6 +2706,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); @@ -2714,6 +2744,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:"); @@ -3467,15 +3498,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()); } } @@ -3503,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:"); @@ -3924,7 +3969,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) { @@ -5008,13 +5064,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 { @@ -5329,39 +5385,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/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; 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(); diff --git a/qtfred/src/ui/FredView.cpp b/qtfred/src/ui/FredView.cpp index 851a71faa1f..3a9ecfd1e2a 100644 --- a/qtfred/src/ui/FredView.cpp +++ b/qtfred/src/ui/FredView.cpp @@ -12,18 +12,23 @@ #include #include #include -#include +#include +#include #include +#include #include #include +#include #include #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -31,6 +36,9 @@ #include #include #include +#include +#include +#include #include #include "mission/Editor.h" @@ -135,6 +143,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); }); } @@ -143,7 +152,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); @@ -702,11 +712,17 @@ void FredView::keyReleaseEvent(QKeyEvent* event) { _inKeyReleaseHandler = false; } -void FredView::on_actionEvents_triggered(bool) { - auto eventEditor = new dialogs::EventEditorDialog(this, _viewport); +void FredView::on_actionMission_Events_triggered(bool) { + auto eventEditor = new dialogs::MissionEventsDialog(this, _viewport); 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; } @@ -724,6 +740,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); @@ -739,13 +761,41 @@ void FredView::on_actionWaypoint_Paths_triggered(bool) { editorDialog->setAttribute(Qt::WA_DeleteOnClose); editorDialog->show(); } -void FredView::on_actionShips_triggered(bool) +void FredView::on_actionJump_Nodes_triggered(bool) { - auto editorDialog = new dialogs::ShipEditorDialog(this, _viewport); + auto editorDialog = new dialogs::JumpNodeEditorDialog(this, _viewport); editorDialog->setAttribute(Qt::WA_DeleteOnClose); editorDialog->show(); +} +void FredView::on_actionShips_triggered(bool) +{ + 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_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); @@ -770,6 +820,11 @@ void FredView::on_actionLoadout_triggered(bool) { editorDialog->setAttribute(Qt::WA_DeleteOnClose); editorDialog->show(); } +void FredView::on_actionVariables_triggered(bool) { + auto editorDialog = new dialogs::VariableDialog(this, _viewport); + editorDialog->show(); +} + DialogButton FredView::showButtonDialog(DialogType type, const SCP_string& title, const SCP_string& message, @@ -849,7 +904,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 { @@ -1125,6 +1185,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; } @@ -1149,17 +1212,36 @@ 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); 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(); } +void FredView::on_actionMusic_Player_triggered(bool) +{ + auto dialog = new dialogs::MusicPlayerDialog(this, _viewport); + dialog->setAttribute(Qt::WA_DeleteOnClose); + 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 7958dc9ae66..59be40a9661 100644 --- a/qtfred/src/ui/FredView.h +++ b/qtfred/src/ui/FredView.h @@ -19,6 +19,11 @@ namespace fred { class Editor; class RenderWidget; +namespace dialogs { +class ShipEditorDialog; +class WingEditorDialog; +} + namespace Ui { class FredView; } @@ -43,6 +48,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); @@ -83,13 +91,17 @@ 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_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); + 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); @@ -129,15 +141,19 @@ 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); 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_Objectives_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 @@ -205,6 +221,9 @@ class FredView: public QMainWindow, public IDialogProvider { Editor* fred = nullptr; 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/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/src/ui/dialogs/BackgroundEditorDialog.cpp b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp index 937cc7082f9..74c890af53c 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.cpp @@ -1,23 +1,961 @@ #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 +#include +#include +#include +#include +#include + +namespace fso::fred::dialogs { + +BackgroundEditorDialog::BackgroundEditorDialog(FredView* parent, EditorViewport* viewport) : QDialog(parent), + ui(new Ui::BackgroundEditor()), _model(new BackgroundEditorDialogModel(this, viewport)), _viewport(viewport) { + + ui->setupUi(this); -BackgroundEditorDialog::BackgroundEditorDialog(FredView* parent, EditorViewport* viewport) : - QDialog(parent), ui(new Ui::BackgroundEditor()), - _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 + updateBackgroundControls(); + + // 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->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); + 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(); + + 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(); + + // 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)); + } + + ui->fogSwatch->setFrameShape(QFrame::Box); + + 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(); + + // Ambient light + ui->ambientSwatch->setMinimumSize(28, 28); + ui->ambientSwatch->setFrameShape(QFrame::Box); + + 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() +{ + 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() +{ + 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::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::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()); + + 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); + + 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::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::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::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()))); +} + +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) +{ + _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); + + 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(); +} + +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(); +} + +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); + 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) +{ + 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); } + +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)); +} + +void BackgroundEditorDialog::on_skyboxModelButton_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.toUtf8().constData()); + + updateSkyboxControls(); +} + +void BackgroundEditorDialog::on_skyboxEdit_textChanged(const QString& arg1) +{ + _model->setSkyboxModelName(arg1.toUtf8().constData()); + updateSkyboxControls(); } + +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); +} + +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 5827277ea65..60a8d526fed 100644 --- a/qtfred/src/ui/dialogs/BackgroundEditorDialog.h +++ b/qtfred/src/ui/dialogs/BackgroundEditorDialog.h @@ -1,32 +1,127 @@ #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: + + // 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); + 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(); + + // 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(); + + // 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); + + // Ambient Light + void on_ambientLightRedSlider_valueChanged(int value); + void on_ambientLightGreenSlider_valueChanged(int value); + void on_ambientLightBlueSlider_valueChanged(int value); + + // Skybox + void on_skyboxModelButton_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); + + // 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; - EditorViewport* _viewport; + std::unique_ptr _model; + EditorViewport* _viewport; + + void initializeUi(); + void updateUi(); + void updateBackgroundControls(); + void refreshBitmapList(); + void updateBitmapControls(); + void refreshSunList(); + void updateSunControls(); + void updateNebulaControls(); + void updateFogSwatch(); + void updateOldNebulaControls(); + void updateAmbientLightControls(); + void updateAmbientSwatch(); + void updateSkyboxControls(); + void updateMiscControls(); + + static int pickBackgroundIndexDialog(QWidget* parent, int count, int defaultIndex = 0); }; -} -} -} +} // 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/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/EventEditorDialog.cpp b/qtfred/src/ui/dialogs/EventEditorDialog.cpp deleted file mode 100644 index 7800fad4a02..00000000000 --- a/qtfred/src/ui/dialogs/EventEditorDialog.cpp +++ /dev/null @@ -1,1127 +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(); - - 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(); - }); - 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/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/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/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/src/ui/dialogs/JumpNodeEditorDialog.cpp b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp new file mode 100644 index 00000000000..49c6e07d7fb --- /dev/null +++ b/qtfred/src/ui/dialogs/JumpNodeEditorDialog.cpp @@ -0,0 +1,129 @@ +#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); +} + +} // 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/src/ui/dialogs/LoadoutDialog.cpp b/qtfred/src/ui/dialogs/LoadoutDialog.cpp index 8952ad3d317..5e569ed0b16 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.push_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.push_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.push_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.push_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(); @@ -527,9 +531,9 @@ void LoadoutDialog::onClearAllUsedWeaponsPressed() _lastSelectionChanged = USED_WEAPONS; } -// TODO! Finish writing a trigger to open that dialog, once the variable editor is created void LoadoutDialog::openEditVariablePressed() { + reinterpret_cast(parent())->on_actionVariables_triggered(true); } void LoadoutDialog::onSelectionRequiredPressed() @@ -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) && !stricmp(ui->usedShipsList->item(x, 0)->text().toStdString().c_str(), shipName.c_str())) { + 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 (!stricmp(ui->listShipsNotUsed->item(x)->text().toStdString().c_str(), shipName.c_str())) { + 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 (!stricmp(ui->listShipsNotUsed->item(x)->text().toStdString().c_str(), shipName.c_str())) { + 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) && - !stricmp(ui->usedShipsList->item(x, 0)->text().toStdString().c_str(), shipName.c_str())) { + 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) && !stricmp(ui->usedWeaponsList->item(x, 0)->text().toStdString().c_str(), weaponName.c_str())) { + 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 (!stricmp(ui->listWeaponsNotUsed->item(x)->text().toStdString().c_str(), weaponName.c_str())) { + 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) && !stricmp(ui->listWeaponsNotUsed->item(x)->text().toStdString().c_str(), weaponName.c_str())) { + 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) && - !stricmp(ui->usedWeaponsList->item(x, 0)->text().toStdString().c_str(), weaponName.c_str())) { + lcase_equal(usedWepName, weaponName)) { ui->usedWeaponsList->setRowHidden(x, true); break; } @@ -699,9 +711,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 +724,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 +736,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 +753,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 +844,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())) { + 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) && !stricmp(ui->usedWeaponsList->item(x, 0)->text().toStdString().c_str(), weapon.c_str())) { + 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().c_str()); + 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().c_str()); + SCP_string weaponName = ui->usedWeaponsList->item(x, 0)->text().toUtf8().constData(); + namesOut.emplace_back(weaponName); } } 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/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/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/src/ui/dialogs/MissionSpecDialog.cpp b/qtfred/src/ui/dialogs/MissionSpecDialog.cpp index 1b9a87c8f52..5de5794e363 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/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/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/src/ui/dialogs/ReinforcementsEditorDialog.cpp b/qtfred/src/ui/dialogs/ReinforcementsEditorDialog.cpp index 5021ae806fd..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.push_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.push_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.push_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/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/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/src/ui/dialogs/ShipEditor/BankModel.cpp b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp new file mode 100644 index 00000000000..e5d6f148004 --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.cpp @@ -0,0 +1,404 @@ +#include "ShipWeaponsDialog.h" + +#include +#include +namespace fso::fred { +BankTreeItem::BankTreeItem(BankTreeItem* parentItem, QString inName) : name(std::move(inName)), 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* inBank, BankTreeItem* parentItem) : BankTreeItem(parentItem), bank(inBank) +{ + 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& 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()] + ")"; + break; + default: + return {}; + } +} + +Qt::ItemFlags BankTreeLabel::getFlags(int column) const +{ + Q_UNUSED(column); + return Qt::ItemIsSelectable; +} + +void BankTreeLabel::setAIClass(int value) +{ + Assert(banks != nullptr); + banks->setAiClass(value); +} + +bool BankTreeLabel::setData(int column, const QVariant& value) +{ + Q_UNUSED(column); + 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 +{ + Q_UNUSED(orientation); + 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); + QVector roles; + roles.append(role); + QAbstractItemModel::dataChanged(index, index, roles); + 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; + + if (row == -1 && !parent.isValid()) + 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) +{ + auto item = dynamic_cast(this->getItem(index)); + Assert(item != nullptr); + if (item != nullptr) { + item->setWeapon(data); + QVector roles; + QAbstractItemModel::dataChanged(index, index, roles); + } +} + +bool BankTreeRoot::setData(int column, const QVariant& value) +{ + Q_UNUSED(column); + Q_UNUSED(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 +{ + Q_UNUSED(column); + 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 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 new file mode 100644 index 00000000000..a0b42d475e4 --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/BankModel.h @@ -0,0 +1,94 @@ +#pragma once +#include "mission/dialogs/ShipEditor/ShipWeaponsDialogModel.h" + +#include +namespace fso::fred { +class BankTreeItem { + public: + explicit BankTreeItem(BankTreeItem* parentItem = nullptr, QString inName = ""); + 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* inBank, 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 override; + bool + dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) override; + 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; + 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 fso::fred \ 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/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/ShipEditor/ShipEditorDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.cpp index d69dc3a7b5d..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,26 +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->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); @@ -49,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()); @@ -167,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); } @@ -188,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(); } @@ -207,161 +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()); - 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(); @@ -400,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(); @@ -457,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() @@ -555,6 +449,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 +460,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); @@ -597,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)) @@ -656,35 +557,6 @@ void ShipEditorDialog::enableDisable() this->setWindowTitle("Edit Ship"); } } - -/*--------------------------------------------------------- - WARNING -Do not try to optimise string entries this convoluted method is necessary to avoid fata 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); - } -} -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(); @@ -712,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() { @@ -807,7 +598,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() { @@ -827,10 +620,12 @@ void ShipEditorDialog::on_deleteButton_clicked() } void ShipEditorDialog::on_weaponsButton_clicked() { - //TODO + auto dialog = new dialogs::ShipWeaponsDialog(this, _viewport, getIfMultipleShips()); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->show(); } void ShipEditorDialog::on_playerOrdersButton_clicked() - { +{ auto dialog = new dialogs::PlayerOrdersDialog(this, _viewport, getIfMultipleShips()); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); @@ -865,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() { @@ -887,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 39869539d36..2c0ac5e8106 100644 --- a/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipEditorDialog.h @@ -1,33 +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 @@ -85,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; @@ -92,46 +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 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/dialogs/ShipEditor/ShipWeaponsDialog.cpp b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp new file mode 100644 index 00000000000..87be9cc36be --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.cpp @@ -0,0 +1,225 @@ +#include "ShipWeaponsDialog.h" + +#include "WeaponsTBLViewer.h" +#include "ui_ShipWeaponsDialog.h" + +#include +#include +#include + +#include +#include +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); + + // 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); + 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); + 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, + 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); + + // Resize Bank view + ui->treeBanks->expandAll(); + ui->treeBanks->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + updateUI(); +} + +ShipWeaponsDialog::~ShipWeaponsDialog() +{ + delete bankModel; + 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) +{ + reject(); + event->ignore(); +} +void ShipWeaponsDialog::on_setAllButton_clicked() +{ + for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { + 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::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) { + 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; + } else { + _viewport->dialogProvider->showButtonDialog(DialogType::Error, + "Illegal 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); + 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, + &ShipWeaponsDialog::updateUI); + connect(ui->listWeapons->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &ShipWeaponsDialog::updateUI); + ui->treeBanks->setModel(bankModel); + ui->treeBanks->expandAll(); + } + updateUI(); +} +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(); + // 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)); + } + 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) +{ + m_currentAI = ui->aiCombo->itemData(index).toInt(); +} + +void ShipWeaponsDialog::on_aiButton_clicked() +{ + for (auto& index : ui->treeBanks->selectionModel()->selectedIndexes()) { + bankModel->setData(index, m_currentAI); + } +} + +void ShipWeaponsDialog::on_buttonClose_clicked() +{ + accept(); +} + +} // 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 new file mode 100644 index 00000000000..7b5efb83578 --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/ShipWeaponsDialog.h @@ -0,0 +1,68 @@ +#ifndef SHIPWEAPONSDIALOG_H +#define SHIPWEAPONSDIALOG_H + +#include "ui/dialogs/ShipEditor/BankModel.h" +#include "ui/widgets/weaponList.h" + +#include +#include + +#include +#include + +namespace fso::fred::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; + + void accept() override; + void reject() override; + + protected: + void closeEvent(QCloseEvent*) override; + + private slots: + void on_buttonClose_clicked(); + void on_aiButton_clicked(); + void on_setAllButton_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: // NOLINT(readability-redundant-access-specifiers) + 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(); + BankTreeModel* bankModel; + int dialogMode; + WeaponModel* weapons; + int m_currentAI = 0; + void aiClassChanged(const int index); +}; +} // 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 new file mode 100644 index 00000000000..78a33cb8d62 --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.cpp @@ -0,0 +1,35 @@ +#include "WeaponsTBLViewer.h" + +#include "ui_ShipTBLViewer.h" + +#include + +#include + +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) +{ + + 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 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 new file mode 100644 index 00000000000..a6bba8fe23e --- /dev/null +++ b/qtfred/src/ui/dialogs/ShipEditor/WeaponsTBLViewer.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include + +namespace fso::fred::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 fso::fred::dialogs \ No newline at end of file diff --git a/qtfred/src/ui/dialogs/VariableDialog.cpp b/qtfred/src/ui/dialogs/VariableDialog.cpp new file mode 100644 index 00000000000..217fa2317c6 --- /dev/null +++ b/qtfred/src/ui/dialogs/VariableDialog.cpp @@ -0,0 +1,1880 @@ +#include "VariableDialog.h" +#include "ui_VariableDialog.h" + +#include +#include +#include + +#include +#include +//#include + +namespace fso::fred::dialogs { + +VariableDialog::VariableDialog(FredView* parent, EditorViewport* viewport) + : QDialog(parent), ui(new Ui::VariableEditorDialog()), _model(new VariableDialogModel(this, viewport)), _viewport(viewport) +{ + this->setFocus(); + 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::reject); + connect(this, &QDialog::accepted, _model.get(), &VariableDialogModel::apply); + + connect(ui->variablesTable, + &QTableWidget::itemChanged, + this, + &VariableDialog::onVariablesTableUpdated); + + connect(ui->variablesTable, + &QTableWidget::itemSelectionChanged, + this, + &VariableDialog::onVariablesSelectionChanged); + + connect(ui->containersTable, + &QTableWidget::itemChanged, + this, + &VariableDialog::onContainersTableUpdated); + + connect(ui->containersTable, + &QTableWidget::itemSelectionChanged, + this, + &VariableDialog::onContainersSelectionChanged); + + connect(ui->containerContentsTable, + &QTableWidget::itemChanged, + this, + &VariableDialog::onContainerContentsTableUpdated); + + connect(ui->containerContentsTable, + &QTableWidget::itemSelectionChanged, + this, + &VariableDialog::onContainerContentsSelectionChanged); + + connect(ui->addVariableButton, + &QPushButton::clicked, + this, + &VariableDialog::onAddVariableButtonPressed); + + connect(ui->copyVariableButton, + &QPushButton::clicked, + this, + &VariableDialog::onCopyVariableButtonPressed); + + connect(ui->deleteVariableButton, + &QPushButton::clicked, + this, + &VariableDialog::onDeleteVariableButtonPressed); + + connect(ui->setVariableAsStringRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetVariableAsStringRadioSelected); + + connect(ui->setVariableAsNumberRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetVariableAsNumberRadioSelected); + + connect(ui->doNotSaveVariableRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onDoNotSaveVariableRadioSelected); + + connect(ui->saveVariableOnMissionCompletedRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSaveVariableOnMissionCompleteRadioSelected); + + connect(ui->saveVariableOnMissionCloseRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSaveVariableOnMissionCloseRadioSelected); + + connect(ui->networkVariableCheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onNetworkVariableCheckboxClicked); + + connect(ui->setVariableAsEternalcheckbox, + &QCheckBox::clicked, + this, + &VariableDialog::onSaveVariableAsEternalCheckboxClicked); + + connect(ui->addContainerButton, + &QPushButton::clicked, + this, + &VariableDialog::onAddContainerButtonPressed); + + connect(ui->copyContainerButton, + &QPushButton::clicked, + this, + &VariableDialog::onCopyContainerButtonPressed); + + connect(ui->deleteContainerButton, + &QPushButton::clicked, + this, + &VariableDialog::onDeleteContainerButtonPressed); + + connect(ui->setContainerAsMapRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerAsMapRadioSelected); + + connect(ui->setContainerAsListRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerAsListRadioSelected); + + connect(ui->setContainerAsStringRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerAsStringRadioSelected); + + connect(ui->setContainerAsNumberRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerAsNumberRadioSelected); + + connect(ui->setContainerKeyAsStringRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerKeyAsStringRadioSelected); + + connect(ui->setContainerKeyAsNumberRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSetContainerKeyAsNumberRadioSelected); + + connect(ui->doNotSaveContainerRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onDoNotSaveContainerRadioSelected); + + connect(ui->saveContainerOnMissionCloseRadio, + &QRadioButton::clicked, + this, + &VariableDialog::onSaveContainerOnMissionCloseRadioSelected); + + connect(ui->saveContainerOnMissionCompletedRadio, + &QRadioButton::clicked, + 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->shiftItemUpButton, + &QPushButton::clicked, + this, + &VariableDialog::onShiftItemUpButtonPressed); + + connect(ui->shiftItemDownButton, + &QPushButton::clicked, + this, + &VariableDialog::onShiftItemDownButtonPressed); + + connect(ui->swapKeysAndValuesButton, + &QPushButton::clicked, + 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")); + ui->variablesTable->setHorizontalHeaderItem(2, new QTableWidgetItem("Notes")); + 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, 220); + 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, 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. + ui->setVariableAsStringRadio->setAutoExclusive(false); + ui->setVariableAsNumberRadio->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); + ui->setContainerAsNumberRadio->setAutoExclusive(false); + 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(); + + 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(); +} + +void VariableDialog::onVariablesTableUpdated() +{ + if (_applyingModel){ + return; + } + + int currentRow = getCurrentVariableRow(); + + if (currentRow < 0){ + return; + } + + auto item = ui->variablesTable->item(currentRow, 0); + 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. + 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()) { + + _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()){ + + _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); + + // we put something in the cell, but the model couldn't process it. + if (strlen(item->text().toUtf8().constData()) && ret.empty()) { + // update of variable name failed, resync UI + apply = true; + + // we had a successful rename. So update the variable we reference. + } else if (!ret.empty()) { + item->setText(ret.c_str()); + _currentVariable = ret; + } + }// 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().toUtf8().constData(); + + // check if data column was altered + if (itemText != _currentVariableData) { + // Variable is a string + if (_model->getVariableType(item->row())){ + SCP_string temp = itemText; + temp = temp.substr(0, NAME_LENGTH - 1); + + SCP_string ret = _model->setVariableStringValue(item->row(), temp); + if (ret.empty()){ + apply = true; + } else { + item->setText(ret.c_str()); + _currentVariableData = ret; + } + + // Variable is a number + } else { + SCP_string source = item->text().toUtf8().constData(); + SCP_string temp = _model->trimIntegerString(source); + + try { + int ret = _model->setVariableNumberValue(item->row(), std::stoi(temp)); + temp = ""; + sprintf(temp, "%i", ret); + item->setText(temp.c_str()); + } + catch (...) { + // that's not good.... + apply = true; + } + + // best we can do is to set this to temp, whether conversion fails or not. + _currentContainerItemCol2 = temp; + } + } + + if (apply) { + applyModel(); + } +} + + +void VariableDialog::onVariablesSelectionChanged() +{ + if (_applyingModel){ + applyModel(); + return; + } + + int row = getCurrentVariableRow(); + + if (row < 0){ + updateVariableOptions(false); + return; + } + + SCP_string newVariableName; + + auto item = ui->variablesTable->item(row, 0); + + if (item){ + newVariableName = item->text().toUtf8().constData(); + } + + item = ui->variablesTable->item(row, 1); + + if (item){ + _currentVariableData = item->text().toUtf8().constData(); + } + + if (newVariableName != _currentVariable){ + _currentVariable = newVariableName; + } + + applyModel(); +} + + +void VariableDialog::onContainersTableUpdated() +{ + if (_applyingModel){ + applyModel(); + return; + } + + int row = getCurrentContainerRow(); + + // just in case something is goofy, return + if (row < 0){ + applyModel(); + 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().toUtf8().constData(); + if (!newString.empty() && newString != "Add Container ..."){ + _model->addContainer(newString); + _currentContainer = newString; + applyModel(); + } + } + else { + applyModel(); + } + + return; + + // are they editing an existing container name? + } else if (ui->containersTable->item(row, 0)){ + SCP_string newName = ui->containersTable->item(row,0)->text().toUtf8().constData(); + + // 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(); + } +} + +void VariableDialog::onContainersSelectionChanged() +{ + if (_applyingModel){ + applyModel(); + return; + } + + int row = getCurrentContainerRow(); + + if (row < 0) { + updateContainerOptions(false); + return; + } + + // guaranteed not to be null, since getCurrentContainerRow already checked. + _currentContainer = ui->containersTable->item(row, 0)->text().toUtf8().constData(); + applyModel(); +} + +void VariableDialog::onContainerContentsTableUpdated() +{ + if (_applyingModel){ + applyModel(); + 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){ + + SCP_string newString; + + if (ui->containerContentsTable->item(row, 0)) { + newString = ui->containerContentsTable->item(row, 0)->text().toUtf8().constData(); + + if (!newString.empty() && newString != "Add item ..."){ + + if (_model->getContainerListOrMap(containerRow)) { + _model->addListItem(containerRow, newString); + } else { + _model->addMapItem(containerRow, newString, ""); + } + + _currentContainerItemCol1 = newString; + _currentContainerItemCol2 = ""; + + applyModel(); + return; + } + + } + + if (ui->containerContentsTable->item(row, 1)) { + newString = ui->containerContentsTable->item(row, 1)->text().toUtf8().constData(); + + if (!newString.empty() && newString != "Add item ..."){ + + // This should not be a list container. + if (_model->getContainerListOrMap(containerRow)) { + applyModel(); + return; + } + + auto ret = _model->addMapItem(containerRow, "", newString); + + _currentContainerItemCol1 = ret.first; + _currentContainerItemCol2 = ret.second; + + 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().toUtf8().constData(); + + if (_model->getContainerListOrMap(containerRow)){ + + 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); + applyModel(); + return; + } + + } else if (newText != _currentContainerItemCol1){ + _model->changeMapItemKey(containerRow, row, newText); + applyModel(); + 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().toUtf8().constData(); + + 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() +{ + if (_applyingModel){ + applyModel(); + return; + } + + int row = getCurrentContainerItemRow(); + + if (row < 0){ + applyModel(); + return; + } + + auto item = ui->containerContentsTable->item(row, 0); + SCP_string newContainerItemName; + + if (!item){ + applyModel(); + return; + } + + newContainerItemName = item->text().toUtf8().constData(); + item = ui->containerContentsTable->item(row, 1); + SCP_string newContainerDataText = (item) ? item->text().toUtf8().constData() : ""; + + if (newContainerItemName != _currentContainerItemCol1 || _currentContainerItemCol2 != newContainerDataText){ + _currentContainerItemCol1 = newContainerItemName; + _currentContainerItemCol2 = newContainerDataText; + applyModel(); + } +} + +void VariableDialog::onAddVariableButtonPressed() +{ + auto ret = _model->addNewVariable(); + _currentVariable = ret; + applyModel(); +} + +void VariableDialog::onCopyVariableButtonPressed() +{ + if (_currentVariable.empty()){ + applyModel(); + return; + } + + int currentRow = getCurrentVariableRow(); + + if (currentRow < 0){ + applyModel(); + return; + } + + auto ret = _model->copyVariable(currentRow); + _currentVariable = ret; + applyModel(); +} + +void VariableDialog::onDeleteVariableButtonPressed() +{ + if (_currentVariable.empty()){ + applyModel(); + return; + } + + int currentRow = getCurrentVariableRow(); + + if (currentRow < 0){ + applyModel(); + return; + } + + // Because of the text update we'll need, this needs an applyModel, whether it fails or not. + SCP_string btn_text = ui->deleteVariableButton->text().toUtf8().constData(); + if (btn_text == "Restore") { + _model->removeVariable(currentRow, false); + applyModel(); + } else { + _model->removeVariable(currentRow, true); + applyModel(); + } +} + +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 + _model->setVariableType(currentRow, true); + applyModel(); +} + +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 + _model->setVariableType(currentRow, false); + applyModel(); +} + +void VariableDialog::onDoNotSaveVariableRadioSelected() +{ + int currentRow = getCurrentVariableRow(); + + if (currentRow < 0 || !ui->doNotSaveVariableRadio->isChecked()){ + applyModel(); + return; + } + + int ret = _model->setVariableOnMissionCloseOrCompleteFlag(currentRow, 0); + + if (ret != 0){ + applyModel(); + } else { + ui->saveVariableOnMissionCompletedRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setChecked(false); + ui->setVariableAsEternalcheckbox->setEnabled(false); + } +} + +void VariableDialog::onSaveVariableOnMissionCompleteRadioSelected() +{ + int row = getCurrentVariableRow(); + + if (row < 0 || !ui->saveVariableOnMissionCompletedRadio->isChecked()){ + applyModel(); + return; + } + + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(row, 1); + + if (ret != 1){ + applyModel(); + } else { + ui->doNotSaveVariableRadio->setChecked(false); + ui->saveVariableOnMissionCloseRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); + } +} + +void VariableDialog::onSaveVariableOnMissionCloseRadioSelected() +{ + int row = getCurrentVariableRow(); + + if (row < 0 || !ui->saveVariableOnMissionCloseRadio->isChecked()){ + applyModel(); + return; + } + + auto ret = _model->setVariableOnMissionCloseOrCompleteFlag(row, 2); + + // out of sync because we did not get the expected return value. + if (ret != 2){ + applyModel(); + } else { + ui->doNotSaveVariableRadio->setChecked(false); + ui->saveVariableOnMissionCompletedRadio->setChecked(false); + + ui->setVariableAsEternalcheckbox->setEnabled(true); + ui->setVariableAsEternalcheckbox->setChecked(_model->getVariableEternalFlag(row)); + } +} + +void VariableDialog::onSaveVariableAsEternalCheckboxClicked() +{ + int row = getCurrentVariableRow(); + + if (row < 0){ + return; + } + + // 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() +{ + int row = getCurrentVariableRow(); + + if (row < 0){ + return; + } + + // 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() +{ + 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(); + +} + +void VariableDialog::onCopyContainerButtonPressed() +{ + int row = getCurrentContainerRow(); + + if (row < 0 ){ + return; + } + + // This will always need an apply model update, whether it succeeds or fails. + _model->copyContainer(row); + applyModel(); +} + +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. + SCP_string btn_text = ui->deleteContainerButton->text().toUtf8().constData(); + if (btn_text == "Restore"){ + _model->removeContainer(row, false); + } else { + _model->removeContainer(row, true); + } + + applyModel(); +} + +void VariableDialog::onSetContainerAsMapRadioSelected() +{ + // to avoid visual weirdness, make it false. + ui->setContainerAsListRadio->setChecked(false); + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerListOrMap(row, false); + applyModel(); +} + +void VariableDialog::onSetContainerAsListRadioSelected() +{ + // to avoid visual weirdness, make it false. + ui->setContainerAsMapRadio->setChecked(false); + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerListOrMap(row, true); + applyModel(); +} + + +void VariableDialog::onSetContainerAsStringRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerValueType(row, true); + applyModel(); +} + +void VariableDialog::onSetContainerAsNumberRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerValueType(row, false); + applyModel(); +} + +void VariableDialog::onSetContainerKeyAsStringRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerKeyType(row, true); + applyModel(); +} + + +void VariableDialog::onSetContainerKeyAsNumberRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + _model->setContainerKeyType(row, false); + applyModel(); +} + +void VariableDialog::onDoNotSaveContainerRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 0) != 0){ + applyModel(); + } else { + ui->doNotSaveContainerRadio->setChecked(true); + ui->saveContainerOnMissionCloseRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + + ui->setContainerAsEternalCheckbox->setChecked(false); + ui->setContainerAsEternalCheckbox->setEnabled(false); + } +} + +void VariableDialog::onSaveContainerOnMissionCompletedRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 1) != 1) + applyModel(); + else { + ui->doNotSaveContainerRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(false); + ui->saveContainerOnMissionCompletedRadio->setChecked(true); + + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); + } +} + +void VariableDialog::onSaveContainerOnMissionCloseRadioSelected() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + if (_model->setContainerOnMissionCloseOrCompleteFlag(row, 2) != 2) + applyModel(); + else { + ui->doNotSaveContainerRadio->setChecked(false); + ui->saveContainerOnMissionCloseRadio->setChecked(true); + ui->saveContainerOnMissionCompletedRadio->setChecked(false); + + ui->setContainerAsEternalCheckbox->setEnabled(true); + ui->setContainerAsEternalCheckbox->setChecked(_model->getContainerEternalFlag(row)); + } +} + +void VariableDialog::onNetworkContainerCheckboxClicked() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + if (ui->networkContainerCheckbox->isChecked() != _model->setContainerNetworkStatus(row, ui->networkContainerCheckbox->isChecked())){ + applyModel(); + } +} + +void VariableDialog::onSetContainerAsEternalCheckboxClicked() +{ + int row = getCurrentContainerRow(); + + if (row < 0){ + applyModel(); + return; + } + + if (ui->setContainerAsEternalCheckbox->isChecked() != _model->setContainerEternalFlag(row, ui->setContainerAsEternalCheckbox->isChecked())){ + applyModel(); + } +} + +void VariableDialog::onAddContainerItemButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + if (_model->getContainerListOrMap(containerRow)) { + _model->addListItem(containerRow); + } else { + _model->addMapItem(containerRow); + } + + applyModel(); +} + +void VariableDialog::onCopyContainerItemButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + int itemRow = getCurrentContainerItemRow(); + + if (itemRow < 0){ + applyModel(); + return; + } + + if (_model->getContainerListOrMap(containerRow)) { + _model->copyListItem(containerRow, itemRow); + } else { + _model->copyMapItem(containerRow, itemRow); + } + + applyModel(); +} + +void VariableDialog::onDeleteContainerItemButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + int itemRow = getCurrentContainerItemRow(); + + if (itemRow < 0){ + applyModel(); + return; + } + + if (_model->getContainerListOrMap(containerRow)) { + _model->removeListItem(containerRow, itemRow); + } else { + _model->removeMapItem(containerRow, itemRow); + } + + applyModel(); +} + +void VariableDialog::onShiftItemUpButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + int itemRow = getCurrentContainerItemRow(); + + // item row being 0 is bad here since we're shifting up. + if (itemRow < 1){ + applyModel(); + return; + } + + _model->shiftListItemUp(containerRow, itemRow); + applyModel(); +} + +void VariableDialog::onShiftItemDownButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + int itemRow = getCurrentContainerItemRow(); + + if (itemRow < 0){ + applyModel(); + return; + } + + _model->shiftListItemDown(containerRow, itemRow); + applyModel(); +} + +void VariableDialog::onSwapKeysAndValuesButtonPressed() +{ + int containerRow = getCurrentContainerRow(); + + if (containerRow < 0){ + applyModel(); + return; + } + + _model->swapKeyAndValues(containerRow); + applyModel(); +} + +void VariableDialog::onSelectFormatComboboxSelectionChanged() +{ + _model->setTextMode(ui->selectFormatCombobox->currentIndex()); + applyModel(); +} + +VariableDialog::~VariableDialog(){}; // NOLINT + + +void VariableDialog::applyModel() +{ + if (_applyingModel) { + return; + } + + _applyingModel = true; + + auto variables = _model->getVariableValues(); + 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)){ + 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 { + auto item = new QTableWidgetItem(variables[x][0].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->variablesTable->setItem(x, 0, item); + } + + // check if this is the current variable. This keeps us selecting the correct variable even when + // 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)){ + 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 { + auto item = new QTableWidgetItem(variables[x][1].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + 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)->setFlags(ui->variablesTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); + } else { + 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); + } + } + + // set the Add variable row + if (ui->variablesTable->item(x, 0)){ + ui->variablesTable->item(x, 0)->setText("Add Variable ..."); + } else { + auto item = new QTableWidgetItem("Add Variable ..."); + ui->variablesTable->setItem(x, 0, item); + } + + if (ui->variablesTable->item(x, 1)){ + ui->variablesTable->item(x, 1)->setFlags(ui->variablesTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->item(x, 1)->setText(""); + } else { + auto item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->setItem(x, 1, item); + } + + if (ui->variablesTable->item(x, 2)){ + ui->variablesTable->item(x, 2)->setFlags(ui->variablesTable->item(x, 2)->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->item(x, 2)->setText(""); + } else { + auto item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->variablesTable->setItem(x, 2, item); + } + + if (_currentVariable.empty() || selectedRow < 0){ + 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().toUtf8().constData(); + } + } + + updateVariableOptions(safeToAlter); + + auto containers = _model->getContainerNames(); + ui->containersTable->setRowCount(static_cast(containers.size() + 1)); + selectedRow = -1; + + 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 { + auto item = new QTableWidgetItem(containers[x][0].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containersTable->setItem(x, 0, item); + } + + // check if this is the current variable. + if (selectedRow < 0 && containers[x][0] == _currentContainer){ + selectedRow = x; + } + + if (ui->containersTable->item(x, 1)){ + ui->containersTable->item(x, 1)->setText(containers[x][1].c_str()); + } else { + 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 { + auto item = new QTableWidgetItem(containers[x][2].c_str()); + ui->containersTable->setItem(x, 2, item); + } + } + + // do we need to switch the delete button to a restore button? + 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. + 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); + } + + } else { + ui->deleteContainerButton->setText("Delete"); + } + + // set the Add container row + if (ui->containersTable->item(x, 0)){ + ui->containersTable->item(x, 0)->setText("Add Container ..."); + } else { + auto item = new QTableWidgetItem("Add Container ..."); + 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 { + auto 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 { + auto item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + 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().toUtf8().constData(); + 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(safeToAlterContainer); + + _applyingModel = false; +}; + +void VariableDialog::updateVariableOptions(bool safeToAlter) +{ + int row = getCurrentVariableRow(); + + 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); + ui->saveVariableOnMissionCompletedRadio->setEnabled(false); + ui->saveVariableOnMissionCloseRadio->setEnabled(false); + ui->setVariableAsEternalcheckbox->setEnabled(false); + ui->networkVariableCheckbox->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + ui->shiftItemDownButton->setEnabled(false); + return; + } + + // options that are always safe + ui->copyVariableButton->setEnabled(true); + ui->doNotSaveVariableRadio->setEnabled(true); + ui->saveVariableOnMissionCompletedRadio->setEnabled(true); + ui->saveVariableOnMissionCloseRadio->setEnabled(true); + ui->networkVariableCheckbox->setEnabled(true); + + // 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 + 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? + 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. + 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); + } + + } else { + ui->deleteVariableButton->setText("Delete"); + } + + int ret = _model->getVariableOnMissionCloseOrCompleteFlag(row); + + if (ret == 0){ + ui->doNotSaveVariableRadio->setChecked(true); + ui->saveVariableOnMissionCompletedRadio->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)); +} + +void VariableDialog::updateContainerOptions(bool safeToAlter) +{ + int row = getCurrentContainerRow(); + + 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); + ui->setContainerKeyAsNumberRadio->setEnabled(false); + ui->doNotSaveContainerRadio->setEnabled(false); + ui->saveContainerOnMissionCompletedRadio->setEnabled(false); + ui->saveContainerOnMissionCloseRadio->setEnabled(false); + ui->setContainerAsEternalCheckbox->setEnabled(false); + ui->setContainerAsMapRadio->setEnabled(false); + ui->setContainerAsListRadio->setEnabled(false); + 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("")); + + + // 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->shiftItemDownButton->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + ui->containerContentsTable->clearSelection(); + ui->containerContentsTable->setRowCount(0); + + + } else { + // options that should always be turned on + ui->copyContainerButton->setEnabled(true); + ui->doNotSaveContainerRadio->setEnabled(true); + ui->saveContainerOnMissionCompletedRadio->setEnabled(true); + ui->saveContainerOnMissionCloseRadio->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); + } else { + ui->setContainerAsStringRadio->setChecked(false); + ui->setContainerAsNumberRadio->setChecked(true); + } + + if (_model->getContainerListOrMap(row)){ + 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("")); + + updateContainerDataOptions(true, safeToAlter); + + } else { + ui->setContainerAsListRadio->setChecked(false); + ui->setContainerAsMapRadio->setChecked(true); + + // Enable Key Controls + ui->setContainerKeyAsStringRadio->setEnabled(safeToAlter); + ui->setContainerKeyAsNumberRadio->setEnabled(safeToAlter); + + // 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")); + ui->containerContentsTable->setHorizontalHeaderItem(1, new QTableWidgetItem("Value")); + updateContainerDataOptions(false, safeToAlter); + } + + ui->networkContainerCheckbox->setChecked(_model->getContainerNetworkStatus(row)); + + int ret = _model->getContainerOnMissionCloseOrCompleteFlag(row); + + if (ret == 0){ + ui->doNotSaveContainerRadio->setChecked(true); + ui->saveContainerOnMissionCompletedRadio->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)); + } + + } +} + +void VariableDialog::updateContainerDataOptions(bool list, bool safeToAlter) +{ + int row = getCurrentContainerRow(); + + // Just in case, 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); + ui->shiftItemDownButton->setEnabled(false); + ui->shiftItemUpButton->setEnabled(false); + ui->swapKeysAndValuesButton->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->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 + if (_model->getContainerValueType(row)){ + auto& strings = _model->getStringValues(row); + int containerItemsRow = -1; + ui->containerContentsTable->setRowCount(static_cast(strings.size()) + 1); + + + 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 { + auto item = new QTableWidgetItem(strings[x].c_str()); + ui->containerContentsTable->setItem(x, 0, item); + } + + // set selected and enable shifting functions + if (containerItemsRow < 0 && strings[x] == _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(strings.size())){ + ui->shiftItemUpButton->setEnabled(false); + } + + if (x <= -1 || x >= static_cast(strings.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(""); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); + } else { + auto 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); + int containerItemsRow = -1; + ui->containerContentsTable->setRowCount(static_cast(numbers.size()) + 1); + + 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 { + auto item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); + 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(""); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() & ~Qt::ItemIsEditable); + } else { + auto 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 { + auto 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 { + auto item = new QTableWidgetItem(""); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } + + // 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")); + + // Enable shift up and down buttons are off in Map mode, because order makes no difference + ui->shiftItemUpButton->setEnabled(false); + 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 || _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); + + int x; + // string valued map. + if (_model->getContainerValueType(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); + + 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()); + } else { + auto item = new QTableWidgetItem(keys[x].c_str()); + ui->containerContentsTable->setItem(x, 0, 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 { + auto item = new QTableWidgetItem(strings[x].c_str()); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } + } + } + + // number valued map + } else { + auto& numbers = _model->getNumberValues(row); + ui->containerContentsTable->setRowCount(static_cast(keys.size()) + 1); + + 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()); + } else { + auto item = new QTableWidgetItem(keys[x].c_str()); + ui->containerContentsTable->setItem(x, 0, 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 { + auto item = new QTableWidgetItem(std::to_string(numbers[x]).c_str()); + 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 { + auto 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, 1)->flags() | Qt::ItemIsEditable); + } else { + auto 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("Add item ..."); + ui->containerContentsTable->item(x, 1)->setFlags(ui->containerContentsTable->item(x, 1)->flags() | Qt::ItemIsEditable); + } else { + auto item = new QTableWidgetItem("Add item ..."); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->containerContentsTable->setItem(x, 1, item); + } + } +} + +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) { + SCP_string var = item->text().toUtf8().constData(); + if (item && item->column() == 0 && var != "Add Variable ...") { + 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) { + SCP_string var = item->text().toUtf8().constData(); + if (item && item->column() == 0 && var != "Add Container ...") { + return item->row(); + } + } + + return -1; +} + +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. + for (const auto& item : items) { + SCP_string var = item->text().toUtf8().constData(); + if (item && ((item->column() == 0 && var != "Add item ...") || (item->column() == 1 && var != "Add item ..."))) { + return item->row(); + } + } + + return -1; +} + +void VariableDialog::checkValidModel() +{ + if (ui->OkCancelButtons->button(QDialogButtonBox::Ok)->hasFocus() && _model->checkValidModel()){ + accept(); + } +} + +} // namespace dialogs + diff --git a/qtfred/src/ui/dialogs/VariableDialog.h b/qtfred/src/ui/dialogs/VariableDialog.h new file mode 100644 index 00000000000..837efc4b65c --- /dev/null +++ b/qtfred/src/ui/dialogs/VariableDialog.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include + +namespace fso::fred::dialogs { + +namespace Ui { +class VariableEditorDialog; +} + +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; + + // basically UpdateUI, but called when there is an inconsistency between model and UI + void applyModel(); + void checkValidModel(); + + // Helper functions for this + void updateVariableOptions(bool safeToAlter); + void updateContainerOptions(bool safeToAlter); + void updateContainerDataOptions(bool list, bool safeToAlter); + + void onVariablesTableUpdated(); + void onVariablesSelectionChanged(); + void onContainersTableUpdated(); + void onContainersSelectionChanged(); + void onContainerContentsTableUpdated(); + void onContainerContentsSelectionChanged(); + void onAddVariableButtonPressed(); + void onDeleteVariableButtonPressed(); + void onCopyVariableButtonPressed(); + void onSetVariableAsStringRadioSelected(); + void onSetVariableAsNumberRadioSelected(); + void onDoNotSaveVariableRadioSelected(); + void onSaveVariableOnMissionCompleteRadioSelected(); + void onSaveVariableOnMissionCloseRadioSelected(); + void onSaveVariableAsEternalCheckboxClicked(); + void onNetworkVariableCheckboxClicked(); + + void onAddContainerButtonPressed(); + void onDeleteContainerButtonPressed(); + void onCopyContainerButtonPressed(); + void onSetContainerAsMapRadioSelected(); + void onSetContainerAsListRadioSelected(); + void onSetContainerAsStringRadioSelected(); + void onSetContainerAsNumberRadioSelected(); + void onSetContainerKeyAsStringRadioSelected(); + void onSetContainerKeyAsNumberRadioSelected(); + void onDoNotSaveContainerRadioSelected(); + void onSaveContainerOnMissionCloseRadioSelected(); + void onSaveContainerOnMissionCompletedRadioSelected(); + void onNetworkContainerCheckboxClicked(); + void onSetContainerAsEternalCheckboxClicked(); + void onAddContainerItemButtonPressed(); + void onCopyContainerItemButtonPressed(); + void onDeleteContainerItemButtonPressed(); + void onShiftItemUpButtonPressed(); + void onShiftItemDownButtonPressed(); + void onSwapKeysAndValuesButtonPressed(); + void onSelectFormatComboboxSelectionChanged(); + + int getCurrentVariableRow(); + int getCurrentContainerRow(); + int getCurrentContainerItemRow(); + + bool _applyingModel = false; + SCP_string _currentVariable; + SCP_string _currentVariableData; + SCP_string _currentContainer; + SCP_string _currentContainerItemCol1; + SCP_string _currentContainerItemCol2; + + void reject() override + { + 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(); + } + } +}; + + + + + +} // namespace 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/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/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/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/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/src/ui/widgets/FlagList.cpp b/qtfred/src/ui/widgets/FlagList.cpp new file mode 100644 index 00000000000..8ba216d917f --- /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 auto checked = flags[i].second; + + auto* item = new QStandardItem(name); + item->setCheckable(true); + item->setCheckState(Qt::CheckState(checked)); + 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 auto checked = item->checkState(); + + 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 Qt::CheckState checked = it->checkState(); + 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..e681a8e5f4f --- /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, int 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: // NOLINT(readability-redundant-access-specifiers) + 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 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/src/ui/widgets/bankTree.cpp b/qtfred/src/ui/widgets/bankTree.cpp new file mode 100644 index 00000000000..f2d28dd22ee --- /dev/null +++ b/qtfred/src/ui/widgets/bankTree.cpp @@ -0,0 +1,103 @@ +#include "bankTree.h" +namespace fso::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().empty()) { + 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 fso::fred \ 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..81268d173cf --- /dev/null +++ b/qtfred/src/ui/widgets/bankTree.h @@ -0,0 +1,25 @@ +#pragma once +#include "ui/dialogs/ShipEditor/BankModel.h" + +#include + +#include +#include +#include +#include +#include +namespace fso::fred { +class bankTree : public QTreeView { + Q_OBJECT + public: + bankTree(QWidget*); + void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; + int getTypeSelected() const; + + protected: + void dragEnterEvent(QDragEnterEvent*) override; + void dropEvent(QDropEvent* event) override; + void dragMoveEvent(QDragMoveEvent*) override; + int typeSelected = -1; +}; +} // namespace fso::fred \ No newline at end of file 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/src/ui/widgets/weaponList.cpp b/qtfred/src/ui/widgets/weaponList.cpp new file mode 100644 index 00000000000..78f40afd018 --- /dev/null +++ b/qtfred/src/ui/widgets/weaponList.cpp @@ -0,0 +1,102 @@ +#include "weaponList.h" + +namespace fso::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; + } + auto drag = new QDrag(this); + QModelIndexList idxs; + idxs.append(idx); + QMimeData* mimeData = model()->mimeData(idxs); + auto 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); + 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 +{ + 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; + return out; + } + if (role == Qt::UserRole) { + const int 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 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 new file mode 100644 index 00000000000..ed15b120798 --- /dev/null +++ b/qtfred/src/ui/widgets/weaponList.h @@ -0,0 +1,38 @@ +#pragma once +#include + +#include +#include +#include +#include +#include +#include +namespace fso::fred { +struct WeaponItem { + WeaponItem(const int id, QString name); + const QString name; + const int id; +}; +class WeaponModel : public QAbstractListModel { + Q_OBJECT + public: + WeaponModel(int type); + ~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; + QVector weapons; +}; +class weaponList : public QListView { + Q_OBJECT + public: + weaponList(QWidget* parent); + + protected: + void mousePressEvent(QMouseEvent* event) override; + void mouseMoveEvent(QMouseEvent* event) override; + QPoint dragStartPosition; + + private: +}; +} // namespace fso::fred \ No newline at end of file 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 - - - - + - + diff --git a/qtfred/ui/BackgroundEditor.ui b/qtfred/ui/BackgroundEditor.ui index bd06bef4caa..7303acef3d6 100644 --- a/qtfred/ui/BackgroundEditor.ui +++ b/qtfred/ui/BackgroundEditor.ui @@ -6,56 +6,107 @@ 0 0 - 639 - 681 + 1041 + 862 Background Editor - + - + - - - - - - - - Import... - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - + + + + 150 + 0 + + + + + + + + Add + + + + + + + Remove + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Import... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Swap With + + + + + + + + 150 + 0 + + + + + + + - + Bitmaps - + - + - + - + @@ -64,17 +115,11 @@ - - - Qt::Horizontal - - - - 40 - 20 - + + + Change - + @@ -88,174 +133,124 @@ - + - + - - - + + + - Heading + Pitch - - - - - - - Pitch + + + + true + + + 16777215 - + Bank - - - - - - - - - Qt::Horizontal + + + true - - - 40 - 20 - + + 16777215 - + - - - - Qt::Horizontal - - - - 40 - 20 - + + + + Heading - + - - - - Qt::Horizontal + + + + true - - - 40 - 20 - + + 16777215 - + - + + + Scale (x/y) + + + Qt::AlignCenter + + + + + - - - Qt::Horizontal - - - - 40 - 20 - + + + 16777215.000000000000000 - - - - - - Scale (x/y) + + 0.100000000000000 - - - Qt::Horizontal + + + 16777215.000000000000000 - - - 40 - 20 - + + 0.100000000000000 - + - - - - - - - - + + + # divisions (x/y) + + + Qt::AlignCenter + + - - - - - Qt::Horizontal - - - - 40 - 20 - - - - + - - - # divisions (x/y) + + + 16777215 - - - Qt::Horizontal + + + 16777215 - - - 40 - 20 - - - - - - - - - - - - - + @@ -265,618 +260,780 @@ - + - Nebula + Suns - + - + - - - Full Nebula - - + - - - + + + - Range + Add - - - - - + + - Pattern + Change - - - - - + + - Lightning storm + Delete - - - + + + + - - - Toggle ship trails - - + - + - + - Fog Near + Pitch - + + + true + + + 16777215 + + - + - Fog Far Multiplier + Heading - + + + true + + + 16777215 + + + + + + + Scale + + + + + + + + 75 + 0 + + + + 16777215.000000000000000 + + + 0.100000000000000 + + - - - - - - Poofs - - - - - - - PoofGreen01 - - - - - - - PoofGreen02 - - - - - - - PoofRed01 - - - - - - - PoofRed02 - - - - - - - PoofPurp01 - - - - - - - PoofPurp02 - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + + + - - - Old Nebula + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Save sun/bitmap angles to mission in correct format - - - - - - - Pattern - - - - - - - - - - Color - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - - - Pitch - - - - - - - - - - Bank - - - - - - - - - - Heading - - - - - - - - - + + + + Qt::Horizontal + + + + 40 + 20 + + + + - + + + Qt::Horizontal + + + + + - - - - - Qt::Horizontal - - - - 40 - 20 - - - - + - - - Swap With + + + Nebula - - - - - - - - - - - suns - - - - - - - + - + - + - Add + Full Nebula - - - Qt::Horizontal + + + + + Range + + + + + + + Pattern + + + + + + + + + + Lightning storm + + + + + + + + + + 16777215 + + + 3000 + + + + + + + + + Toggle ship trails - - - 40 - 20 - + + + + + + + + Fog Near Multiplier + + + + + + + Fog Far Multiplier + + + + + + + 16777215.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + 16777215.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + + + Display background bitmaps in nebula - + - + - Delete + Override Nebula Fog Palette + + + + + + + QFrame::Box + + + + + + + + + + 0 + 0 + + + + R + + + + + + + 255 + + + + + + + G + + + + + + + 255 + + + + + + + B + + + + + + + 255 + + + + + - - - - - + + + + + Poofs + + + + + + + QAbstractItemView::MultiSelection + + + + + + + + + + + Old Nebula + + - - - + + + - Heading + Pattern - - + + + + + + Color + + + + + + + + + + - + Pitch - + Bank + + + + Heading + + + - + + + + 100 + 0 + + + + true + + + 16777215 + + - - - - - - Qt::Horizontal - - + + - 40 - 20 + 100 + 0 - - - - - - Qt::Horizontal + + true + + + 16777215 - + + + + + - 40 - 20 + 100 + 0 - + + true + + + 16777215 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Ambient light + + + + - + + + G: 0 + + + + + + + 1 + + + 255 + Qt::Horizontal - - - 40 - 20 - - - + - - + + - Scale + B: 0 - - + + + + + 0 + 0 + + + + R: 0 + + + false + + - - + + + + 1 + + + 255 + Qt::Horizontal - - - 40 - 20 - + + + + + + QFrame::Box + + + + + + + + + + 1 + + + 255 - + + Qt::Horizontal + + - - - - - - - - Ambient light - - - - - - - - Qt::Horizontal - - - - - - - Red: 0 - - - - - - - Qt::Horizontal - - - - - - - Green: 0 - - - - - - - Qt::Horizontal - - - - - - - Blue: 0 - - - - - - - - - - - - Skybox - - - - - - - - Skybox Model - - - - - - - - - - - - - - Pitch - - - + + + + + + Skybox + + - + + + + + Skybox Model + + + + + + + - - - Bank - - + + + + + Pitch + + + + + + + true + + + 16777215 + + + + + + + Bank + + + + + + + true + + + 16777215 + + + + + + + Heading + + + + + + + true + + + 16777215 + + + + - + + + + + No lighting + + + + + + + Transparent + + + + + + + Force clamp + + + + + + + No z-buffer + + + + + + + No cull + + + + + + + No glow maps + + + + + + + + + + + Misc + + - + - Heading + Number of stars: - - - - - - - - - - No lighting - - - - - - - Transparent - - - - - - - Force clamp - - - - - - - No z-buffer - - - - - - - No cull + + + 2000 - - - - - - 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 + + + + + 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 + + + + + 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 - - - - + + + + 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/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() diff --git a/qtfred/ui/FredView.ui b/qtfred/ui/FredView.ui index b578a59a5e2..af613091eef 100644 --- a/qtfred/ui/FredView.ui +++ b/qtfred/ui/FredView.ui @@ -20,7 +20,7 @@ 0 0 926 - 21 + 22 @@ -168,20 +168,28 @@ - - - - - - + + + + + + + + + + + - - + + + + + + - @@ -231,9 +239,12 @@ + + + @@ -1124,9 +1135,9 @@ Shift+Y - + - &Events + Mission &Events Shift+E @@ -1439,6 +1450,14 @@ Ctrl+Shift+D + + + true + + + Always save Display Names + + true @@ -1489,14 +1508,56 @@ &Empty - + - Mission Objectives + Mission Goals Shift+G + + + &Variables + + + Open Variables and Containers Editor + + + Shift+V + + + + + Jump &Nodes + + + Shift+J + + + + + Mission Cutscenes + + + Shift+X + + + + + Music Player + + + + + Volumetric Nebula + + + + + Calculate Relative Coordinates + + 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 + + + + + + + + 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 + + + + + + + + + + + + + diff --git a/qtfred/ui/LoadoutDialog.ui b/qtfred/ui/LoadoutDialog.ui index a80e96f859c..318c72af474 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/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
+
+
+ + +
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 - - - - + 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 - - - - +
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/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 + + + + + + + + + + + + + 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 - - - - +
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 - - - - +
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 + + + + + + + + + + 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 - - - - + 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 + + + + + + + + + + + + diff --git a/qtfred/ui/ShipEditorDialog.ui b/qtfred/ui/ShipEditorDialog.ui index 919134dcf3f..f9eef784f4c 100644 --- a/qtfred/ui/ShipEditorDialog.ui +++ b/qtfred/ui/ShipEditorDialog.ui @@ -7,7 +7,7 @@ 0 0 468 - 688 + 754 @@ -37,100 +37,100 @@ - - + + - Team + Display Name - + + + + Alt Name + + + + <html><head/><body><p>Sets the Ship's AI</p></body></html> - - - - <html><head/><body><p>Type to add Ship's Cargo or select previous</p></body></html> - - - true + + + + Ship 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> - - - - <html><head/><body><p>Sets the ship's name</p></body></html> - - - - + Cargo - + AI Class - - + + - Ship Name + Team - - + + - <html><head/><body><p>Sets ship's callsign (Replaces name in messages)</p></body></html> + <html><head/><body><p>Type to add Ship's Cargo or select previous</p></body></html> true - - + + - Alt Name + Callsign - - - - Ship Class + + + + <html><head/><body><p>Sets the ship's name</p></body></html> - - - - Callsign + + + + <html><head/><body><p>Sets ship's callsign (Replaces name in messages)</p></body></html> + + + true @@ -144,6 +144,20 @@ + + + + <html><head/><body><p>Sets the ship's class</p></body></html> + + + + + + + Ship Class + + + @@ -162,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 + + + @@ -169,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 % + + + @@ -226,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 - - - @@ -250,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 % - - + + @@ -864,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 + + + + + + diff --git a/qtfred/ui/ShipWeaponsDialog.ui b/qtfred/ui/ShipWeaponsDialog.ui new file mode 100644 index 00000000000..0d6329025fa --- /dev/null +++ b/qtfred/ui/ShipWeaponsDialog.ui @@ -0,0 +1,206 @@ + + + fso::fred::dialogs::ShipWeaponsDialog + + + + 0 + 0 + 764 + 622 + + + + Weapons Editor + + + true + + + + + + Mode + + + + + + Primary + + + + + + + Secondary + + + + + + + Tertiary + + + + + + + + + + + + Weapons + + + + + + QAbstractScrollArea::AdjustIgnored + + + QAbstractItemView::NoEditTriggers + + + true + + + QAbstractItemView::DragOnly + + + + + + + View Table + + + + + + + + + + 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
+
+
+ + + + +
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 + + + + + + + + + + diff --git a/qtfred/ui/VariableDialog.ui b/qtfred/ui/VariableDialog.ui new file mode 100644 index 00000000000..6551fb54423 --- /dev/null +++ b/qtfred/ui/VariableDialog.ui @@ -0,0 +1,659 @@ + + + fso::fred::dialogs::VariableEditorDialog + + + Qt::WindowModal + + + + 0 + 0 + 899 + 711 + + + + + 899 + 711 + + + + + 899 + 711 + + + + Variables Editor + + + + + + + + + 734 + 643 + + + + Variables + + + + + 580 + 120 + 81 + 101 + + + + Type Options + + + + + 10 + 20 + 71 + 80 + + + + + + + String + + + + + + + Number + + + + + + + + + + 10 + 30 + 551 + 191 + + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + Qt::DotLine + + + false + + + false + + + false + + + + + + 680 + 20 + 191 + 201 + + + + Persistence Options + + + + + 10 + 20 + 181 + 181 + + + + + + + No Persistence + + + + + + + Save on Mission Completed + + + + + + + Save on Mission Close + + + + + + + Eternal + + + + + + + Network Variable + + + + + + + + + + 580 + 30 + 82 + 86 + + + + + + + Add + + + + + + + Copy + + + + + + + Delete + + + + + + + + + 0 + 230 + 881 + 431 + + + + + 0 + 380 + + + + Containers + + + + + 10 + 210 + 661 + 211 + + + + Container Contents + + + + + 10 + 30 + 511 + 171 + + + + Qt::ScrollBarAlwaysOn + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + Qt::DotLine + + + false + + + false + + + + + + 550 + 30 + 106 + 175 + + + + + + + Add + + + + + + + Copy + + + + + + + Shift Up + + + + + + + Shift Down + + + + + + + Swap Key/Value + + + + + + + + 8 + + + + Delete + + + + + + + + + + 580 + 30 + 82 + 85 + + + + + + + Add + + + + + + + Copy + + + + + + + + 8 + + + + Delete + + + + + + + + + 680 + 20 + 191 + 181 + + + + Persistence Options + + + + + 10 + 20 + 181 + 161 + + + + + + + No Persistence + + + + + + + Save on Mission Completed + + + + + + + Save on Mission Close + + + + + + + Eternal + + + + + + + Network Variable + + + + + + + + + + 680 + 210 + 191 + 211 + + + + Type Options + + + + + 10 + 20 + 181 + 181 + + + + + + + Container Type + + + + + + + + + List + + + + + + + Map + + + + + + + + + Key Type + + + + + + + + + String + + + + + + + Number + + + + + + + + + Data Type + + + + + + + + + String + + + + + + + Number + + + + + + + + + + + + 10 + 30 + 551 + 171 + + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + Qt::DotLine + + + false + + + false + + + false + + + + + + 580 + 140 + 81 + 20 + + + + Type Format + + + Qt::AlignCenter + + + + + + 570 + 160 + 101 + 24 + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + true + + + + + + + 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 + + + + 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 - - + + + 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 + + + + + + + + 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 + + + + + + + + + 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
+
+
+ + + + +