diff --git a/CHANGELOG.md b/CHANGELOG.md index 135cab2..f87150e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ CI собирает обе платформы и прикладывает бин **События** - `player_death` несёт `weapon` и `distance`. +- `player_hurt`/`player_hurt_post`/`player_trace_attack` несут `weapon` — + classname гранаты/C4 как отдельного инфликтора, иначе активное оружие + атакующего; позволяет писать per-weapon множитель урона в Lua. - `player_team_change`, `weapon_fire` — опрашиваются каждый кадр, ловят любой источник смены, включая сторонние моды. - `map_change`, `plugin_unload` — точки, где плагин снимает свои эффекты. diff --git a/scripts/docs/api/hook.py b/scripts/docs/api/hook.py index 37b6042..5630c1f 100644 --- a/scripts/docs/api/hook.py +++ b/scripts/docs/api/hook.py @@ -219,12 +219,14 @@ def event(name, brief, fields, cancel=None, example=None, extra=None, ('e.attacker', 'player \\| nil', '`nil` при падении и уроне мира'), ('e.damage', 'number', 'запись: сколько урона применить'), ('e.bits', 'number', 'маска типа урона: `DMG_FALL`, `DMG_BULLET`'), - ('e.hitgroup', 'number \\| nil', 'куда попали: `1` голова, `2` грудь, `3` живот, `4`/`5` руки, `6`/`7` ноги; `nil`, если урон не от попадания')], + ('e.hitgroup', 'number \\| nil', 'куда попали: `1` голова, `2` грудь, `3` живот, `4`/`5` руки, `6`/`7` ноги; `nil`, если урон не от попадания'), + ('e.weapon', 'string \\| nil', 'classname источника: граната/C4 своим именем, иначе оружие в руках атакующего; `nil` для падения и урона мира')], slug='player_hurt', cancel='`e:cancel()` гасит урон полностью: ни звука боли, ни брони, ни смерти. Цепочка на этом обрывается.', example=""" -hook.add("player:hurt", "myplugin.double", function(e) - e.damage = e.damage * 2 +hook.add("player:hurt", "myplugin.per_weapon", function(e) + local mult = ({ weapon_awp = 1.5, weapon_deagle = 1.2, hegrenade = 0.8 })[e.weapon] + if mult then e.damage = e.damage * mult end end) """, extra='''Единственное событие, которое меняет игру. Каждый следующий обработчик видит @@ -241,7 +243,8 @@ def event(name, brief, fields, cancel=None, example=None, extra=None, ('e.attacker', 'player \\| nil', '`nil` при падении и уроне мира'), ('e.damage', 'number', 'сколько урона применилось'), ('e.bits', 'number', 'маска типа урона'), - ('e.hitgroup', 'number \\| nil', 'куда попали: `1` голова, `2` грудь, `3` живот, `4`/`5` руки, `6`/`7` ноги; `nil`, если урон не от попадания')], + ('e.hitgroup', 'number \\| nil', 'куда попали: `1` голова, `2` грудь, `3` живот, `4`/`5` руки, `6`/`7` ноги; `nil`, если урон не от попадания'), + ('e.weapon', 'string \\| nil', 'то же, что в `player:hurt`')], slug='player_hurt_post', extra='`e.victim:health()` здесь уже актуальное. Менять нечего — событие неотменяемое.', regamedll=True), @@ -251,10 +254,11 @@ def event(name, brief, fields, cancel=None, example=None, extra=None, ('e.damage', 'number', 'запись: сырой урон этого попадания, до брони и множителей'), ('e.bits', 'number', 'маска типа урона'), ('e.hitgroup', 'number \\| nil', 'куда попали: `1` голова, `2` грудь, `3` живот, `4`/`5` руки, `6`/`7` ноги'), - ('e.x, e.y, e.z', 'number', 'точка попадания')], + ('e.x, e.y, e.z', 'number', 'точка попадания'), + ('e.weapon', 'string \\| nil', 'оружие в руках атакующего; `nil`, если распознать не удалось')], slug='player_trace_attack', cancel='`e:cancel()` (или `e.damage = 0`) убирает это попадание целиком: ни крови, ни вклада в итоговый урон, который дальше увидит `player:hurt`.', - extra='Название — по движковому хуку `CBasePlayer::TraceAttack` (тот же, что `RG_CBasePlayer_TraceAttack` в ReAPI). Дробовик даёт одно `player:trace_attack` на каждую долетевшую дробину и одно `player:hurt`/`weapon:fire` в сумме — здесь урон ещё не сложен.', + extra='Название — по движковому хуку `CBasePlayer::TraceAttack` (тот же, что `RG_CBasePlayer_TraceAttack` в ReAPI). Дробовик даёт одно `player:trace_attack` на каждую долетевшую дробину и одно `player:hurt`/`weapon:fire` в сумме — здесь урон ещё не сложен. В отличие от `player:hurt`, `e.weapon` тут всегда берётся из активного оружия атакующего — у попадания трассировкой нет отдельного инфликтора.', regamedll=True, see=[('player:hurt', 'player_hurt.md')]), event('player:heal', 'Игроку вот-вот дадут здоровье — аптечка, админ-команда; своей регенерации в CS нет.', @@ -278,7 +282,7 @@ def event(name, brief, fields, cancel=None, example=None, extra=None, """, extra='Суицид приходит как `e.killer.id == e.victim.id`. Вместе с `e.killer` в `nil` уходят `e.weapon` и `e.distance`.', regamedll=True, - notes=[('note', 'Граната и взрыв C4 приходят как оружие в руках убийцы, а не как\n`hegrenade`. Отличить снаряд можно по `e.bits` в `player:hurt`.')]), + notes=[('note', 'Граната и взрыв C4 приходят как оружие в руках убийцы, а не как\n`hegrenade`. Отличить снаряд точнее можно через `e.weapon` в `player:hurt` —\nтам для гранаты/C4 приходит classname самого снаряда (`hegrenade`, `grenade`,\n`c4`), а не оружие в руках.')]), event('player:team_change', 'Игрок сменил сторону.', [PLAYER, ('e.old_team', 'string', '`CT`, `T`, `SPEC`, `NONE`'), ('e.new_team', 'string', 'куда перешёл')], diff --git a/src/lua_events.cpp b/src/lua_events.cpp index ce17120..ff7dacf 100644 --- a/src/lua_events.cpp +++ b/src/lua_events.cpp @@ -821,9 +821,10 @@ void LuaEvents::fire_player_spawn(int id) } float LuaEvents::fire_player_hurt(int victim, int attacker, float damage, int bits, - int hitgroup) + int hitgroup, const char *weapon) { float result = damage; + std::string weapon_name = weapon ? weapon : ""; bool cancelled = run(CSLUA_EVENT_PLAYER_HURT, [=](lua_State *L) { @@ -833,6 +834,8 @@ float LuaEvents::fire_player_hurt(int victim, int attacker, float damage, int bi lua_pushinteger(L, bits); lua_setfield(L, -2, "bits"); set_hitgroup(L, hitgroup); + if (!weapon_name.empty()) + set_string(L, weapon_name, "weapon"); }, [&result](lua_State *L) { // Whatever the chain left in e.damage is what the game applies. @@ -849,8 +852,10 @@ float LuaEvents::fire_player_hurt(int victim, int attacker, float damage, int bi } void LuaEvents::fire_player_hurt_post(int victim, int attacker, float damage, int bits, - int hitgroup) + int hitgroup, const char *weapon) { + std::string weapon_name = weapon ? weapon : ""; + notify(CSLUA_EVENT_PLAYER_HURT_POST, [=](lua_State *L) { set_player(L, victim, "victim"); set_player_or_nil(L, attacker, "attacker"); @@ -858,6 +863,8 @@ void LuaEvents::fire_player_hurt_post(int victim, int attacker, float damage, in lua_pushinteger(L, bits); lua_setfield(L, -2, "bits"); set_hitgroup(L, hitgroup); + if (!weapon_name.empty()) + set_string(L, weapon_name, "weapon"); }); } @@ -1198,9 +1205,10 @@ void LuaEvents::fire_bomb_defuse_start(int player, bool defuser) } float LuaEvents::fire_player_trace_attack(int victim, int attacker, float damage, int bits, - int hitgroup, float x, float y, float z) + int hitgroup, float x, float y, float z, const char *weapon) { float result = damage; + std::string weapon_name = weapon ? weapon : ""; bool cancelled = run(CSLUA_EVENT_PLAYER_TRACE_ATTACK, [=](lua_State *L) { @@ -1213,6 +1221,8 @@ float LuaEvents::fire_player_trace_attack(int victim, int attacker, float damage set_number(L, x, "x"); set_number(L, y, "y"); set_number(L, z, "z"); + if (!weapon_name.empty()) + set_string(L, weapon_name, "weapon"); }, [&result](lua_State *L) { lua_getfield(L, -1, "damage"); diff --git a/src/lua_events.h b/src/lua_events.h index be4f3cc..d4f55f4 100644 --- a/src/lua_events.h +++ b/src/lua_events.h @@ -263,17 +263,21 @@ class LuaEvents // damage is by reference in TakeDamage. Returns the final damage: e.damage // after the chain, or 0 if a handler cancelled. hitgroup is -1 when the - // damage did not come from a hit (fall, world, gas). - float fire_player_hurt(int victim, int attacker, float damage, int bits, int hitgroup); + // damage did not come from a hit (fall, world, gas). weapon is the + // classname behind the hit - the inflictor (grenade, c4) if it is its own + // entity, otherwise the attacker's active item; NULL when neither applies. + float fire_player_hurt(int victim, int attacker, float damage, int bits, int hitgroup, + const char *weapon); // Post: the damage the game actually applied, for observers. - void fire_player_hurt_post(int victim, int attacker, float damage, int bits, int hitgroup); + void fire_player_hurt_post(int victim, int attacker, float damage, int bits, int hitgroup, + const char *weapon); // The exact hit before TakeDamage sums same-frame hits. damage is the raw // per-hit amount before armor/multipliers. x,y,z is the trace position. // Returns the (possibly changed) damage, 0 if cancelled. float fire_player_trace_attack(int victim, int attacker, float damage, int bits, - int hitgroup, float x, float y, float z); + int hitgroup, float x, float y, float z, const char *weapon); // TakeHealth. Returns the (possibly changed) amount, 0 if cancelled. float fire_player_heal(int player, float amount, int bits); diff --git a/src/regamedll.cpp b/src/regamedll.cpp index 72a7a4e..3b5bbd0 100644 --- a/src/regamedll.cpp +++ b/src/regamedll.cpp @@ -162,6 +162,33 @@ static int attacker_slot(entvars_t *pevAttacker) return cslua_valid_player_id(idx) ? idx : 0; } +// The classname behind a hit: the inflictor's own classname when it is a +// distinct entity (grenade, C4 - not the attacker itself), otherwise the +// attacker's active weapon. NULL for fall damage, world damage, and the like. +// Copies into buf since the source string lives inside the game's entity. +static const char *resolve_damage_weapon(entvars_t *pevInflictor, entvars_t *pevAttacker, + char *buf, size_t buf_size) +{ + const char *weapon = NULL; + + if (pevInflictor && pevInflictor != pevAttacker) + weapon = STRING(pevInflictor->classname); + + if ((!weapon || !*weapon) && pevAttacker) { + int aslot = attacker_slot(pevAttacker); + CBasePlayer *attacker = aslot ? cslua_player_entity(aslot) : NULL; + if (attacker && attacker->m_pActiveItem && attacker->m_pActiveItem->pev) + weapon = STRING(attacker->m_pActiveItem->pev->classname); + } + + if (!weapon || !*weapon) + return NULL; + + cslua_snprintf(buf, buf_size, "%s", weapon); + buf[buf_size - 1] = '\0'; + return buf; +} + // TraceAttack fires once per hit (per shotgun pellet) and only accumulates into // the multi-damage buffer. flDamage is the raw per-hit amount, before armor and // multipliers. Not by reference, so a changed value is forwarded into callNext. @@ -180,7 +207,12 @@ static void hook_trace_attack(IReGameHook_CBasePlayer_TraceAttack *chain, CBaseP float y = ptr ? ptr->vecEndPos.y : 0.0f; float z = ptr ? ptr->vecEndPos.z : 0.0f; - flDamage = g_events.fire_player_trace_attack(vslot, aslot, flDamage, bitsDamageType, hitgroup, x, y, z); + // TraceAttack has no separate inflictor - a hitscan trace always comes + // straight from the attacker's own weapon. + char weapon_buf[64]; + const char *weapon = resolve_damage_weapon(NULL, pevAttacker, weapon_buf, sizeof weapon_buf); + + flDamage = g_events.fire_player_trace_attack(vslot, aslot, flDamage, bitsDamageType, hitgroup, x, y, z, weapon); if (flDamage <= 0.0f) return; // fully blocked: no blood, no multidamage @@ -203,13 +235,16 @@ static BOOL hook_takedamage(IReGameHook_CBasePlayer_TakeDamage *chain, CBasePlay int hitgroup = (bitsDamageType & (DMG_BULLET | DMG_SLASH | DMG_CLUB)) ? victim->m_LastHitGroup : -1; - flDamage = g_events.fire_player_hurt(vslot, aslot, flDamage, bitsDamageType, hitgroup); + char weapon_buf[64]; + const char *weapon = resolve_damage_weapon(pevInflictor, pevAttacker, weapon_buf, sizeof weapon_buf); + + flDamage = g_events.fire_player_hurt(vslot, aslot, flDamage, bitsDamageType, hitgroup, weapon); if (flDamage <= 0.0f) return FALSE; // fully blocked: no pain sound, no armor loss BOOL result = chain->callNext(victim, pevInflictor, pevAttacker, flDamage, bitsDamageType); - g_events.fire_player_hurt_post(vslot, aslot, flDamage, bitsDamageType, hitgroup); + g_events.fire_player_hurt_post(vslot, aslot, flDamage, bitsDamageType, hitgroup, weapon); return result; }