From 1b2d078338ae367042232213ee357c9447418417 Mon Sep 17 00:00:00 2001 From: sub-rob <76086318+sub-rob@users.noreply.github.com> Date: Sun, 22 Oct 2023 05:59:24 -0700 Subject: [PATCH] expose events --- CMake/Package.cmake | 1 + CMake/Resources/gameevents.ini | 287 ++++++++++++++++++++++++++++++ Source2Py/src/Events.cpp | 71 ++++++++ Source2Py/src/Events.h | 40 +++++ Source2Py/src/PyInclude.h | 10 ++ Source2Py/src/PyModule.h | 16 +- Source2Py/src/PyPlugin.cpp | 20 ++- Source2Py/src/PyPlugin.h | 3 + Source2Py/src/PyRuntime.h | 4 +- Source2Py/src/Source2Py.cpp | 2 +- Source2Py/src/Source2Py.h | 11 +- Source2Py/src/Source2PyPlugin.cpp | 13 ++ Source2Py/src/Utility.h | 27 +++ 13 files changed, 485 insertions(+), 20 deletions(-) create mode 100644 CMake/Resources/gameevents.ini create mode 100644 Source2Py/src/Events.cpp create mode 100644 Source2Py/src/Events.h create mode 100644 Source2Py/src/PyInclude.h create mode 100644 Source2Py/src/Utility.h diff --git a/CMake/Package.cmake b/CMake/Package.cmake index 17e0722..8e735e5 100644 --- a/CMake/Package.cmake +++ b/CMake/Package.cmake @@ -8,6 +8,7 @@ add_custom_command(TARGET Source2Py POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $ "${SOURCE2PY_PACKAGE_DIR}/bin" COMMAND ${CMAKE_COMMAND} -E copy "${SOURCE2PY_PACKAGE_RES_DIR}/pyplugins.ini" "${SOURCE2PY_PACKAGE_DIR}" + COMMAND ${CMAKE_COMMAND} -E copy "${SOURCE2PY_PACKAGE_RES_DIR}/gameevents.ini" "${SOURCE2PY_PACKAGE_DIR}/bin" # Copy sample plugins to package directory COMMAND ${CMAKE_COMMAND} -E copy "${SAMPLE_PLUGINS_DIR}/SamplePlugin.py" "${SOURCE2PY_PACKAGE_DIR}/plugins" diff --git a/CMake/Resources/gameevents.ini b/CMake/Resources/gameevents.ini new file mode 100644 index 0000000..0e335a0 --- /dev/null +++ b/CMake/Resources/gameevents.ini @@ -0,0 +1,287 @@ +# Dumped event names from CS2, not all are tested +# https://cs2.poggu.me/dumped-data/game-events/ + +server_spawn +server_pre_shutdown +server_shutdown +server_message +server_cvar +player_activate +player_connect_full +player_full_update +player_connect +player_disconnect +player_info +player_spawn +player_team +local_player_team +local_player_controller_team +player_changename +player_hurt +player_chat +local_player_pawn_changed +teamplay_broadcast_audio +finale_start +player_stats_updated +user_data_downloaded +ragdoll_dissolved +team_info +team_score +hltv_cameraman +hltv_chase +hltv_rank_camera +hltv_rank_entity +hltv_fixed +hltv_message +hltv_status +hltv_title +hltv_chat +hltv_versioninfo +hltv_replay +demo_start +demo_stop +demo_skip +map_shutdown +map_transition +hostname_changed +difficulty_changed +game_message +game_newmap +round_start +round_end +round_start_pre_entity +round_start_post_nav +round_freeze_end +teamplay_round_start +player_death +player_footstep +player_hintmessage +break_breakable +break_prop +entity_killed +door_close +vote_started +vote_failed +vote_passed +vote_changed +vote_cast_yes +vote_cast_no +achievement_event +achievement_earned +achievement_write_failed +bonus_updated +spec_target_updated +spec_mode_updated +entity_visible +gameinstructor_draw +gameinstructor_nodraw +flare_ignite_npc +helicopter_grenade_punt_miss +physgun_pickup +inventory_updated +cart_updated +store_pricesheet_updated +item_schema_initialized +drop_rate_modified +event_ticket_modified +gc_connected +instructor_start_lesson +instructor_close_lesson +instructor_server_hint_create +clientside_lesson_closed +dynamic_shadow_light_changed +gameui_hidden +items_gifted +player_score +player_shoot +game_init +game_start +game_end +round_announce_match_point +round_announce_final +round_announce_last_round_half +round_announce_match_start +round_announce_warmup +round_end_upload_stats +round_officially_ended +round_time_warning +ugc_map_info_received +ugc_map_unsubscribed +ugc_map_download_error +ugc_file_download_finished +ugc_file_download_start +begin_new_match +dm_bonus_weapon_start +survival_announce_phase +broken_breakable +player_decal +achievement_increment +set_instructor_group_enabled +instructor_server_hint_stop +read_game_titledata +write_game_titledata +reset_game_titledata +weaponhud_selection +vote_ended +vote_cast +vote_options +endmatch_mapvote_selecting_map +endmatch_cmm_start_reveal_items +client_loadout_changed +add_player_sonar_icon +add_bullet_hit_marker +other_death +item_purchase +bomb_beginplant +bomb_abortplant +bomb_planted +bomb_defused +bomb_exploded +bomb_dropped +bomb_pickup +defuser_dropped +defuser_pickup +announce_phase_end +cs_intermission +bomb_begindefuse +bomb_abortdefuse +hostage_follows +hostage_hurt +hostage_killed +hostage_rescued +hostage_stops_following +hostage_rescued_all +hostage_call_for_help +vip_escaped +vip_killed +player_radio +bomb_beep +weapon_fire +weapon_fire_on_empty +grenade_thrown +weapon_outofammo +weapon_reload +weapon_zoom +silencer_detach +inspect_weapon +weapon_zoom_rifle +player_spawned +item_pickup +item_pickup_slerp +item_pickup_failed +item_remove +ammo_pickup +item_equip +enter_buyzone +exit_buyzone +buytime_ended +enter_bombzone +exit_bombzone +enter_rescue_zone +exit_rescue_zone +silencer_off +silencer_on +buymenu_open +buymenu_close +round_prestart +round_poststart +grenade_bounce +hegrenade_detonate +flashbang_detonate +smokegrenade_detonate +smokegrenade_expired +molotov_detonate +decoy_detonate +decoy_started +tagrenade_detonate +inferno_startburn +inferno_expire +inferno_extinguish +decoy_firing +bullet_impact +player_jump +player_blind +player_falldamage +door_moving +mb_input_lock_success +mb_input_lock_cancel +nav_blocked +nav_generate +achievement_info_loaded +hltv_changed_mode +cs_game_disconnected +cs_round_final_beep +cs_round_start_beep +cs_win_panel_round +cs_win_panel_match +cs_match_end_restart +cs_pre_restart +show_deathpanel +hide_deathpanel +player_avenged_teammate +achievement_earned_local +repost_xbox_achievements +match_end_conditions +round_mvp +show_survival_respawn_status +client_disconnect +gg_player_levelup +ggtr_player_levelup +ggprogressive_player_levelup +gg_killed_enemy +gg_final_weapon_achieved +gg_bonus_grenade_achieved +switch_team +gg_leader +gg_team_leader +gg_player_impending_upgrade +write_profile_data +trial_time_expired +update_matchmaking_stats +player_reset_vote +enable_restart_voting +sfuievent +start_vote +player_given_c4 +gg_reset_round_start_sounds +tr_player_flashbanged +tr_mark_complete +tr_mark_best_time +tr_exit_hint_trigger +bot_takeover +tr_show_finish_msgbox +tr_show_exit_msgbox +jointeam_failed +teamchange_pending +material_default_complete +cs_prev_next_spectator +cs_handle_ime_event +nextlevel_changed +seasoncoin_levelup +tournament_reward +start_halftime +ammo_refill +parachute_pickup +parachute_deploy +dronegun_attack +drone_dispatched +loot_crate_visible +loot_crate_opened +open_crate_instr +smoke_beacon_paradrop +survival_paradrop_spawn +survival_paradrop_break +drone_cargo_detached +drone_above_roof +choppers_incoming_warning +firstbombs_incoming_warning +dz_item_interaction +survival_teammate_respawn +survival_no_respawns_warning +survival_no_respawns_final +player_ping +player_ping_stop +player_sound +guardian_wave_restart +team_intro_start +team_intro_end \ No newline at end of file diff --git a/Source2Py/src/Events.cpp b/Source2Py/src/Events.cpp new file mode 100644 index 0000000..5b114d0 --- /dev/null +++ b/Source2Py/src/Events.cpp @@ -0,0 +1,71 @@ +#include "Events.h" +#include "Utility.h" + +#include +#include +#include +#include + +#define EVENTLISTENER_SLOTS 300 // todo: remove + +namespace Source2Py { + + extern IGameEventManager2* gameevents; + extern IServerGameDLL* server; + + std::vector eventListeners; + + bool EventService::Init() { + gameevents = static_cast(CallVFunc(server)); // get interface from ISource2Server vtable (func signature incomplete) + if (!gameevents) { + Log::Write("Failed to load IGameEventManager2!"); + return false; + } + + Log::Write("IGameEventManager2 @ 0x" + AddrToString(static_cast(gameevents))); + Log::Write("EventService initialized"); + return true; + } + + bool EventService::LoadEventsFromFile(const std::string& filepath) { + std::ifstream eventsFile(filepath); + + if (eventsFile.fail()) { + Log::Error("Failed to load events file: " + filepath); + return false; + } + + // Keep the EventListeners memory block from jumping around + // todo: precalculate the exact vector size needed. this is a crash waiting to happen if events + eventListeners.reserve(EVENTLISTENER_SLOTS); + Log::Write("Reserved " + std::to_string(EVENTLISTENER_SLOTS) + " EventListener slots (" + std::to_string(sizeof(EventListener) * EVENTLISTENER_SLOTS) + " bytes)"); + + std::string line; + while (std::getline(eventsFile, line)) { + // ignore comments + if (line[0] == '#' || line[0] == ';' || line.empty()) + continue; + + eventListeners.push_back(line); + EventListener& listener = eventListeners.back(); + + if (!gameevents->AddListener(&listener, listener.GetEventName(), true)) { + Log::Write("Failed to add EventListener: " + std::string(listener.GetEventName())); + eventListeners.pop_back(); + } + } + + Log::Write("Added " + std::to_string(eventListeners.size()) + " EventListeners"); + if (eventListeners.size() > EVENTLISTENER_SLOTS) // todo: remove + Log::Error("Added more EventListeners than reserved, prepare for crash!"); + + return true; + } + + void EventService::UnloadEvents() { + for (auto& listener : eventListeners) + gameevents->RemoveListener(&listener); + + Log::Write("Unloaded all EventListeners"); + } +} \ No newline at end of file diff --git a/Source2Py/src/Events.h b/Source2Py/src/Events.h new file mode 100644 index 0000000..622fb4a --- /dev/null +++ b/Source2Py/src/Events.h @@ -0,0 +1,40 @@ +#pragma once + +#include "Source2Py.h" + +#include +#include + +namespace Source2Py { + + extern Source2PyPlugin g_Source2PyPlugin; + + class EventListener; + + class EventService { + public: + static bool Init(); + static bool LoadEventsFromFile(const std::string& filepath); + static void UnloadEvents(); + + private: + + }; + + class EventListener : public IGameEventListener2 { + public: + EventListener(const std::string& eventName) : m_EventName(eventName) {} + + // FireEvent is called by EventManager if event just occurred + // KeyValue memory will be freed by manager if not needed anymore + void FireGameEvent(IGameEvent* event) override { + g_Source2PyPlugin.Hook_FireGameEvent(event); // Dispatch event to g_Source2PyPlugin (to be then dispatched to Python) + } + + const char* GetEventName() const { return m_EventName.c_str(); } + + private: + std::string m_EventName; + }; + +} \ No newline at end of file diff --git a/Source2Py/src/PyInclude.h b/Source2Py/src/PyInclude.h new file mode 100644 index 0000000..0903ca8 --- /dev/null +++ b/Source2Py/src/PyInclude.h @@ -0,0 +1,10 @@ +#pragma once + +#include +namespace py = pybind11; + +// pybind11 does some sketchy things that breaks the Source 2 SDK when compiling with MSVC +#ifdef _WIN32 + #undef _DEBUG + #define _DEBUG 1 +#endif \ No newline at end of file diff --git a/Source2Py/src/PyModule.h b/Source2Py/src/PyModule.h index 172f037..ad7bef0 100644 --- a/Source2Py/src/PyModule.h +++ b/Source2Py/src/PyModule.h @@ -1,18 +1,28 @@ +// Include this _once_ in a source file to define Source2Py module + #pragma once #include "PyAPI.h" +#include "PyInclude.h" -#include -namespace py = pybind11; +#include PYBIND11_EMBEDDED_MODULE(Source2Py, m) { using namespace Source2Py; - m.def("Print", &PyAPI::ConPrint); + m.def("ServerPrint", &PyAPI::ConPrint); m.def("ClientPrint", &PyAPI::ClientConPrint); m.def("ServerCommand", &PyAPI::ServerCommand); m.def("ClientCommand", &PyAPI::ClientCommand); m.def("SetTimescale", &PyAPI::SetTimescale); + + py::class_(m, "GameEvent") + .def("GetName", &IGameEvent::GetName) + .def("GetID", &IGameEvent::GetID) + .def("GetBool", &IGameEvent::GetBool) + .def("GetInt", &IGameEvent::GetInt) + .def("GetFloat", &IGameEvent::GetFloat) + .def("GetString", &IGameEvent::GetString); } \ No newline at end of file diff --git a/Source2Py/src/PyPlugin.cpp b/Source2Py/src/PyPlugin.cpp index daa96e0..644936f 100644 --- a/Source2Py/src/PyPlugin.cpp +++ b/Source2Py/src/PyPlugin.cpp @@ -28,31 +28,31 @@ namespace Source2Py { } void PyPlugin::Load() { - PyRuntime::ExecuteObjectMethod(m_PluginObject, "Load"); + PyRuntime::ExecuteObjectMethod(m_PluginObject, "OnPluginLoad"); } void PyPlugin::Unload() { - PyRuntime::ExecuteObjectMethod(m_PluginObject, "Unload"); + PyRuntime::ExecuteObjectMethod(m_PluginObject, "OnPluginUnload"); } void PyPlugin::GameFrame(bool simulating, bool firstTick, bool lastTick) { - PyRuntime::ExecuteObjectMethod(m_PluginObject, "GameFrame", simulating, firstTick, lastTick); + PyRuntime::ExecuteObjectMethod(m_PluginObject, "OnGameFrame", simulating, firstTick, lastTick); } void PyPlugin::ClientActive(int playerSlot, bool loadGame, const char* name, uint64_t xuid) { - PyRuntime::ExecuteObjectMethod(m_PluginObject, "ClientActive", playerSlot, loadGame, name, xuid); + PyRuntime::ExecuteObjectMethod(m_PluginObject, "OnClientActive", playerSlot, loadGame, name, xuid); } void PyPlugin::ClientDisconnect(int playerSlot, int reason, const char* name, uint64_t xuid, const char* networkID) { - PyRuntime::ExecuteObjectMethod(m_PluginObject, "ClientDisconnect", playerSlot, reason, name, xuid, networkID); + PyRuntime::ExecuteObjectMethod(m_PluginObject, "OnClientDisconnect", playerSlot, reason, name, xuid, networkID); } void PyPlugin::ClientPutInServer(int playerSlot, char const* name, int type, uint64_t xuid) { - PyRuntime::ExecuteObjectMethod(m_PluginObject, "ClientPutInServer", playerSlot, name, type, xuid); + PyRuntime::ExecuteObjectMethod(m_PluginObject, "OnClientPutInServer", playerSlot, name, type, xuid); } void PyPlugin::ClientSettingsChanged(int playerSlot) { - PyRuntime::ExecuteObjectMethod(m_PluginObject, "ClientSettingsChanged", playerSlot); + PyRuntime::ExecuteObjectMethod(m_PluginObject, "OnClientSettingsChanged", playerSlot); } void PyPlugin::OnClientConnected(int playerSlot, const char* name, uint64_t xuid, const char* networkID, const char* address, bool fakePlayer) { @@ -60,7 +60,11 @@ namespace Source2Py { } void PyPlugin::ClientConnect(int playerSlot, const char* name, uint64_t xuid, const char* networkID) { - PyRuntime::ExecuteObjectMethod(m_PluginObject, "ClientConnect", playerSlot, name, xuid, networkID); + PyRuntime::ExecuteObjectMethod(m_PluginObject, "OnClientConnect", playerSlot, name, xuid, networkID); + } + + void PyPlugin::FireGameEvent(IGameEvent* event) { + PyRuntime::ExecuteObjectMethod(m_PluginObject, "OnGameEvent", event); } } \ No newline at end of file diff --git a/Source2Py/src/PyPlugin.h b/Source2Py/src/PyPlugin.h index d6e43f4..6a48bbd 100644 --- a/Source2Py/src/PyPlugin.h +++ b/Source2Py/src/PyPlugin.h @@ -2,6 +2,8 @@ #include "PyRuntime.h" +#include + namespace Source2Py { class PyPlugin { @@ -24,6 +26,7 @@ namespace Source2Py { void OnClientConnected(int playerSlot, const char* name, uint64_t xuid, const char* networkID, const char* address, bool fakePlayer); void ClientConnect(int playerSlot, const char* name, uint64_t xuid, const char* networkID); //void ClientCommand(int playerSlot, const CCommand& _cmd); (todo: port CCommand) + void FireGameEvent(IGameEvent* event); bool IsValid() const { return m_Valid; } diff --git a/Source2Py/src/PyRuntime.h b/Source2Py/src/PyRuntime.h index 670fae5..0b8656f 100644 --- a/Source2Py/src/PyRuntime.h +++ b/Source2Py/src/PyRuntime.h @@ -1,9 +1,7 @@ #pragma once #include "Log.h" - -#include -namespace py = pybind11; +#include "PyInclude.h" namespace Source2Py { diff --git a/Source2Py/src/Source2Py.cpp b/Source2Py/src/Source2Py.cpp index a8abb93..822e5f3 100644 --- a/Source2Py/src/Source2Py.cpp +++ b/Source2Py/src/Source2Py.cpp @@ -40,7 +40,7 @@ namespace Source2Py { } } - Log::Write("Loaded " + std::to_string(m_Plugins.size()) + " Python plugins"); + Log::Write("Loaded " + std::to_string(m_Plugins.size()) + " Python plugin(s)"); return true; } diff --git a/Source2Py/src/Source2Py.h b/Source2Py/src/Source2Py.h index aef5f39..61050bf 100644 --- a/Source2Py/src/Source2Py.h +++ b/Source2Py/src/Source2Py.h @@ -1,16 +1,15 @@ #pragma once -#include "PyPlugin.h" - #include -#include -#include -#include +#include "PyPlugin.h" #include #include namespace fs = std::filesystem; +#include + + namespace Source2Py { @@ -38,6 +37,8 @@ namespace Source2Py { void Hook_OnClientConnected(CPlayerSlot slot, const char* pszName, uint64 xuid, const char* pszNetworkID, const char* pszAddress, bool bFakePlayer); bool Hook_ClientConnect(CPlayerSlot slot, const char* pszName, uint64 xuid, const char* pszNetworkID, bool unk1, CBufferString* pRejectReason); void Hook_ClientCommand(CPlayerSlot nSlot, const CCommand& _cmd); + void Hook_FireGameEvent(IGameEvent* event); + // Plugin meta information virtual const char* GetAuthor() override { return "s95rob"; } diff --git a/Source2Py/src/Source2PyPlugin.cpp b/Source2Py/src/Source2PyPlugin.cpp index 59d760a..2647032 100644 --- a/Source2Py/src/Source2PyPlugin.cpp +++ b/Source2Py/src/Source2PyPlugin.cpp @@ -1,6 +1,8 @@ #include "Source2Py.h" #include "Log.h" #include "PyRuntime.h" +#include "Utility.h" +#include "Events.h" #include @@ -39,6 +41,7 @@ namespace Source2Py { GET_V_IFACE_ANY(GetServerFactory, server, IServerGameDLL, INTERFACEVERSION_SERVERGAMEDLL); GET_V_IFACE_ANY(GetServerFactory, gameclients, IServerGameClients, INTERFACEVERSION_SERVERGAMECLIENTS); GET_V_IFACE_ANY(GetEngineFactory, g_pNetworkServerService, INetworkServerService, NETWORKSERVERSERVICE_INTERFACE_VERSION); + EventService::Init(); SH_ADD_HOOK_MEMFUNC(IServerGameDLL, GameFrame, server, this, &Source2PyPlugin::Hook_GameFrame, true); SH_ADD_HOOK_MEMFUNC(IServerGameClients, ClientActive, gameclients, this, &Source2PyPlugin::Hook_ClientActive, true); @@ -53,6 +56,9 @@ namespace Source2Py { fs::path exePath = fs::current_path(); // save for later fs::current_path(GetPluginBaseDirectory()); + // Load events from dumped events file + EventService::LoadEventsFromFile("bin/gameevents.ini"); + if (!PyRuntime::Init()) success = false; @@ -82,6 +88,8 @@ namespace Source2Py { PyRuntime::Close(); + EventService::UnloadEvents(); + Log::Write("Plugin unloaded successfully"); return true; @@ -137,4 +145,9 @@ namespace Source2Py { for (auto& plugin : m_Plugins) plugin.GameFrame(simulating, bFirstTick, bLastTick); } + + void Source2PyPlugin::Hook_FireGameEvent(IGameEvent* event) { + for (auto& plugin : m_Plugins) + plugin.FireGameEvent(event); + } } \ No newline at end of file diff --git a/Source2Py/src/Utility.h b/Source2Py/src/Utility.h new file mode 100644 index 0000000..3db2998 --- /dev/null +++ b/Source2Py/src/Utility.h @@ -0,0 +1,27 @@ +#pragma once + +// Virtual function invoker lifted from https://github.com/komashchenko/MiniVIP +template +constexpr T CallVFunc(void* pThis, Args... args) noexcept +{ + return reinterpret_cast (reinterpret_cast(pThis)[0][index])(pThis, args...); +} + +#include +#include + +inline std::string AddrToString(void* address) { + std::stringstream ss; + ss << address; + return ss.str(); +} + +#include + +template +using Ref = std::shared_ptr; +template +constexpr Ref NewRef(Args&& ... args) +{ + return std::make_shared(std::forward(args)...); +} \ No newline at end of file