From a42d49be9f1de33e66ec7825a2d3465c3d2e21b1 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Fri, 27 Feb 2026 13:14:37 -0600 Subject: [PATCH 1/3] set-guard-range --- code/ai/aicode.cpp | 26 ++++++++++++++++++------- code/parse/sexp.cpp | 47 +++++++++++++++++++++++++++++++++++++++++++++ code/parse/sexp.h | 1 + code/ship/ship.cpp | 1 + code/ship/ship.h | 1 + 5 files changed, 69 insertions(+), 7 deletions(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index bd48755ba89..b954b17a5e0 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -321,6 +321,18 @@ void ai_cleanup_dock_mode_objective(object *objp); // the "autopilot" object *Autopilot_flight_leader = NULL; +static inline float ai_guard_threshold(const object* guarded_objp, float threshold) +{ + if (guarded_objp != nullptr && guarded_objp->type == OBJ_SHIP && guarded_objp->instance >= 0) { + const float configured = Ships[guarded_objp->instance].max_guard_radius; + if (configured > 0.0f) { + return configured; + } + } + + return threshold; +} + /** * Sets the timestamp used to tell is it is a good time for this team to rearm. * Ends a 'bad rearm time' @@ -5115,9 +5127,9 @@ int maybe_resume_previous_mode(object *objp, ai_info *aip) // If guarding ship is far away from guardee and enemy is far away from guardee, // then stop chasing and resume guarding. - if (dist > (MAX_GUARD_DIST + guard_objp->radius) * 6) { + if (dist > ai_guard_threshold(guard_objp, 6.0f)) { if ((En_objp != NULL) && (En_objp->type == OBJ_SHIP)) { - if (vm_vec_dist_quick(&guard_objp->pos, &En_objp->pos) > (MAX_GUARD_DIST + guard_objp->radius) * 6) { + if (vm_vec_dist_quick(&guard_objp->pos, &En_objp->pos) > ai_guard_threshold(guard_objp, 6.0f)) { Assert(aip->previous_mode == AIM_GUARD); aip->mode = aip->previous_mode; aip->submode = AIS_GUARD_PATROL; @@ -10515,7 +10527,7 @@ int ai_guard_find_nearby_bomb(object *guarding_objp, object *guarded_objp) dist = vm_vec_dist_quick(&bomb_objp->pos, &guarded_objp->pos); - if (dist < (MAX_GUARD_DIST + guarded_objp->radius)*3) { + if (dist < ai_guard_threshold(guarded_objp, 3.0f)) { dist_to_guarding_obj = vm_vec_dist_quick(&bomb_objp->pos, &guarding_objp->pos); if ( dist_to_guarding_obj < closest_dist_to_guarding_obj ) { closest_dist_to_guarding_obj = dist_to_guarding_obj; @@ -10559,11 +10571,11 @@ void ai_guard_find_nearby_ship(object *guarding_objp, object *guarded_objp) if (Ship_info[eshipp->ship_info_index].class_type >= 0 && (Ship_types[Ship_info[eshipp->ship_info_index].class_type].flags[Ship::Type_Info_Flags::AI_guards_attack])) { dist = vm_vec_dist_quick(&enemy_objp->pos, &guarded_objp->pos); - if (dist < (MAX_GUARD_DIST + guarded_objp->radius)*3) + if (dist < ai_guard_threshold(guarded_objp, 3.0f)) { guard_object_was_hit(guarding_objp, enemy_objp); - } - else if ((dist < 3000.0f) && (Ai_info[eshipp->ai_index].target_objnum == guarding_aip->guard_objnum)) + } else if ((dist < ai_guard_threshold(guarded_objp, 3000.0f)) && + (Ai_info[eshipp->ai_index].target_objnum == guarding_aip->guard_objnum)) { guard_object_was_hit(guarding_objp, enemy_objp); } @@ -10590,7 +10602,7 @@ void ai_guard_find_nearby_asteroid(object *guarding_objp, object *guarded_objp) if ( asteroid_objp->type == OBJ_ASTEROID ) { // Attack asteroid if near guarded ship dist = vm_vec_dist_quick(&asteroid_objp->pos, &guarded_objp->pos); - if ( dist < (MAX_GUARD_DIST + guarded_objp->radius)*2) { + if (dist < ai_guard_threshold(guarded_objp, 2.0f)) { dist_to_self = vm_vec_dist_quick(&asteroid_objp->pos, &guarding_objp->pos); if ( OBJ_INDEX(guarded_objp) == asteroid_collide_objnum(asteroid_objp) ) { if( dist_to_self < closest_danger_asteroid_dist ) { diff --git a/code/parse/sexp.cpp b/code/parse/sexp.cpp index 5d43c16f66b..33945758664 100644 --- a/code/parse/sexp.cpp +++ b/code/parse/sexp.cpp @@ -524,6 +524,7 @@ SCP_vector Operators = { { "ship-no-guardian", OP_SHIP_NO_GUARDIAN, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, { "ship-guardian-threshold", OP_SHIP_GUARDIAN_THRESHOLD, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, { "ship-subsys-guardian-threshold", OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD, 3, INT_MAX, SEXP_ACTION_OPERATOR, }, + { "set-guard-range", OP_SET_GUARD_RANGE, 2, INT_MAX, SEXP_ACTION_OPERATOR, }, // MjnMixael { "self-destruct", OP_SELF_DESTRUCT, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, { "destroy-instantly", OP_DESTROY_INSTANTLY, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, // Admiral MS { "destroy-instantly-with-debris", OP_DESTROY_INSTANTLY_WITH_DEBRIS, 1, INT_MAX, SEXP_ACTION_OPERATOR, }, // Asteroth @@ -19467,6 +19468,29 @@ void sexp_ship_guardian_threshold(int node) } } +// MjnMixael +void sexp_set_guard_range(int node) +{ + int range, n = node; + bool is_nan, is_nan_forever; + + range = eval_num(n, is_nan, is_nan_forever); + if (is_nan || is_nan_forever) + return; + n = CDR(n); + + for (; n != -1; n = CDR(n)) { + auto ship_entry = eval_ship(n); + if (!ship_entry || !ship_entry->has_shipp()) { + continue; + } + + // Intentionally no lower bound validation beyond disabling at <= 0. + // Mission authors may choose very small positive values for highly restrictive escort behavior. + ship_entry->shipp()->max_guard_radius = (range > 0) ? static_cast(range) : -1.0f; + } +} + // Goober5000 void sexp_ship_subsys_guardian_threshold(int node) { @@ -28860,6 +28884,11 @@ int eval_sexp(int cur_node, int referenced_node) sexp_val = SEXP_TRUE; break; + case OP_SET_GUARD_RANGE: + sexp_set_guard_range(node); + sexp_val = SEXP_TRUE; + break; + case OP_SHIP_SUBSYS_TARGETABLE: sexp_ship_deal_with_subsystem_flag(cur_node, node, Ship::Subsystem_Flags::Untargetable, true, false); sexp_val = SEXP_TRUE; @@ -31597,6 +31626,7 @@ int query_operator_return_type(int op) case OP_SHIP_NO_GUARDIAN: case OP_SHIP_GUARDIAN_THRESHOLD: case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD: + case OP_SET_GUARD_RANGE: case OP_SHIP_VANISH: case OP_PROP_VANISH: case OP_DESTROY_INSTANTLY: @@ -32308,6 +32338,12 @@ int query_operator_argument_type(int op, int argnum) else return OPF_SUBSYS_OR_GENERIC; + case OP_SET_GUARD_RANGE: + if (argnum == 0) + return OPF_NUMBER; + else + return OPF_SHIP; + case OP_SHIP_SUBSYS_TARGETABLE: case OP_SHIP_SUBSYS_UNTARGETABLE: if (argnum == 0) @@ -36881,6 +36917,7 @@ int get_category(int op_id) case OP_JUMP_NODE_HIDE_JUMPNODE: case OP_SHIP_GUARDIAN_THRESHOLD: case OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD: + case OP_SET_GUARD_RANGE: case OP_SET_SKYBOX_MODEL: case OP_SHIP_CREATE: case OP_PROP_CREATE: @@ -37238,6 +37275,7 @@ int get_subcategory(int op_id) case OP_ALTER_SHIP_FLAG: case OP_ALTER_WING_FLAG: + case OP_SET_GUARD_RANGE: case OP_PROTECT_SHIP: case OP_UNPROTECT_SHIP: case OP_BEAM_PROTECT_SHIP: @@ -40433,6 +40471,15 @@ SCP_vector Sexp_help = { "\t2:\tShip housing the subsystem(s) (ships must be in-mission).\r\n" "\t3+:\tSubsystems to make unkillable." }, + // MjnMixael + { OP_SET_GUARD_RANGE, "set-guard-range\r\n" + "\tSets the max range in meters at which any ships guarding this ship will engage with threats.\r\n" + "This range will override the default dynamic range behavior for ships obeying a guard order.\r\n" + "If the value is <= 0, regular dynamic guard range behavior will resume. Positive values are used as is with no size validation based on ship class.\r\n\r\n" + "Takes 2 or more arguments...\r\n" + "\t1:\tGuard range cap in meters (<= 0 disables cap).\r\n" + "\t2+:\tShip(s) to apply the cap to (ships must be in-mission)." }, + // Goober5000 { OP_SHIP_STEALTHY, "ship-stealthy\r\n" "\tCauses the ships listed in this sexpression to become stealth ships (i.e. invisible to radar).\r\n\r\n" diff --git a/code/parse/sexp.h b/code/parse/sexp.h index 45461e9e234..e03296f89ca 100644 --- a/code/parse/sexp.h +++ b/code/parse/sexp.h @@ -695,6 +695,7 @@ enum : int { OP_JUMP_NODE_HIDE_JUMPNODE, // WMC OP_SHIP_GUARDIAN_THRESHOLD, // Goober5000 OP_SHIP_SUBSYS_GUARDIAN_THRESHOLD, // Goober5000 + OP_SET_GUARD_RANGE, //MjnMixael OP_SET_SKYBOX_MODEL, // taylor OP_SHIP_CREATE, OP_PROP_CREATE, // MjnMixael diff --git a/code/ship/ship.cpp b/code/ship/ship.cpp index 6220b927477..a94233c9b5e 100644 --- a/code/ship/ship.cpp +++ b/code/ship/ship.cpp @@ -7190,6 +7190,7 @@ void ship::clear() ship_max_hull_strength = 0.0f; ship_guardian_threshold = 0; + max_guard_radius = -1.0f; ship_name[0] = 0; display_name.clear(); diff --git a/code/ship/ship.h b/code/ship/ship.h index 87b79f48211..be031e53956 100644 --- a/code/ship/ship.h +++ b/code/ship/ship.h @@ -619,6 +619,7 @@ class ship float max_weapon_regen_per_second; // wookieejedi - make this a ship object variable int ship_guardian_threshold; // Goober5000 - now also determines whether ship is guardian'd + float max_guard_radius; // Optional clamp for guard engagement/resume ranges; <= 0 means unused char ship_name[NAME_LENGTH]; From 9924bbc4f3db824674880d4930b60f892143cdb4 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Sun, 15 Mar 2026 22:41:22 -0500 Subject: [PATCH 2/3] make sure old path remains unchanged --- code/ai/aicode.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index b954b17a5e0..3b9317b4948 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -330,7 +330,11 @@ static inline float ai_guard_threshold(const object* guarded_objp, float thresho } } - return threshold; + if (guarded_objp != nullptr) { + return (MAX_GUARD_DIST + guarded_objp->radius) * threshold; + } + + return threshold * MAX_GUARD_DIST; } /** From 5d5dca7148730ebc4189649ef0a5735b04fc5fc0 Mon Sep 17 00:00:00 2001 From: Mike Nelson Date: Mon, 16 Mar 2026 14:24:00 -0500 Subject: [PATCH 3/3] address feedback again --- code/ai/aicode.cpp | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/code/ai/aicode.cpp b/code/ai/aicode.cpp index 3b9317b4948..ca085b91fb0 100644 --- a/code/ai/aicode.cpp +++ b/code/ai/aicode.cpp @@ -330,11 +330,7 @@ static inline float ai_guard_threshold(const object* guarded_objp, float thresho } } - if (guarded_objp != nullptr) { - return (MAX_GUARD_DIST + guarded_objp->radius) * threshold; - } - - return threshold * MAX_GUARD_DIST; + return threshold; } /** @@ -5131,9 +5127,10 @@ int maybe_resume_previous_mode(object *objp, ai_info *aip) // If guarding ship is far away from guardee and enemy is far away from guardee, // then stop chasing and resume guarding. - if (dist > ai_guard_threshold(guard_objp, 6.0f)) { + if (dist > ai_guard_threshold(guard_objp, (MAX_GUARD_DIST + guard_objp->radius) * 6)) + { if ((En_objp != NULL) && (En_objp->type == OBJ_SHIP)) { - if (vm_vec_dist_quick(&guard_objp->pos, &En_objp->pos) > ai_guard_threshold(guard_objp, 6.0f)) { + if (vm_vec_dist_quick(&guard_objp->pos, &En_objp->pos) > ai_guard_threshold(guard_objp, (MAX_GUARD_DIST + guard_objp->radius) * 6)) { Assert(aip->previous_mode == AIM_GUARD); aip->mode = aip->previous_mode; aip->submode = AIS_GUARD_PATROL; @@ -10531,7 +10528,7 @@ int ai_guard_find_nearby_bomb(object *guarding_objp, object *guarded_objp) dist = vm_vec_dist_quick(&bomb_objp->pos, &guarded_objp->pos); - if (dist < ai_guard_threshold(guarded_objp, 3.0f)) { + if (dist < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 3)) { dist_to_guarding_obj = vm_vec_dist_quick(&bomb_objp->pos, &guarding_objp->pos); if ( dist_to_guarding_obj < closest_dist_to_guarding_obj ) { closest_dist_to_guarding_obj = dist_to_guarding_obj; @@ -10575,7 +10572,7 @@ void ai_guard_find_nearby_ship(object *guarding_objp, object *guarded_objp) if (Ship_info[eshipp->ship_info_index].class_type >= 0 && (Ship_types[Ship_info[eshipp->ship_info_index].class_type].flags[Ship::Type_Info_Flags::AI_guards_attack])) { dist = vm_vec_dist_quick(&enemy_objp->pos, &guarded_objp->pos); - if (dist < ai_guard_threshold(guarded_objp, 3.0f)) + if (dist < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 3)) { guard_object_was_hit(guarding_objp, enemy_objp); } else if ((dist < ai_guard_threshold(guarded_objp, 3000.0f)) && @@ -10606,7 +10603,7 @@ void ai_guard_find_nearby_asteroid(object *guarding_objp, object *guarded_objp) if ( asteroid_objp->type == OBJ_ASTEROID ) { // Attack asteroid if near guarded ship dist = vm_vec_dist_quick(&asteroid_objp->pos, &guarded_objp->pos); - if (dist < ai_guard_threshold(guarded_objp, 2.0f)) { + if (dist < ai_guard_threshold(guarded_objp, (MAX_GUARD_DIST + guarded_objp->radius) * 2)) { dist_to_self = vm_vec_dist_quick(&asteroid_objp->pos, &guarding_objp->pos); if ( OBJ_INDEX(guarded_objp) == asteroid_collide_objnum(asteroid_objp) ) { if( dist_to_self < closest_danger_asteroid_dist ) {