From 60dc50780d8200428d0ea1999e4b23547cbe3d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=A4rtner?= Date: Thu, 25 May 2023 19:23:27 +0200 Subject: [PATCH 01/10] first try to add damage dispatcher, hook and handler for hooking into in minqlx plugins --- hooks.c | 38 ++++++++++++++++++++++++++++++++++++++ pyminqlx.h | 2 ++ python/minqlx/_events.py | 11 ++++++++++- python/minqlx/_handlers.py | 13 +++++++++++++ python_dispatchers.c | 13 +++++++++++++ python_embed.c | 11 +++++++++++ quake_common.h | 8 +++++++- 7 files changed, 94 insertions(+), 2 deletions(-) diff --git a/hooks.c b/hooks.c index 1db8e4b..f6d675b 100644 --- a/hooks.c +++ b/hooks.c @@ -223,6 +223,37 @@ void __cdecl My_G_StartKamikaze(gentity_t* ent) { if (client_id != -1) KamikazeExplodeDispatcher(client_id, is_used_on_demand); } + +void __cdecl My_G_Damage( + gentity_t* target, // entity that is being damaged + gentity_t* inflictor, // entity that is causing the damage + gentity_t* attacker, // entity that caused the inflictor to damage targ + vec3_t dir, // direction of the attack for knockback + vec3_t point, // point at which the damage is being inflicted, used for headshots + int damage, // amount of damage being inflicted + int dflags, // these flags are used to control how T_Damage works + int mod // means_of_death indicator + ) { + int target_id; + int attacker_id = -1; + + G_Damage(target, inflictor, attacker, dir, point, damage, dflags, mod); + + if (!target) { + return; + } + + if (!target->client) { + return; + } + + target_id = target->client->ps.clientNum; + + if attacker && attacker->client { + attacker_id = attacker->client->ps.clientNum; + + DamageDispatcher(target_id, attacker_id, damage, dflags, mod);) +} #endif // Hook static functions. Can be done before program even runs. @@ -342,6 +373,13 @@ void HookVm(void) { DebugPrint("ERROR: Failed to hook ClientSpawn: %d\n", res); failed = 1; } + count++; + + res = Hook((void*)G_Damage, My_G_Damage, (void*)&G_Damage); + if (res) { + DebugPrint("ERROR: Failed to hook G_Damage: %d\n", res); + failed = 1; + } count++; if (failed) { diff --git a/pyminqlx.h b/pyminqlx.h index ea8874f..493f1d9 100644 --- a/pyminqlx.h +++ b/pyminqlx.h @@ -61,6 +61,7 @@ extern PyObject* client_spawn_handler; extern PyObject* kamikaze_use_handler; extern PyObject* kamikaze_explode_handler; +extern PyObject* damage_handler; // Custom console command handler. These are commands added through Python that can be used // from the console or using RCON. @@ -91,5 +92,6 @@ void ClientSpawnDispatcher(int client_id); void KamikazeUseDispatcher(int client_id); void KamikazeExplodeDispatcher(int client_id, int is_used_on_demand); +void DamageDispatcher(int target_id, int attacker_id, int damage, int dflags, int mod); #endif /* PYMINQLX_H */ diff --git a/python/minqlx/_events.py b/python/minqlx/_events.py index 1e25443..ef572e0 100644 --- a/python/minqlx/_events.py +++ b/python/minqlx/_events.py @@ -30,7 +30,7 @@ class EventDispatcher: to hook into events by registering an event handler. """ - no_debug = ("frame", "set_configstring", "stats", "server_command", "death", "kill", "command", "console_print") + no_debug = ("frame", "set_configstring", "stats", "server_command", "death", "kill", "command", "console_print", "damage") need_zmq_stats_enabled = False def __init__(self): @@ -581,6 +581,14 @@ class KamikazeExplodeDispatcher(EventDispatcher): def dispatch(self, player, is_used_on_demand): return super().dispatch(player, is_used_on_demand) +class DamageDispatcher(EventDispatcher): + """Event that goes off when someone is inflicted with damage.""" + + name = "damage" + need_zmq_stats_enabled = False + + def dispatch(self, target, attacker, damage, dflags, mod): + return super().dispatch(target, attacker, damage, dflags, mod) EVENT_DISPATCHERS = EventDispatcherManager() EVENT_DISPATCHERS.add_dispatcher(ConsolePrintDispatcher) @@ -615,3 +623,4 @@ def dispatch(self, player, is_used_on_demand): EVENT_DISPATCHERS.add_dispatcher(KillDispatcher) EVENT_DISPATCHERS.add_dispatcher(DeathDispatcher) EVENT_DISPATCHERS.add_dispatcher(UserinfoDispatcher) +EVENT_DISPATCHERS.add_dispatcher(DamageDispatcher) diff --git a/python/minqlx/_handlers.py b/python/minqlx/_handlers.py index d915512..1d218db 100644 --- a/python/minqlx/_handlers.py +++ b/python/minqlx/_handlers.py @@ -432,6 +432,18 @@ def handle_kamikaze_explode(client_id, is_used_on_demand): minqlx.log_exception() return True +def handle_damage(target, attacker, damage, dflags, mod): + target_player = minqlx.Player(target) + attacker_player = minqlx.Player(attacker) if attacker is not None else None + # noinspection PyBroadException + try: + minqlx.EVENT_DISPATCHERS["damage"].dispatch( + target_player, attacker_player, damage, dflags, mod + ) + except: # noqa: E722 + minqlx.log_exception() + return True + def handle_console_print(text): """Called whenever the server prints something to the console and when rcon is used.""" try: @@ -510,3 +522,4 @@ def register_handlers(): minqlx.register_handler("kamikaze_use", handle_kamikaze_use) minqlx.register_handler("kamikaze_explode", handle_kamikaze_explode) + minqlx.register_handler("damage", handle_damage) diff --git a/python_dispatchers.c b/python_dispatchers.c index 30ed72d..74fb25e 100644 --- a/python_dispatchers.c +++ b/python_dispatchers.c @@ -294,3 +294,16 @@ void KamikazeExplodeDispatcher(int client_id, int is_used_on_demand) { PyGILState_Release(gstate); } + +void DamageDispatcher(int target_id, int attacker_id, int damage, int dflags, int mod) { + if (!damage_handler) + return; // No registered handler + + PyGILState_STATE gstate = PyGILState_Ensure(); + + PyObject* result = PyObject_CallFunction(damage_handler, "iiiii", target_id, attacker_id > 0 ? attacker_id : Py_None, damage, dflags, mod); + + Py_XDECREF(result); + + PyGILState_Release(gstate); +} diff --git a/python_embed.c b/python_embed.c index c295e25..3cf67a0 100644 --- a/python_embed.c +++ b/python_embed.c @@ -27,6 +27,8 @@ PyObject* client_spawn_handler = NULL; PyObject* kamikaze_use_handler = NULL; PyObject* kamikaze_explode_handler = NULL; +PyObject* damage_handler = NULL; + static PyThreadState* mainstate; static int initialized = 0; @@ -71,6 +73,8 @@ static handler_t handlers[] = { {"kamikaze_use", &kamikaze_use_handler}, {"kamikaze_explode", &kamikaze_explode_handler}, + {"damage", &damage_handler}, + {NULL, NULL} }; @@ -1873,6 +1877,13 @@ static PyObject* PyMinqlx_InitModule(void) { PyModule_AddIntMacro(module, MOD_HMG); PyModule_AddIntMacro(module, MOD_RAILGUN_HEADSHOT); + // damage flags + PyModule_AddIntMacro(module, DAMAGE_RADIUS); + PyModule_AddIntMacro(module, DAMAGE_NO_ARMOR); + PyModule_AddIntMacro(module, DAMAGE_NO_KNOCKBACK); + PyModule_AddIntMacro(module, DAMAGE_NO_PROTECTION); + PyModule_AddIntMacro(module, DAMAGE_NO_TEAM_PROTECTION); + // Initialize struct sequence types. PyStructSequence_InitType(&player_info_type, &player_info_desc); PyStructSequence_InitType(&player_state_type, &player_state_desc); diff --git a/quake_common.h b/quake_common.h index eeeca62..ca49750 100644 --- a/quake_common.h +++ b/quake_common.h @@ -114,7 +114,12 @@ along with this program. If not, see . #define FL_DROPPED_ITEM 0x00001000 -#define DAMAGE_NO_PROTECTION 0x00000008 +// damage flags +#define DAMAGE_RADIUS 0x00000001 // damage was indirect +#define DAMAGE_NO_ARMOR 0x00000002 // armor does not protect from this damage +#define DAMAGE_NO_KNOCKBACK 0x00000004 // do not affect velocity, just view angles +#define DAMAGE_NO_PROTECTION 0x00000008 // armor, shields, invulnerability, and godmode have no effect +#define DAMAGE_NO_TEAM_PROTECTION 0x00000010 // armor, shields, invulnerability, and godmode have no effect typedef enum {qfalse, qtrue} qboolean; typedef unsigned char byte; @@ -1565,6 +1570,7 @@ char* __cdecl My_ClientConnect(int clientNum, qboolean firstTime, qboolean isBot void __cdecl My_ClientSpawn(gentity_t* ent); void __cdecl My_G_StartKamikaze(gentity_t* ent); +void __cdecl My_G_Damage(gentity_t* target, gentity_t* inflictor, gentity_t* attacker, vec3_t dir, vec3_t point, int damage, int dflags, int mod); #endif // Custom commands added using Cmd_AddCommand during initialization. From f8ae6e7ab1c53a92bfed425ddb649f09d3c0f0b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=A4rtner?= Date: Thu, 25 May 2023 19:28:58 +0200 Subject: [PATCH 02/10] fixed compilation errors --- hooks.c | 2 +- python_dispatchers.c | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/hooks.c b/hooks.c index f6d675b..6e13fd6 100644 --- a/hooks.c +++ b/hooks.c @@ -249,7 +249,7 @@ void __cdecl My_G_Damage( target_id = target->client->ps.clientNum; - if attacker && attacker->client { + if (attacker && attacker->client) { attacker_id = attacker->client->ps.clientNum; DamageDispatcher(target_id, attacker_id, damage, dflags, mod);) diff --git a/python_dispatchers.c b/python_dispatchers.c index 74fb25e..aae0656 100644 --- a/python_dispatchers.c +++ b/python_dispatchers.c @@ -301,8 +301,13 @@ void DamageDispatcher(int target_id, int attacker_id, int damage, int dflags, in PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallFunction(damage_handler, "iiiii", target_id, attacker_id > 0 ? attacker_id : Py_None, damage, dflags, mod); - + PyObject* result + if (attacker_id >= 0) { + result = PyObject_CallFunction(damage_handler, "iiiii", target_id, attacker_id, damage, dflags, mod); + } else { + result = PyObject_CallFunction(damage_handler, "iOiii", target_id, Py_None, damage, dflags, mod); + } + Py_XDECREF(result); PyGILState_Release(gstate); From 96268f01a833490ff29e1d2927d7acb9065d4e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=A4rtner?= Date: Thu, 25 May 2023 19:29:27 +0200 Subject: [PATCH 03/10] fixed compilation errors --- python_dispatchers.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_dispatchers.c b/python_dispatchers.c index aae0656..1d37009 100644 --- a/python_dispatchers.c +++ b/python_dispatchers.c @@ -301,7 +301,7 @@ void DamageDispatcher(int target_id, int attacker_id, int damage, int dflags, in PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result + PyObject* result; if (attacker_id >= 0) { result = PyObject_CallFunction(damage_handler, "iiiii", target_id, attacker_id, damage, dflags, mod); } else { From b3881ba4224e6776e1423768a4f1c853881163a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=A4rtner?= Date: Thu, 25 May 2023 19:30:48 +0200 Subject: [PATCH 04/10] fixed compilation errors --- hooks.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hooks.c b/hooks.c index 6e13fd6..c705006 100644 --- a/hooks.c +++ b/hooks.c @@ -252,7 +252,7 @@ void __cdecl My_G_Damage( if (attacker && attacker->client) { attacker_id = attacker->client->ps.clientNum; - DamageDispatcher(target_id, attacker_id, damage, dflags, mod);) + DamageDispatcher(target_id, attacker_id, damage, dflags, mod); } #endif From 31a6342087d6ebf53356b08d387b9ed71ce18c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=A4rtner?= Date: Thu, 25 May 2023 19:31:39 +0200 Subject: [PATCH 05/10] fixed compilation errors --- hooks.c | 1 + 1 file changed, 1 insertion(+) diff --git a/hooks.c b/hooks.c index c705006..9f1a4b5 100644 --- a/hooks.c +++ b/hooks.c @@ -251,6 +251,7 @@ void __cdecl My_G_Damage( if (attacker && attacker->client) { attacker_id = attacker->client->ps.clientNum; + } DamageDispatcher(target_id, attacker_id, damage, dflags, mod); } From 8b0587102bfac99e5ee940bb3fb5859702623821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=A4rtner?= Date: Thu, 25 May 2023 19:39:41 +0200 Subject: [PATCH 06/10] incorporated python version check updates into this branch as well --- python_embed.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python_embed.c b/python_embed.c index 3cf67a0..2833810 100644 --- a/python_embed.c +++ b/python_embed.c @@ -1503,7 +1503,11 @@ void replace_item_core(gentity_t* ent, int item_id) { static PyObject* PyMinqlx_ReplaceItems(PyObject* self, PyObject* args) { PyObject *arg1, *arg2 ; int entity_id = 0, item_id = 0; + #if PY_VERSION_HEX < ((3 << 24) | (7 << 16)) char *entity_classname = NULL, *item_classname = NULL; + #else + const char *entity_classname = NULL, *item_classname = NULL; + #endif gentity_t* ent; @@ -1925,7 +1929,9 @@ PyMinqlx_InitStatus_t PyMinqlx_Initialize(void) { Py_SetProgramName(PYTHON_FILENAME); PyImport_AppendInittab("_minqlx", &PyMinqlx_InitModule); Py_Initialize(); + #if PY_VERSION_HEX < ((3 << 24) | (7 << 16)) PyEval_InitThreads(); + #endif // Add the main module. PyObject* main_module = PyImport_AddModule("__main__"); From e5ecd85d2d455f4a0e2bcf2378631bb383e726dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=A4rtner?= Date: Thu, 25 May 2023 20:08:34 +0200 Subject: [PATCH 07/10] Update _handlers.py --- python/minqlx/_handlers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/minqlx/_handlers.py b/python/minqlx/_handlers.py index 1d218db..5a80645 100644 --- a/python/minqlx/_handlers.py +++ b/python/minqlx/_handlers.py @@ -432,13 +432,13 @@ def handle_kamikaze_explode(client_id, is_used_on_demand): minqlx.log_exception() return True -def handle_damage(target, attacker, damage, dflags, mod): - target_player = minqlx.Player(target) - attacker_player = minqlx.Player(attacker) if attacker is not None else None +def handle_damage(target_id, attacker_id, damage, dflags, mod): + target_player = minqlx.Player(target_id) + inflictor_player = minqlx.Player(attacker_id) if attacker_id is not None and attacker_id >= 0 else None # noinspection PyBroadException try: minqlx.EVENT_DISPATCHERS["damage"].dispatch( - target_player, attacker_player, damage, dflags, mod + target_player, inflictor_player, damage, dflags, mod ) except: # noqa: E722 minqlx.log_exception() From a80144e60392b3514a5a600a14e616c3b44b5b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=A4rtner?= Date: Fri, 26 May 2023 13:38:26 +0200 Subject: [PATCH 08/10] added an is_chatting boolean flag to the PlayerState struct --- python_embed.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python_embed.c b/python_embed.c index 2833810..b7189a7 100644 --- a/python_embed.c +++ b/python_embed.c @@ -121,6 +121,7 @@ static PyStructSequence_Field player_state_fields[] = { {"powerups", "The player's powerups."}, {"holdable", "The player's holdable item."}, {"flight", "A struct sequence with flight parameters."}, + {"is_chatting", "Whether the player is currently chatting."}, {"is_frozen", "Whether the player is frozen(freezetag)."}, {NULL} }; @@ -761,7 +762,8 @@ static PyObject* PyMinqlx_PlayerState(PyObject* self, PyObject* args) { PyLong_FromLongLong(g_entities[client_id].client->ps.stats[STAT_FLIGHT_REFUEL])); PyStructSequence_SetItem(state, 11, flight); - PyStructSequence_SetItem(state, 12, PyBool_FromLong(g_entities[client_id].client->ps.pm_type == 4)); + PyStructSequence_SetItem(state, 12, PyBool_FromLong(g_entities[client_id].client->ps.eFlags & EF_TALK != 0)); + PyStructSequence_SetItem(state, 13, PyBool_FromLong(g_entities[client_id].client->ps.pm_type == 4)); return state; } From 3112eb030fca1d531e2641048ebb820486f5b9a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=A4rtner?= Date: Fri, 2 Jun 2023 15:36:59 +0200 Subject: [PATCH 09/10] Update _handlers.py better filter potential players by the entity_id in range(0, 64) (MAX_PLAYERS = 64 in ql source). Otherwise you will get dmg events from stuff like crushers, lava, trigger_hurt trying to be converted into a player that obviously does not exist. --- python/minqlx/_handlers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/python/minqlx/_handlers.py b/python/minqlx/_handlers.py index 5a80645..ddb2454 100644 --- a/python/minqlx/_handlers.py +++ b/python/minqlx/_handlers.py @@ -433,8 +433,8 @@ def handle_kamikaze_explode(client_id, is_used_on_demand): return True def handle_damage(target_id, attacker_id, damage, dflags, mod): - target_player = minqlx.Player(target_id) - inflictor_player = minqlx.Player(attacker_id) if attacker_id is not None and attacker_id >= 0 else None + target_player = minqlx.Player(target_id) if target_id in range(0, 64) else None + inflictor_player = minqlx.Player(attacker_id) if attacker_id is not None and attacker_id in range(0, 64) else None # noinspection PyBroadException try: minqlx.EVENT_DISPATCHERS["damage"].dispatch( From 3d84b2bd216d4458906fa439f36fa0d4b1aaec5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=A4rtner?= Date: Sun, 13 Aug 2023 19:34:47 +0000 Subject: [PATCH 10/10] incorporated latest remarks --- hooks.c | 6 ++---- python/minqlx/_handlers.py | 12 +++++++----- python_embed.c | 4 +--- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/hooks.c b/hooks.c index 9f1a4b5..5d7d62f 100644 --- a/hooks.c +++ b/hooks.c @@ -247,11 +247,9 @@ void __cdecl My_G_Damage( return; } - target_id = target->client->ps.clientNum; + target_id = target - g_entities; - if (attacker && attacker->client) { - attacker_id = attacker->client->ps.clientNum; - } + attacker_id = attacker - g_entities; DamageDispatcher(target_id, attacker_id, damage, dflags, mod); } diff --git a/python/minqlx/_handlers.py b/python/minqlx/_handlers.py index ddb2454..7a2dd38 100644 --- a/python/minqlx/_handlers.py +++ b/python/minqlx/_handlers.py @@ -433,14 +433,16 @@ def handle_kamikaze_explode(client_id, is_used_on_demand): return True def handle_damage(target_id, attacker_id, damage, dflags, mod): - target_player = minqlx.Player(target_id) if target_id in range(0, 64) else None - inflictor_player = minqlx.Player(attacker_id) if attacker_id is not None and attacker_id in range(0, 64) else None - # noinspection PyBroadException + target_player = minqlx.Player(target_id) if target_id in range(0, 64) else target_id + attacker_player = ( + minqlx.Player(attacker_id) if attacker_id in range(0, 64) else attacker_id + ) + try: minqlx.EVENT_DISPATCHERS["damage"].dispatch( - target_player, inflictor_player, damage, dflags, mod + target_player, attacker_player, damage, dflags, mod ) - except: # noqa: E722 + except: minqlx.log_exception() return True diff --git a/python_embed.c b/python_embed.c index 2a2a3df..e34d394 100644 --- a/python_embed.c +++ b/python_embed.c @@ -122,7 +122,6 @@ static PyStructSequence_Field player_state_fields[] = { {"powerups", "The player's powerups."}, {"holdable", "The player's holdable item."}, {"flight", "A struct sequence with flight parameters."}, - {"is_chatting", "Whether the player is currently chatting."}, {"is_frozen", "Whether the player is frozen(freezetag)."}, {NULL} }; @@ -763,8 +762,7 @@ static PyObject* PyMinqlx_PlayerState(PyObject* self, PyObject* args) { PyLong_FromLongLong(g_entities[client_id].client->ps.stats[STAT_FLIGHT_REFUEL])); PyStructSequence_SetItem(state, 11, flight); - PyStructSequence_SetItem(state, 12, PyBool_FromLong(g_entities[client_id].client->ps.eFlags & EF_TALK != 0)); - PyStructSequence_SetItem(state, 13, PyBool_FromLong(g_entities[client_id].client->ps.pm_type == 4)); + PyStructSequence_SetItem(state, 12, PyBool_FromLong(g_entities[client_id].client->ps.pm_type == 4)); return state; }