Files
Boutros Tawaifi 49464c56f6 Add an "On Ship Damage" script hook (#7894)
* Add an "On Ship Damage" script hook

Runs after resolved damage has been applied to a ship (lethal hits run before
the destruction callback). Hook variables: Target, Source, Attacker,
WeaponClass, WeaponSubtype, SourceKind (PROJECTILE, BEAM, SHOCKWAVE, COLLISION,
OTHER), DamageType, ShieldDamage, HullDamage, Quadrant, Ballistic, BeamWeapon
and LethalHit. Conditions can filter on the target ship. Nothing is computed
unless a script has registered the hook.

* Move the On Ship Damage hook call out of a lambda

ship_do_damage() now calls a file-level static function, ship_run_damage_hook(), that takes the values it needs as arguments instead of capturing the function state. Hook behaviour is unchanged.

* Report subsystem damage in the On Ship Damage hook

Adds SubsystemDamage, the total subsystem strength a damage application removed, so a hit absorbed by subsystems no longer looks like a small hull hit. The hook description now says it only runs where damage is applied, which excludes multiplayer clients.

* Address On Ship Damage review comments

- Remove WeaponClass, WeaponSubtype, Ballistic and BeamWeapon from the
  hook. Scripts read these from Source when it is a weapon.
- Add a "Weapon class" condition for the On Ship Damage hook. It matches
  only when the damage source is a weapon of that class; a weapon name
  that does not exist never matches.
- SubsystemDamage is now a table keyed by subsystem handle that lists only
  the subsystems that lost strength. The total is removed.
- Rename PROJECTILE to NON_BEAM_WEAPON.
- Classify SourceKind by damage source: COLLISION for ship, debris and
  asteroid collisions; EXPLOSION for ship-death and docked-ship blasts and
  asteroid blasts; WASH for engine wash; SELF for kills where the ship
  damages itself; OTHER for other ship-sourced damage.
- Reword the hook description.

* Key On Ship Damage SubsystemDamage by canonical name

Lua tables cannot use subsystem handles as keys in a usable way, so key
the SubsystemDamage table by each subsystem's CanonicalName instead.

* Tag ship-vs-prop damage as a collision and use in_bounds

The ship-vs-prop collision call didn't pass DamageSource::Collision, so
the On Ship Damage hook reported those hits as OTHER. Every other
collision path was already tagged.

Also use SCP_vector::in_bounds() for the Weapon_info and Damage_types
index checks in ship_run_damage_hook().
2026-11-06 18:41:29 -05:00

333 lines
16 KiB
C++

#include "hook_conditions.h"
#include <functional>
#include <utility>
#include "gamesequence/gamesequence.h"
#include "parse/sexp.h"
#include "ship/ship.h"
#include "weapon/weapon.h"
#include "io/key.h"
// ---- Hook Condition System Macro and Class defines ----
#define HOOK_CONDITIONS_START(name) const SCP_unordered_map<SCP_string, const std::unique_ptr<const ParseableCondition>> name::conditions = []() { \
SCP_unordered_map<SCP_string, const std::unique_ptr<const ParseableCondition>> build;
#define HOOK_CONDITIONS_END return build; \
}();
#define HOOK_CONDITION(conditionsClassName, conditionParseName, documentation, argument, argumentParse, argumentValid) \
build.emplace(conditionParseName, std::make_unique<ParseableConditionImpl<conditionsClassName, \
decltype(std::declval<conditionsClassName>().argument), decltype(argumentParse(std::declval<SCP_string>()))>> \
(documentation, &conditionsClassName::argument, argumentParse, argumentValid))
extern const char *Scan_code_text_english[];
namespace scripting {
template<typename conditions_t, typename operating_t, typename cache_t>
class EvaluatableConditionImpl;
template<typename conditions_t, typename operating_t, typename cache_t>
class ParseableConditionImpl : public ParseableCondition {
const operating_t conditions_t::* object;
std::function<cache_t(const SCP_string&)> cache;
std::function<bool(operating_t, const cache_t&)> evaluate;
template<typename _conditions_t, typename _operating_t, typename _cache_t> friend class EvaluatableConditionImpl;
public:
std::unique_ptr<EvaluatableCondition> parse(const SCP_string& input) const override {
return std::make_unique<EvaluatableConditionImpl<conditions_t, operating_t, cache_t>>(*this, input);
}
ParseableConditionImpl(SCP_string documentation_, const operating_t conditions_t::* object_, std::function<cache_t(const SCP_string&)> cache_, std::function<bool(operating_t, const cache_t&)> evaluate_) :
ParseableCondition(std::move(documentation_)), object(object_), cache(std::move(cache_)), evaluate(std::move(evaluate_)) { }
};
template<typename conditions_t, typename operating_t, typename cache_t>
class EvaluatableConditionImpl : public EvaluatableCondition {
const ParseableConditionImpl<conditions_t, operating_t, cache_t>& condition;
cache_t cached;
public:
EvaluatableConditionImpl(const ParseableConditionImpl<conditions_t, operating_t, cache_t>& _condition, const SCP_string& input) : condition(_condition), cached(condition.cache(input)) { }
bool evaluate(const std::any& conditionContext) const override {
const conditions_t& conditions = std::any_cast<conditions_t>(conditionContext);
return condition.evaluate(conditions.*(condition.object), cached);
}
};
namespace hooks {
// ---- Hook Condition System Utility and Parsing methods ----
static bool conditionCompareShip(const ship* shipp, const SCP_string& value) {
return shipp != nullptr && stricmp(shipp->ship_name, value.c_str()) == 0;
}
static bool conditionCompareShipType(const ship* shipp, const int& value) {
return shipp != nullptr && Ship_info[shipp->ship_info_index].class_type == value;
}
static bool conditionCompareShipClass(const ship* shipp, const int& value) {
return shipp != nullptr && shipp->ship_info_index == value;
}
static bool conditionCompareWeaponClass(const weapon* wep, const int& value) {
return wep != nullptr && wep->weapon_info_index == value;
}
static bool conditionIsObjecttype(const object* objp, const int& value) {
return objp != nullptr && objp->type == value;
}
template<typename fnc_t, typename value_t>
static bool conditionObjectIsShipDo(fnc_t fnc, const object* objp, const value_t& value) {
if (objp != nullptr && objp->type == OBJ_SHIP) {
return fnc(&Ships[objp->instance], value);
}
return false;
}
template<typename fnc_t, typename value_t>
static bool conditionObjectIsWeaponDo(fnc_t fnc, const object* objp, const value_t& value) {
if (objp != nullptr && objp->type == OBJ_WEAPON) {
return fnc(&Weapons[objp->instance], value);
}
return false;
}
static int conditionCompareRawControl(int keypress, const int& cached_key) {
//For reasons only known to Volition, LCtrl and RCtrl are differentiated in name, while Alt and Shift are not.
//As only the first of these identical names will be matched, replace the R versions with the L versions
int key_down = keypress & KEY_MASK;
switch(key_down) {
case KEY_RALT:
key_down = KEY_LALT;
break;
case KEY_RSHIFT:
key_down = KEY_LSHIFT;
break;
default:
break;
}
return cached_key == key_down;
}
static SCP_string conditionParseString(const SCP_string& name) {
return name;
}
static int conditionParseActionId(const SCP_string& name) {
for (int i = 0; i < (int)Control_config.size(); i++) {
if (lcase_equal(Control_config[i].text, name))
return i;
}
return -1;
}
static int conditionParseShipType(const SCP_string& name) {
return ship_type_name_lookup(name.c_str());
}
static int conditionParseShipClass(const SCP_string& name) {
return ship_info_lookup(name.c_str());
}
static int conditionParseWeaponClass(const SCP_string& name) {
return weapon_info_lookup(name.c_str());
}
static int conditionParseObjectType(const SCP_string& name) {
for (int i = 0; i < MAX_OBJECT_TYPES; i++) {
if (stricmp(Object_type_names[i], name.c_str()) == 0)
return i;
}
return -1;
}
static int conditionParseRawControl(const SCP_string& name) {
for (int key = 0; key < NUM_KEYS; key++){
if (stricmp(Scan_code_text_english[key], name.c_str()) == 0)
return key & KEY_MASK;
}
return -1;
}
// ---- Hook Condition Helpers ----
#define HOOK_CONDITION_SHIPP(classname, prefix, documentationAddendum, shipp) \
HOOK_CONDITION(classname, prefix "Ship", "Specifies the name of the ship " documentationAddendum, shipp, conditionParseString, conditionCompareShip); \
HOOK_CONDITION(classname, prefix "Ship class", "Specifies the class of the ship " documentationAddendum, shipp, conditionParseShipClass, conditionCompareShipClass); \
HOOK_CONDITION(classname, prefix "Ship type", "Specifies the type of the ship " documentationAddendum, shipp, conditionParseShipType, conditionCompareShipType);
#define HOOK_CONDITION_SHIP_OBJP(classname, prefix, documentationAddendum, objp_) \
HOOK_CONDITION(classname, prefix "Ship", "Specifies the name of the ship " documentationAddendum, objp_, conditionParseString, [](const object* objp, const SCP_string& shipname) -> bool { \
return conditionObjectIsShipDo(&conditionCompareShip, objp, shipname); \
}); \
HOOK_CONDITION(classname, prefix "Ship class", "Specifies the class of the ship " documentationAddendum, objp_, conditionParseShipClass, [](const object* objp, const int& shipclass) -> bool { \
return conditionObjectIsShipDo(&conditionCompareShipClass, objp, shipclass); \
}); \
HOOK_CONDITION(classname, prefix "Ship type", "Specifies the type of the ship " documentationAddendum, objp_, conditionParseShipType, [](const object* objp, const int& shiptype) -> bool { \
return conditionObjectIsShipDo(&conditionCompareShipType, objp, shiptype); \
});
// ---- Hook Conditions ----
HOOK_CONDITIONS_START(ControlActionConditions)
HOOK_CONDITION(ControlActionConditions, "Action", "Specifies the action triggered by a keypress.", action_index, conditionParseActionId, [](int target, const int& key) -> bool {
if (gameseq_get_depth() < 0)
return false;
return key == target;
});
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(ShipSourceConditions)
HOOK_CONDITION_SHIPP(ShipSourceConditions, "", "that was the source of the event.", source_shipp);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(CollisionConditions)
//These conditions with "must be one of the ships" instead of "must be both ships" is unlike the previous behaviour of PR #4231, but is closer to the original behaviour, where a specific ship of the two must match
HOOK_CONDITION(CollisionConditions, "Ship", "Specifies the name of the ship which was part of the collision. At least one ship must be part of the collision and match.", participating_objects, conditionParseString, [](CollisionConditions::ParticipatingObjects po, const SCP_string& shipname) -> bool {
if(conditionObjectIsShipDo(&conditionCompareShip, po.objp_a, shipname))
return true;
if (conditionObjectIsShipDo(&conditionCompareShip, po.objp_b, shipname))
return true;
return false;
});
HOOK_CONDITION(CollisionConditions, "Ship class", "Specifies the class of the ship which was part of the collision. At least one ship must be part of the collision and match.", participating_objects, conditionParseShipClass, [](CollisionConditions::ParticipatingObjects po, const int& shipclass) -> bool {
if (conditionObjectIsShipDo(&conditionCompareShipClass, po.objp_a, shipclass))
return true;
if (conditionObjectIsShipDo(&conditionCompareShipClass, po.objp_b, shipclass))
return true;
return false;
});
HOOK_CONDITION(CollisionConditions, "Ship type", "Specifies the type of the ship which was part of the collision. At least one ship must be part of the collision and match.", participating_objects, conditionParseShipType, [](CollisionConditions::ParticipatingObjects po, const int& shiptype) -> bool {
if (conditionObjectIsShipDo(&conditionCompareShipType, po.objp_a, shiptype))
return true;
if (conditionObjectIsShipDo(&conditionCompareShipType, po.objp_b, shiptype))
return true;
return false;
});
HOOK_CONDITION(CollisionConditions, "Weapon class", "Specifies the name of the weapon class which was part of the collision. At least one weapon must be part of the collision and match.", participating_objects, conditionParseWeaponClass, [](CollisionConditions::ParticipatingObjects po, const int& weaponclass) -> bool {
if (conditionObjectIsWeaponDo(&conditionCompareWeaponClass, po.objp_a, weaponclass))
return true;
if (conditionObjectIsWeaponDo(&conditionCompareWeaponClass, po.objp_b, weaponclass))
return true;
return false;
});
HOOK_CONDITION(CollisionConditions, "Object type", "Specifies the type of the object which was part of the collision. At least one object must match.", participating_objects, conditionParseObjectType, [](CollisionConditions::ParticipatingObjects po, const int& objecttype) -> bool {
if (conditionIsObjecttype(po.objp_a, objecttype))
return true;
if (conditionIsObjecttype(po.objp_b, objecttype))
return true;
return false;
});
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(ShipDeathConditions)
HOOK_CONDITION_SHIPP(ShipDeathConditions, "", "that died.", dying_shipp);
HOOK_CONDITIONS_END
// Damage with no weapon has weapon_info_index -1, and so does a weapon name that does not exist, so a named weapon
// class must also be a real weapon to match.
static bool conditionDamageWeaponClassMatches(int damage_weapon_index, const int& weapon_index)
{
return damage_weapon_index >= 0 && damage_weapon_index == weapon_index;
}
HOOK_CONDITIONS_START(ShipDamageConditions)
HOOK_CONDITION_SHIPP(ShipDamageConditions, "Target ", "that took damage.", target_shipp);
HOOK_CONDITION(ShipDamageConditions, "Weapon class", "Specifies the class of the weapon that caused the damage. For a shockwave, this is the weapon that created it.", weapon_info_index, conditionParseWeaponClass, conditionDamageWeaponClassMatches);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(SubsystemDeathConditions)
HOOK_CONDITION_SHIPP(SubsystemDeathConditions, "", "whose subsystem got destroyed.", affected_shipp);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(ShipDepartConditions)
HOOK_CONDITION_SHIPP(ShipDepartConditions, "", "that departed.", leaving_shipp);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(WeaponDeathConditions)
HOOK_CONDITION(WeaponDeathConditions, "Weapon class", "Specifies the class of the weapon that died.", dying_wep, conditionParseWeaponClass, conditionCompareWeaponClass);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(WeaponProximityTriggeredConditions)
HOOK_CONDITION(WeaponProximityTriggeredConditions, "Weapon class", "Specifies the class of the weapon that was triggered.", triggered_wep, conditionParseWeaponClass, conditionCompareWeaponClass);
HOOK_CONDITION_SHIPP(WeaponProximityTriggeredConditions, "", "that triggered the weapon.", trigger_shipp);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(ObjectDeathConditions)
HOOK_CONDITION_SHIP_OBJP(ObjectDeathConditions, "", "that died.", dying_objp);
HOOK_CONDITION(ObjectDeathConditions, "Weapon class", "Specifies the class of the weapon that died.", dying_objp, conditionParseWeaponClass, [](const object* objp, const int& weaponclass) -> bool {
return conditionObjectIsWeaponDo(&conditionCompareWeaponClass, objp, weaponclass);
});
HOOK_CONDITION(ObjectDeathConditions, "Object type", "Specifies the type of the object that died.", dying_objp, conditionParseObjectType, conditionIsObjecttype);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(ShipArriveConditions)
HOOK_CONDITION_SHIPP(ShipArriveConditions, "", "that arrived.", spawned_shipp);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(WeaponCreatedConditions)
HOOK_CONDITION_SHIP_OBJP(WeaponCreatedConditions, "", "that fired the weapon.", parent_objp);
HOOK_CONDITION(WeaponCreatedConditions, "Object type", "Specifies the type of the object that is the parent of this weapon.", parent_objp, conditionParseObjectType, conditionIsObjecttype);
HOOK_CONDITION(WeaponCreatedConditions, "Weapon class", "Specifies the class of the weapon that was fired.", spawned_wep, conditionParseWeaponClass, conditionCompareWeaponClass);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(WeaponEquippedConditions)
HOOK_CONDITION_SHIPP(WeaponEquippedConditions, "", "that has the weapon equipped.", user_shipp);
HOOK_CONDITION(WeaponEquippedConditions, "Weapon class", "Specifies the class of the weapon that the ship needs to have equipped in at least one bank.", user_shipp, conditionParseWeaponClass, [](const ship* wielder, const int& weaponclass) -> bool {
for (int i = 0; i < MAX_SHIP_PRIMARY_BANKS; i++) {
if (wielder->weapons.primary_bank_weapons[i] >= 0 && wielder->weapons.primary_bank_weapons[i] == weaponclass)
return true;
}
for (int i = 0; i < MAX_SHIP_SECONDARY_BANKS; i++) {
if (wielder->weapons.secondary_bank_weapons[i] >= 0 && wielder->weapons.secondary_bank_weapons[i] == weaponclass)
return true;
}
return false;
});
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(WeaponUsedConditions)
HOOK_CONDITION_SHIPP(WeaponUsedConditions, "", "that fired the weapon.", user_shipp);
HOOK_CONDITION(WeaponUsedConditions, "Weapon class", "Specifies the class of the weapon that was fired.", weaponclasses, conditionParseWeaponClass, [](const SCP_vector<int>& weaponclass_list, const int& weaponclass) -> bool {
return std::count(weaponclass_list.cbegin(), weaponclass_list.cend(), weaponclass) > 0;
});
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(WeaponSelectedConditions)
HOOK_CONDITION_SHIPP(WeaponSelectedConditions, "", "that has selected the weapon.", user_shipp);
HOOK_CONDITION(WeaponSelectedConditions, "Weapon class", "Specifies the class of the weapon that was selected.", weaponclass, conditionParseWeaponClass, std::equal_to<int>());
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(WeaponDeselectedConditions)
HOOK_CONDITION_SHIPP(WeaponDeselectedConditions, "", "that has deselected the weapon.", user_shipp);
HOOK_CONDITION(WeaponDeselectedConditions, "Weapon class", "Specifies the class of the weapon that was deselected.", weaponclass_prev, conditionParseWeaponClass, std::equal_to<int>());
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(ObjectDrawConditions)
HOOK_CONDITION_SHIP_OBJP(ObjectDrawConditions, "", "that was drawn / drawn from.", drawn_from_objp);
HOOK_CONDITION(ObjectDrawConditions, "Weapon class", "Specifies the class of the weapon that was drawn / drawn from.", drawn_from_objp, conditionParseWeaponClass, [](const object* objp, const int& weaponclass) -> bool {
return conditionObjectIsWeaponDo(&conditionCompareWeaponClass, objp, weaponclass);
});
HOOK_CONDITION(ObjectDrawConditions, "Object type", "Specifies the type of the object that was drawn / drawn from.", drawn_from_objp, conditionParseObjectType, conditionIsObjecttype);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(KeyPressConditions)
HOOK_CONDITION(KeyPressConditions, "Raw KeyPress", "The key that is pressed, with no consideration for any modifier keys.", keycode, conditionParseRawControl, conditionCompareRawControl);
HOOK_CONDITIONS_END
HOOK_CONDITIONS_START(CommOrderConditions)
HOOK_CONDITION_SHIPP(CommOrderConditions, "", "that sent the order.", source);
HOOK_CONDITION(CommOrderConditions, "Object type", "Specifies the type of object that is the target of the order.", target, conditionParseObjectType, conditionIsObjecttype);
HOOK_CONDITION_SHIP_OBJP(CommOrderConditions, "Target ", "that is being targeted.", target);
HOOK_CONDITIONS_END
}
}