diff --git a/CMakeLists.txt b/CMakeLists.txt index 1282c1ad..75d537d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,11 +91,6 @@ setup_build_configuration() # Add subdirectories for dependencies and projects add_subdirectory("deps") -if(WIN32) - # cppmods link against the full UE4SS target; enable on Linux once libUE4SS.so links - add_subdirectory("cppmods") -endif() - option(UE4SS_BUILD_TESTS "Build native unit tests" OFF) if(UE4SS_BUILD_TESTS) enable_testing() diff --git a/assets/MapGenBP/Content/MapGen/MapCreator.uasset b/assets/MapGenBP/Content/MapGen/MapCreator.uasset deleted file mode 100644 index 6ed90bde..00000000 Binary files a/assets/MapGenBP/Content/MapGen/MapCreator.uasset and /dev/null differ diff --git a/assets/MapGenBP/Content/MapGen/MapSpawnStruct.uasset b/assets/MapGenBP/Content/MapGen/MapSpawnStruct.uasset deleted file mode 100644 index 7c0397f2..00000000 Binary files a/assets/MapGenBP/Content/MapGen/MapSpawnStruct.uasset and /dev/null differ diff --git a/assets/MapGenBP/Content/MapGen/MeshStruct.uasset b/assets/MapGenBP/Content/MapGen/MeshStruct.uasset deleted file mode 100644 index 499074e0..00000000 Binary files a/assets/MapGenBP/Content/MapGen/MeshStruct.uasset and /dev/null differ diff --git a/assets/MapGenBP/Readme.md b/assets/MapGenBP/Readme.md deleted file mode 100644 index 75ade956..00000000 --- a/assets/MapGenBP/Readme.md +++ /dev/null @@ -1,11 +0,0 @@ -## UE4SS Map Dumper - Map Gen Editor BP - -Extract to your project's content folder (Content/MapGen/) - -Import the dumped .csv file as a datatable based on "MapSpawnStruct". - -Drag the BP onto an empty map. - -Select the Actor within the map, and set the relevant datatable. - -Press "Create Map" under default in the details tab. \ No newline at end of file diff --git a/cmake/modules/IDEOrganization.cmake b/cmake/modules/IDEOrganization.cmake index af5d2cf5..6e772bb7 100644 --- a/cmake/modules/IDEOrganization.cmake +++ b/cmake/modules/IDEOrganization.cmake @@ -162,8 +162,6 @@ function(organize_targets_by_source_dir) set_target_properties(${target} PROPERTIES FOLDER "deps/first") elseif(REL_SOURCE_DIR MATCHES "^deps/third") set_target_properties(${target} PROPERTIES FOLDER "deps/third") - elseif(REL_SOURCE_DIR MATCHES "^cppmods") - set_target_properties(${target} PROPERTIES FOLDER "Mods") elseif(REL_SOURCE_DIR MATCHES "^UE4SS") set_target_properties(${target} PROPERTIES FOLDER "RE-UE4SS") endif() diff --git a/cppmods/CMakeLists.txt b/cppmods/CMakeLists.txt deleted file mode 100644 index b682e35b..00000000 --- a/cppmods/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -# Enable IDE organization with folders -set_property(GLOBAL PROPERTY USE_FOLDERS ON) - -# Add C++ mods -add_subdirectory("KismetDebuggerMod") -add_subdirectory("EventViewerMod") - -# Organize targets in the "mods" folder -get_property(TARGETS DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY BUILDSYSTEM_TARGETS) -foreach(target ${TARGETS}) - set_target_properties(${target} PROPERTIES FOLDER "mods") -endforeach() diff --git a/cppmods/EventViewerMod/CMakeLists.txt b/cppmods/EventViewerMod/CMakeLists.txt deleted file mode 100644 index 71209af7..00000000 --- a/cppmods/EventViewerMod/CMakeLists.txt +++ /dev/null @@ -1,28 +0,0 @@ -cmake_minimum_required(VERSION 3.22) -set(TARGET EventViewerMod) -project(${TARGET}) - -include(FetchContent) - -FetchContent_Declare( - concurrentqueue - GIT_REPOSITORY https://github.com/cameron314/concurrentqueue.git - GIT_TAG c68072129c8a5b4025122ca5a0c82ab14b30cb03 -) -FetchContent_MakeAvailable(concurrentqueue) - -add_library(${TARGET} SHARED - src/dllmain.cpp - src/EventViewer.cpp - src/Middleware.cpp - src/Client.cpp - src/Structs.cpp - src/StringPool.cpp - src/EntryCallStackRenderer.cpp - src/FilterCountRenderer.cpp -) - -target_include_directories(${TARGET} PRIVATE "include") -target_link_libraries(${TARGET} PRIVATE ImGui) -target_link_libraries(${TARGET} PRIVATE concurrentqueue) -target_link_libraries(${TARGET} PUBLIC UE4SS) \ No newline at end of file diff --git a/cppmods/EventViewerMod/README.md b/cppmods/EventViewerMod/README.md deleted file mode 100644 index 2b5c3454..00000000 --- a/cppmods/EventViewerMod/README.md +++ /dev/null @@ -1,128 +0,0 @@ -# EventViewerMod - -A UE4SS C++ mod that captures Unreal Engine call flow and renders it live in ImGui. - -It hooks **ProcessEvent**, **ProcessInternal**, and **ProcessLocalScriptFunction** concurrently and uses a **single -unified depth counter** so nested and recursive call chains keep a consistent indentation story (PE → PI → PLSF → …). - -## What you can do - -- Watch a **live call stack** with depth-indented entries. -- Switch between **Stack** and **Frequency** modes. -- Filter captures with **case-insensitive** whitelist/blacklist substring rules. -- Pause the stream and use right-click context menus to copy names, add filters, or open a focused call-stack modal. -- Save the current view (or everything) to a timestamped text file. - -## UI overview - -### Enable / Start / Pause - -- **Enable** toggles the mod on/off (and persists that setting). -- **Start/Stop** controls whether the middleware is actively capturing and dequeuing. -- **Pause** keeps capturing logic installed but stops dequeuing and UI growth. - -### Target filter - -The **Target** combo is a *view filter*, not a capture filter: - -- **All** shows the call stack exactly as the middleware reports it. -- **ProcessEvent / ProcessInternal / ProcessLocalScriptFunction** show only entries that originated from that hook. - -Important: depth is **not** recomputed when you filter. If you hide callers, the remaining entries keep their original -depth so you can still read the true nesting structure. - -### Modes - -- **Stack**: live call stack history (ordered by time, per thread). -- **Frequency**: aggregates by function and tracks how often it appears. - -### Thread picker - -Captures are grouped by the originating `std::thread::id`. The combo lets you switch which thread you’re viewing. The -game thread is labeled with `(Game)` when detected. - -### Performance knobs - -- **Max MS Read Time** and **Max Count Per Iteration** bound how much work `dequeue()` is allowed to do per ImGui frame. - -### Saving captures - -- **Save** writes the current thread + current mode to a timestamped file. -- **Save All** dumps both modes for all threads. - -## Filtering (case-insensitive) - -Whitelist and blacklist entries are **comma-separated tokens**. - -- Tokens are trimmed and converted to lowercase (ASCII-only lowercasing). -- Filtering is done against the entry’s cached **lower-cased** strings. -- The UI always displays the original (non-lowercased) names. - -Rules: - -- **Whitelist**: if empty, everything passes. If non-empty, an entry passes if **any** whitelist token is a substring - match. -- **Blacklist**: if any blacklist token is a substring match, the entry fails. -- **Show Tick Functions** is an additional filter gate applied on top. - -## Right-click menus - -Both stack entries and frequency entries have a right-click menu (when enabled by the current render flags) with helpers -such as: - -- Copy function/caller names to clipboard -- Add function/caller to whitelist/blacklist -- Open the call stack modal (when the stream is paused) - -## Call stack modal - -When the stream is paused, the context menu can open a modal window that shows an entry’s root call chain. - -Definitions: - -- The **root caller** of an entry is the depth `0` entry that began the call chain that ultimately led to the selected - entry. - -The modal provides: - -- **Show full context** - - Enabled: shows all calls produced by the root caller (the entire subtree under that root). - - Disabled: shows the path from the root → selected entry, plus the calls triggered by the selected entry. -- **Disable Indent Colors** - - Mirrors the main window’s behavior. - -## Architecture (high-level) - -- **Middleware** (`include/Middleware.hpp`, `src/Middleware.cpp`) - - Owns the UE hooks and pushes lightweight capture entries into a `moodycamel::ConcurrentQueue`. - - Uses thread-local producer tokens for low overhead under high call volume. - - Uses a one-time barrier/flag to prevent enqueuing until all hooks are installed (to keep depth sane). - -- **Client** (`include/Client.hpp`, `src/Client.cpp`) - - ImGui renderer + persistent UI state. - - Dequeues entries, groups by thread, maintains stack/frequency views, and applies filters. - -- **StringPool** (`include/StringPool.hpp`, `src/StringPool.cpp`) - - Interns function and caller strings and returns stable `std::string_view` pairs. - - Caches both original and lowercased variants. - - Produces a function hash (from Unreal’s `ComparisonIndex`) to avoid expensive string comparisons in hot paths. - -- **EntryCallStackRenderer** (`include/EntryCallStackRenderer.hpp`, `src/EntryCallStackRenderer.cpp`) - - Manages the call-stack modal’s state and rendering. - -## Files and persistence - -- UI state is stored as JSON at: - - `Mods/EventViewerMod/config/settings.json` -- Capture dumps are written to: - - `Mods/EventViewerMod/captures/` - -## Building - -This mod is intended to be compiled as a UE4SS C++ mod (MSVC, `/std:c++latest`). - -## Notes and gotchas - -- String views returned from `StringPool` are stable until the pool is cleared. The current implementation is designed - for “grow-only” usage during a session and never clears, but if later implementation does want to support clearing it, - they should also clear all threads. diff --git a/cppmods/EventViewerMod/include/Client.hpp b/cppmods/EventViewerMod/include/Client.hpp deleted file mode 100644 index 6a004412..00000000 --- a/cppmods/EventViewerMod/include/Client.hpp +++ /dev/null @@ -1,94 +0,0 @@ -#pragma once - -// EventViewerMod: ImGui-facing front-end. -// -// Owns persistent UI state (filters, selected view, per-thread buffers) and pulls capture entries -// from the Middleware each frame. The hot path (hooking + enqueue) lives in Middleware; Client -// is deliberately written as a consumer that can be throttled (max ms / max count per frame). -// -// Threading notes: -// - render() is expected to be called only on the ImGui thread. -// - request_save_state() may be called from any thread (it uses an atomic flag). - -#include -#include - -#include -#include -#include - -#include - -namespace RC::EventViewerMod -{ - class EntryCallStackRenderer; - - class Client - { - public: - // [Thread-ImGui] - auto render() -> void; - - // [Thread-Any] Saves state on the next frame. - auto request_save_state() -> void; - - // [Thread-ImGui] - auto add_to_white_list(std::string_view item) -> void; - - // [Thread-ImGui] - auto add_to_black_list(std::string_view item) -> void; - - // [Thread-ImGui] - auto render_entry_stack_modal(const CallStackEntry* entry) -> void; - - // [Thread-Any] - static auto GetInstance() -> Client&; - - private: - Client(); - - auto render_cfg() -> void; - auto render_perf_opts() -> void; - auto render_view() -> void; - - static auto combo_with_flags(const char* label, int* current_item, const char* const items[], int items_count, ImGuiComboFlags_ flags = ImGuiComboFlags_None) - -> bool; - - auto save_state() -> void; - auto load_state() -> void; - auto check_save_request() -> bool; - - auto apply_filters_to_history(bool whitelist_changed, bool blacklist_changed, bool tick_changed) -> void; - auto dequeue() -> void; - - auto passes_filters(std::string_view test_str) const -> bool; - - enum class ESaveMode - { - none, - current, - all - }; - - auto save(ESaveMode mode) -> void; - auto serialize_view(ThreadInfo& info, EMode mode, EMiddlewareHookTarget hook_target, std::ofstream& out) const -> void; - auto serialize_all_views(std::ofstream& out) -> void; - - auto clear_threads() -> void; - - auto can_render_entry(const CallStackEntry& entry) const -> bool; - auto resize_render_set(ThreadInfo& thread, size_t max_size) const -> void; - - UIState m_state{}; - - Middleware& m_middleware; - - std::filesystem::path m_cfg_path{}; - std::filesystem::path m_dump_dir{}; - - std::unique_ptr m_entry_call_stack_renderer{}; - FilterCountRenderer m_filter_count_renderer{}; - - bool m_imgui_thread_id_set = false; - }; -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/EntryCallStackRenderer.hpp b/cppmods/EventViewerMod/include/EntryCallStackRenderer.hpp deleted file mode 100644 index d3e4d9f3..00000000 --- a/cppmods/EventViewerMod/include/EntryCallStackRenderer.hpp +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -// EventViewerMod: Call stack modal renderer. -// -// The main Stack view is a scrolling, time-ordered history. This helper renders a *focused slice*: -// given a selected CallStackEntry, it builds a context vector and renders it in an ImGui modal. -// -// Concepts: -// - "Root" caller: the depth==0 entry that started the call chain. An entry can be its own root. -// - Full context: show all entries produced by the root call chain. -// - Focused context: show the path leading to the selected entry, the entry itself, and then -// everything that happens underneath that entry. -// -// Note: ImGui modals require calling OpenPopup() on the frame you want the modal to begin opening. - -#include -#include - -namespace RC::EventViewerMod -{ - // Renders the call stack/context modal for a single selected entry. - // The context vector is prepared by the caller (Client) and passed in by value. - class EntryCallStackRenderer - { - public: - EntryCallStackRenderer() = delete; - EntryCallStackRenderer(const EntryCallStackRenderer& Other) = delete; - EntryCallStackRenderer(EntryCallStackRenderer&& Other) noexcept = delete; - EntryCallStackRenderer& operator=(const EntryCallStackRenderer& Other) = delete; - EntryCallStackRenderer& operator=(EntryCallStackRenderer&& Other) noexcept = delete; - - EntryCallStackRenderer(size_t target_idx, std::vector context); - - // Returns false when the modal is finished and can be destroyed. - auto render() -> bool; - - private: - size_t m_target_idx; - const CallStackEntry* m_target_ptr; - std::vector m_context; - std::string m_last_save_path; - bool m_disable_indent_colors = false; - bool m_show_full_context = false; - // ImGui popups need an explicit OpenPopup() call. We request it once so the modal can appear. - bool m_requested_open = false; - - auto save() -> void; - }; -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/Enums.hpp b/cppmods/EventViewerMod/include/Enums.hpp deleted file mode 100644 index b738c461..00000000 --- a/cppmods/EventViewerMod/include/Enums.hpp +++ /dev/null @@ -1,153 +0,0 @@ -#pragma once - -// EventViewerMod: UI enums and small reflection helpers. -// -// We use X-macros to define enums once and auto-generate: -// - E_Size -// - E_NameArray (for ImGui combo boxes) -// - to_string(...) and to_prefix_string(...) -// -// MiddlewareHookTarget is a *bitmask* enum (powers of two). In the UI it is used as a view filter: -// selecting a target hides entries not produced by that hook, but DOES NOT change indentation depth. - -namespace RC::EventViewerMod -{ -#define EVM_MIDDLEWARE_HOOK_TARGET_FLAGS(X, EnumName) \ - X(EnumName, All, ((1 << 0) | (1 << 1) | (1 << 2))) \ - X(EnumName, ProcessEvent, (1 << 0)) \ - X(EnumName, ProcessInternal, (1 << 1)) \ - X(EnumName, ProcessLocalScriptFunction, (1 << 2)) - -#define EVM_MODE(X, EnumName) \ - X(EnumName, Stack) \ - X(EnumName, Frequency) - -#define EVM_STRINGIZE_1(x) #x -#define EVM_STRINGIZE(x) EVM_STRINGIZE_1(x) - -// Per-item emitters -#define EVM_ENUM_ELEM(EnumName, v) v, -#define EVM_ENUM_CASE(EnumName, v) \ - case E##EnumName::v: \ - return EVM_STRINGIZE(v); -#define EVM_ENUM_STR(EnumName, v) EVM_STRINGIZE(v), -#define EVM_ENUM_COUNT(EnumName, v) +1 - -// Prefix emitters -#define EVM_ENUM_PREFIX_CASE(EnumName, v) \ - case E##EnumName::v: \ - return "(" EVM_STRINGIZE(v) ") "; - -// Per-item emitters (with values) -#define EVM_ENUM_ELEM_V(EnumName, v, val) v = val, -#define EVM_ENUM_CASE_V(EnumName, v, val) \ - case E##EnumName::v: \ - return EVM_STRINGIZE(v); -#define EVM_ENUM_STR_V(EnumName, v, val) EVM_STRINGIZE(v), -#define EVM_ENUM_VAL_V(EnumName, v, val) E##EnumName::v, -#define EVM_ENUM_COUNT_V(EnumName, v, val) +1 - -// Prefix emitters (with values) -#define EVM_ENUM_PREFIX_CASE_V(EnumName, v, val) \ - case E##EnumName::v: \ - return "(" EVM_STRINGIZE(v) ") "; - -#define EVM_DECLARE_REFLECTED_ENUM(Name, LIST2) \ - enum class E##Name : int{LIST2(EVM_ENUM_ELEM, Name)}; \ - \ - inline static constexpr int E##Name##_Size = 0 LIST2(EVM_ENUM_COUNT, Name); \ - \ - inline static constexpr const char* E##Name##_NameArray[E##Name##_Size] = {LIST2(EVM_ENUM_STR, Name)}; \ - \ - inline constexpr const char* to_string(E##Name e) noexcept \ - { \ - switch (e) \ - { \ - LIST2(EVM_ENUM_CASE, Name) \ - default: \ - return ""; \ - } \ - } \ - \ - inline constexpr const char* to_prefix_string(E##Name e) noexcept \ - { \ - switch (e) \ - { \ - LIST2(EVM_ENUM_PREFIX_CASE, Name) \ - default: \ - return "() "; \ - } \ - } - -#define EVM_DECLARE_REFLECTED_ENUM_VALUES(Name, LIST3) \ - enum class E##Name : int{LIST3(EVM_ENUM_ELEM_V, Name)}; \ - \ - inline static constexpr int E##Name##_Size = 0 LIST3(EVM_ENUM_COUNT_V, Name); \ - \ - inline static constexpr E##Name E##Name##_ValueArray[E##Name##_Size] = {LIST3(EVM_ENUM_VAL_V, Name)}; \ - \ - inline static constexpr const char* E##Name##_NameArray[E##Name##_Size] = {LIST3(EVM_ENUM_STR_V, Name)}; \ - \ - inline constexpr const char* to_string(E##Name e) noexcept \ - { \ - switch (e) \ - { \ - LIST3(EVM_ENUM_CASE_V, Name) \ - default: \ - return ""; \ - } \ - } - - EVM_DECLARE_REFLECTED_ENUM_VALUES(MiddlewareHookTarget, EVM_MIDDLEWARE_HOOK_TARGET_FLAGS); - EVM_DECLARE_REFLECTED_ENUM(Mode, EVM_MODE); - -#undef EVM_DECLARE_REFLECTED_ENUM_VALUES -#undef EVM_DECLARE_REFLECTED_ENUM -#undef EVM_ENUM_PREFIX_CASE_V -#undef EVM_ENUM_PREFIX_CASE -#undef EVM_ENUM_COUNT_V -#undef EVM_ENUM_VAL_V -#undef EVM_ENUM_STR_V -#undef EVM_ENUM_CASE_V -#undef EVM_ENUM_ELEM_V -#undef EVM_ENUM_COUNT -#undef EVM_ENUM_STR -#undef EVM_ENUM_CASE -#undef EVM_ENUM_ELEM -#undef EVM_STRINGIZE -#undef EVM_STRINGIZE_1 -#undef EVM_MODE -#undef EVM_MIDDLEWARE_HOOK_TARGET_FLAGS - - enum ECallStackEntryRenderFlags_ : uint8_t - { - ECallStackEntryRenderFlags_None = 0, - ECallStackEntryRenderFlags_WithSupportMenus = 1, - ECallStackEntryRenderFlags_WithSupportMenusCallStackModal = 3, - ECallStackEntryRenderFlags_IndentColors = 4, - ECallStackEntryRenderFlags_Highlight = 8 - }; - - enum ECallFrequencyEntryRenderFlags_ : uint8_t - { - ECallFrequencyEntryRenderFlags_None = 0, - ECallFrequencyEntryRenderFlags_WithSupportMenus = 1 - }; - - inline constexpr const char* to_prefix_string(const EMiddlewareHookTarget e) noexcept - { - switch (e) - { - case EMiddlewareHookTarget::ProcessEvent: - return "(PE) "; - case EMiddlewareHookTarget::ProcessInternal: - return "(PI) "; - case EMiddlewareHookTarget::ProcessLocalScriptFunction: - return "(PLSF) "; - case EMiddlewareHookTarget::All: - return "(ALL) "; - default: - return "(UnknownTarget) "; - } - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/EventViewer.hpp b/cppmods/EventViewerMod/include/EventViewer.hpp deleted file mode 100644 index 3086d201..00000000 --- a/cppmods/EventViewerMod/include/EventViewer.hpp +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -// EventViewerMod: UE4SS mod entry + ImGui tab registration. -// -// UE4SS calls start_mod() (see dllmain.cpp) to create this mod instance. On Unreal init we register -// a new ImGui tab and route rendering to Client. - -#include -#include - -namespace RC::EventViewerMod -{ - class EventViewerMod : public CppUserModBase - { - public: - EventViewerMod(); - auto on_unreal_init() -> void override; - - private: - std::atomic_flag m_unreal_loaded{}; - }; -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/FilterCountRenderer.hpp b/cppmods/EventViewerMod/include/FilterCountRenderer.hpp deleted file mode 100644 index 2f23f21e..00000000 --- a/cppmods/EventViewerMod/include/FilterCountRenderer.hpp +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -namespace RC::EventViewerMod -{ - class FilterCountRenderer - { - public: - FilterCountRenderer() = default; - auto add() -> void; - auto render_and_reset(bool show_tooltip) -> void; - static auto render(size_t count, bool show_tooltip) -> void; - - private: - size_t m_count = 0; - }; -} // namespace RC::EventViewerMod \ No newline at end of file diff --git a/cppmods/EventViewerMod/include/HelpStrings.hpp b/cppmods/EventViewerMod/include/HelpStrings.hpp deleted file mode 100644 index 12df0dcb..00000000 --- a/cppmods/EventViewerMod/include/HelpStrings.hpp +++ /dev/null @@ -1,115 +0,0 @@ -#pragma once - -// clang-format off - -// EventViewerMod: Centralized UI help strings. -// -// Keeping these in one place makes it easier to tweak copy without touching the main UI code. - - -// EventViewerMod: Small user-facing help strings shown in the UI. - -#include - -#define FILTER_NOTE "Note that filters don't affect the stack depth; they will always be shown as-is to indicate "\ - "callers that may be potentially filtered out. You can right click any function while paused "\ - "and select \"Show Call Stack\" to see the unfiltered call stack for that function.\n\n"\ - "Additionally, filters are applied to both incoming calls and the current history of all calls "\ - "in all threads." - -namespace RC::EventViewerMod -{ - // From ImGui demo - inline static void HelpMarker(const char* desc) - { - ImGui::SameLine(); - ImGui::TextDisabled("(?)"); - if (ImGui::BeginItemTooltip()) - { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::TextUnformatted(desc); - ImGui::PopTextWrapPos(); - ImGui::EndTooltip(); - } - } - - struct HelpStrings - { - inline static constexpr auto HELP_TARGET = "Filter by which target should be shown.\n\n" - "Calls are prefixed by the target that called them with their initials.\n\n" - "A common path that many function calls follow/cause is ProcessEvent->ProcessInternal->ProcessLocalScriptFunction.\n\n" - FILTER_NOTE; - - inline static constexpr auto HELP_MODE = "Select the mode that the view should run in.\n\n" - "Stack: View a filtered stream (see below) of the live call stack for the selected Target.\n\n" - "Frequency: View a table of all of the functions called so far with the number of times " - "each function has been called."; - - inline static constexpr auto HELP_LIST_FILTER = "Whitelist/Blacklist filters both accept comma-separate lists of strings with case-insensitive matching.\n\n" - "Note that you can press enter to apply them when focused as well as press the button.\n\n" - "If you don't have any filters, then all calls that match your Target and \"Show Builtin Tick Functions\" settings " - "will be shown.\n\n" - "The caller's name and the function's name make up a call's name, in the form of \"Caller.Function\".\n\n" - "For a call to pass the whitelist filter, the call must have at least one of the comma-separated strings as a substring of its name. " - "Only having a whitelist filter is effectively the same as a normal search.\n\n" - "For a call to pass the blacklist filter, the call must not have any of the comma-separated strings as a substring of its name.\n\n" - "The whitelist filter applies before the blacklist filter, and each call must pass them in that order to be visible.\n\n" - FILTER_NOTE; - - inline static constexpr auto HELP_THREAD = "Select the thread to monitor.\n\n" - "This generally tries to default to the game thread, marked with \"(Game)\"."; - - inline static constexpr auto HELP_CLEAR = "Clears this thread's call stack and frequency counter."; - - inline static constexpr auto HELP_CLEAR_ALL = "Clears all call stacks and frequency counters. This also removes all known threads."; - - inline static constexpr auto HELP_SAVE = "Save the current filtered view to a text file.\n\n" - "This means that if the Mode is \"Call Stack\", the current call stack view " - "is saved, with all of the same filters applied, and if the Mode is \"Frequency\", " - "the table of frequencies is saved.\n\n" - "The file is saved in Mods\\EventViewerMod\\captures from the ue4ss directory."; - - inline static constexpr auto HELP_SAVE_ALL = "Save all views to a text file.\n\n" - "The file is saved in Mods\\EventViewerMod\\captures from the ue4ss directory."; - - inline static constexpr auto HELP_SHOW_BUILTIN_TICK = "Attempts to automatically filter out \"Tick\" and \"ReceiveTick\" functions from the view.\n\n" - "This is inherently faster than blacklisting \"Tick\", but may not block all \"Tick\" functions."; - - inline static constexpr auto HELP_CEMODAL_SHOW_FULL_CONTEXT = "Shows all function calls associated with the selected call.\n\n" - "Note that this whole view may differ from what you see in the normal Call Stack Mode since " - "this has no filters applied at all."; - - inline static constexpr auto HELP_CEMODAL_SAVE = "Saves this entry's call stack to a text file, respecting the \"Show Full Context\" setting.\n\n" - "The file is saved in Mods\\EventViewerMod\\captures from the ue4ss directory."; - - inline static constexpr auto HELP_MAX_MS_READ_TIME = "The max amount of time the ImGui thread will spend dequeuing calls in milliseconds in a frame.\n\n" - "Basically, the ImGui thread will dequeue up to \"Max Count Per Iteration\", until the queue is empty, or " - "until it hits this time limit."; - inline static constexpr auto HELP_MAX_COUNT_PER_ITERATION = "The max amount of calls that will be dequeued in a frame.\n\n" - "Basically, the ImGui thread will dequeue up to \"Max Count Per Iteration\", until the queue is empty, or " - "until it hits the time limit in \"Max MS Read Time\"."; - inline static constexpr auto HELP_QUEUE_PROFILE_VALUES = "Enqueue Avg (microseconds): The average time it takes a hook to enqueue a call. It should be as low as possible.\n\n" - "Dequeue Avg (microseconds): The average time it takes for the ImGui thread to dequeue a single call. This should be as low " - "as possible, though it's likely to increase over time.\n\n" - "Pending Avg (calls): The average amount of calls that are left in the queue after the ImGui thread finishes an iteration of" - "dequeuing. Should be 0 or very close to it.\n\n" - "Time Slot Exceeded Count: Amount of times the ImGui thread exceeded the time set in \"Max MS Read Time\"." - "Should be 0, or at least rarely moving."; - - inline static constexpr auto HELP_TEXT_VIRTUALIZATION_COUNT = "The total amount of calls that can be streamed into ImGui while running.\n\n" - "Changing this value will clear all threads.\n\n" - "When scrolling up while paused, you'll eventually run into a \"Load More\" button, " - "where the number of calls loaded doubles every time from this number.\n\n" - "This doesn't affect filtering, nor does it erase calls, it just controls how much is in " - "ImGui at a time (apart from clearing the threads when changing this value).\n\n" - "Higher values means making the \"Load More\" button appear later, but can" - "come in at a hefty performance cost."; - - inline static constexpr auto HELP_ADD_CALLER_AND_FUNC_NAME_WARNING = "WARNING: Adding Caller or Caller + Function name to a filter may block output completely in " - "Frequency Mode, since the Frequency Mode is only aware of the function name!"; - }; -} - -#undef FILTER_NOTE - -// clang-format on \ No newline at end of file diff --git a/cppmods/EventViewerMod/include/Middleware.hpp b/cppmods/EventViewerMod/include/Middleware.hpp deleted file mode 100644 index 9682f5f5..00000000 --- a/cppmods/EventViewerMod/include/Middleware.hpp +++ /dev/null @@ -1,195 +0,0 @@ -#pragma once - -// EventViewerMod: Capture backend (UE hook installation + queueing). -// -// Hooks multiple Unreal call sites and enqueues CallStackEntry objects into a lock-free -// moodycamel::ConcurrentQueue. The queue is drained on the ImGui thread by Client. -// -// Design constraints: -// - Hooks are extremely hot (ProcessEvent/ProcessInternal/ProcessLocalScriptFunction), so this code -// avoids allocations where possible and uses thread_local producer tokens. -// - Depth is unified across the hooked functions so nested / recursive call flows keep consistent -// indentation regardless of which hook produced a given entry. - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include -#include - -namespace RC::EventViewerMod -{ - class Middleware - { - public: - // By-reference singleton. - static auto GetInstance() -> Middleware&; - - // [Thread-Any] Enqueues info on a call. The hook_target is captured at enqueue-time. - auto enqueue(EMiddlewareHookTarget hook_target, RC::Unreal::UObject* context, RC::Unreal::UFunction* function) -> void; - - // [Thread-ImGui] Dequeues call info. - // max_ms - maximum wall time (ms) to spend dequeuing. - // max_count_per_iteration - max items pulled per bulk-dequeue. - // on_dequeue - receives the dequeued entry by rvalue-ref (move). - auto dequeue(uint16_t max_ms, uint16_t max_count_per_iteration, const std::function& on_dequeue) -> void; - - // [Thread-ImGui] Pauses stream, removes hooks. Also drains queue. - auto stop() -> bool; - - // [Thread-ImGui] - [[nodiscard]] auto is_paused() const -> bool; - - // [Thread-ImGui] Resumes stream by installing hooks. - auto start() -> bool; - - // [Thread-ImGui] - auto set_imgui_thread_id(std::thread::id id) -> void; - - // [Thread-ImGui] - [[nodiscard]] auto get_imgui_thread_id() const -> std::thread::id; - - // [Thread-ImGui] - [[nodiscard]] auto get_average_enqueue_time() const -> double; - - // [Thread-ImGui] - [[nodiscard]] auto get_average_dequeue_time() const -> double; - - ~Middleware(); - - private: - Middleware(); - - auto assert_on_imgui_thread() const -> void; - [[nodiscard]] auto is_tick_fn(const RC::Unreal::UFunction* fn) const -> bool; - auto stop_impl(bool do_assert) -> bool; - - template - struct HookController - { - Unreal::Hook::GlobalCallbackId (*register_prehook_fn)(CallbackType, Unreal::Hook::FCallbackOptions){}; - Unreal::Hook::GlobalCallbackId (*register_posthook_fn)(CallbackType, Unreal::Hook::FCallbackOptions){}; - CallbackType m_pre_callback{}; - CallbackType m_post_callback{}; - Unreal::Hook::GlobalCallbackId m_prehook_id = Unreal::Hook::ERROR_ID; - Unreal::Hook::GlobalCallbackId m_posthook_id = Unreal::Hook::ERROR_ID; - - auto install_prehook() -> bool; - auto install_posthook() -> bool; - auto unhook() -> void; - auto is_hooked() const -> bool; - - inline static Unreal::Hook::FCallbackOptions m_cb_options{false, true, STR("EventViewer"), STR("CallStackMonitor")}; - }; - - private: - HookController m_pe_controller{}; - HookController m_pi_controller{}; - HookController m_plsf_controller{}; - - bool m_paused = true; - - std::thread::id m_imgui_id{}; - - // Queue - moodycamel::ConcurrentQueue m_queue{}; - moodycamel::ConsumerToken m_imgui_consumer_token{m_queue}; - std::vector m_buffer{}; - - // Detected tick functions - std::unordered_set m_tick_fns{}; - - inline static thread_local uint32_t m_depth = 0; - std::atomic_uint64_t m_depth_reset_counter = 0; - - std::atomic_flag m_allow_queue; - }; - - template - auto Middleware::HookController::install_prehook() -> bool - { - if (m_prehook_id != RC::Unreal::Hook::ERROR_ID) - { - Output::send(STR("[EventViewerMod] Failed to install prehook because it's already installed!")); - return false; - } - - if (!register_prehook_fn) - { - Output::send(STR("[EventViewerMod] Failed to install prehook because register function is unknown! Is FName.toString known?")); - return false; - } - - m_prehook_id = register_prehook_fn(m_pre_callback, m_cb_options); - - if (m_prehook_id == RC::Unreal::Hook::ERROR_ID) - { - Output::send(STR("[EventViewerMod] Failed to install prehook!")); - return false; - } - - return true; - } - - template - auto Middleware::HookController::install_posthook() -> bool - { - if (m_posthook_id != RC::Unreal::Hook::ERROR_ID) - { - Output::send(STR("[EventViewerMod] Failed to install posthook because it's already installed!")); - return false; - } - - if (!register_posthook_fn) - { - Output::send(STR("[EventViewerMod] Failed to install posthook because register function is unknown! Is FName.toString known?")); - return false; - } - - m_posthook_id = register_posthook_fn(m_post_callback, m_cb_options); - - if (m_posthook_id == RC::Unreal::Hook::ERROR_ID) - { - Output::send(STR("[EventViewerMod] Failed to install posthook!")); - return false; - } - - return true; - } - - template - auto Middleware::HookController::unhook() -> void - { - if (m_prehook_id == RC::Unreal::Hook::ERROR_ID && m_posthook_id == RC::Unreal::Hook::ERROR_ID) - { - Output::send(STR("[EventViewerMod] Failed to remove hooks because it's not active!")); - return; - } - - if (!RC::Unreal::Hook::UnregisterCallback(m_prehook_id)) - { - Output::send(STR("[EventViewerMod] Failed to unregister prehook!")); - } - - if (!RC::Unreal::Hook::UnregisterCallback(m_posthook_id)) - { - Output::send(STR("[EventViewerMod] Failed to unregister posthook!")); - } - - m_prehook_id = m_posthook_id = RC::Unreal::Hook::ERROR_ID; - } - - template - auto Middleware::HookController::is_hooked() const -> bool - { - return (m_prehook_id != RC::Unreal::Hook::ERROR_ID && m_posthook_id != RC::Unreal::Hook::ERROR_ID); - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/QueueProfiler.hpp b/cppmods/EventViewerMod/include/QueueProfiler.hpp deleted file mode 100644 index 33e1337d..00000000 --- a/cppmods/EventViewerMod/include/QueueProfiler.hpp +++ /dev/null @@ -1,92 +0,0 @@ -#pragma once - -// EventViewerMod: Lightweight queue timing instrumentation. -// -// This is not a general-purpose profiler; it's a small helper to measure enqueue/dequeue costs -// and queue backlog under real game load. Numbers are aggregated and displayed in the UI. - -#include -#include - -class QueueProfiler -{ - public: - static void BeginEnqueue() - { - enqueue_count.fetch_add(1, std::memory_order_relaxed); - enqueue_start = std::chrono::high_resolution_clock::now(); - } - - static void EndEnqueue() - { - enqueue_total.fetch_add(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - enqueue_start).count(), - std::memory_order_relaxed); - } - - static double GetEnqueueAverage() - { - return (enqueue_total.load(std::memory_order_relaxed) / static_cast(enqueue_count.load(std::memory_order_relaxed))); - } - - static void BeginDequeue() - { - dequeue_count.fetch_add(1, std::memory_order_relaxed); - dequeue_start = std::chrono::high_resolution_clock::now(); - } - - static void EndDequeue() - { - const auto now = std::chrono::high_resolution_clock::now(); - dequeue_total.fetch_add(std::chrono::duration_cast(now - dequeue_start).count(), std::memory_order_relaxed); - } - - static double GetDequeueAverage() - { - return (dequeue_total.load(std::memory_order_relaxed) / static_cast(dequeue_count.load(std::memory_order_relaxed))); - } - - static void AddPendingCount(const int64_t pending) - { - pending_count.fetch_add(1, std::memory_order_relaxed); - pending_total.fetch_add(pending, std::memory_order_relaxed); - } - - static void AddTimeExceededCount() - { - time_exceeded_total.fetch_add(1, std::memory_order_relaxed); - } - - static uint64_t GetTimeExceededCount() - { - return time_exceeded_total.load(std::memory_order_relaxed); - } - - static double GetPendingAverage() - { - return (pending_total.load(std::memory_order_relaxed) / static_cast(pending_count.load(std::memory_order_relaxed))); - } - - static void Reset() - { - enqueue_count.store(0, std::memory_order_release); - enqueue_total.store(0, std::memory_order_release); - dequeue_count.store(0, std::memory_order_release); - dequeue_total.store(0, std::memory_order_release); - pending_total.store(0, std::memory_order_release); - pending_count.store(0, std::memory_order_release); - time_exceeded_total.store(0, std::memory_order_release); - } - - private: - inline static std::atomic_int64_t enqueue_total = 0; - inline static std::atomic_int64_t dequeue_total = 0; - inline static std::atomic_int64_t pending_total = 0; - inline static std::atomic_int64_t time_exceeded_total = 0; - - inline static std::atomic_int64_t enqueue_count = 0; - inline static std::atomic_int64_t dequeue_count = 0; - inline static std::atomic_int64_t pending_count = 0; - - inline static thread_local std::chrono::time_point enqueue_start; - inline static thread_local std::chrono::time_point dequeue_start; -}; \ No newline at end of file diff --git a/cppmods/EventViewerMod/include/StringPool.hpp b/cppmods/EventViewerMod/include/StringPool.hpp deleted file mode 100644 index 7d550974..00000000 --- a/cppmods/EventViewerMod/include/StringPool.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -// EventViewerMod: String interning + hashing. -// -// Unreal provides stable numeric identifiers for names (ComparisonIndex). StringPool uses those to: -// - Deduplicate storage for function/caller strings. -// - Provide fast comparisons via hashes (avoid expensive string comparisons in hot paths). -// - Cache both original and lowercased strings for case-insensitive filtering. -// -// The pool is designed for “grow-only” lifetime during a session; string_views returned from the pool -// stay valid as long as the underlying storage isn't cleared. - -#include -#include -#include -#include -#include - -#include - -#include - -namespace RC::EventViewerMod -{ - class StringPool - { - public: - // Returns string_views owned by the pool: - // - full_name / function_name for display - // - lower_cased_* for case-insensitive filtering - // - function_hash for fast equality on function identity - auto get_strings(RC::Unreal::UObject* caller, RC::Unreal::UFunction* function) -> AllNameStringViews; - - // Returns path name of a function, same as calling GetPathName but with a string view instead. - auto get_path_name(uint32_t function_hash) -> std::string_view; - - // Clears the string pool. Currently unused, but may be useful if games ever start recycling FName indices. - // Note that this will invalidate any string_views acquired through the getters, so the ImGui views should be cleared before doing this - // in the same frame. - auto clear() -> void; - - static auto GetInstance() -> StringPool&; - - StringPool(const StringPool& Other) = delete; - StringPool(StringPool&& Other) noexcept = delete; - StringPool& operator=(const StringPool& Other) = delete; - StringPool& operator=(StringPool&& Other) noexcept = delete; - - private: - StringPool() = default; - - struct StringInfo - { - size_t function_begin; - std::string full_name; - std::string lower_cased_full_name; - }; - - // TODO replace with better concurrent hash map solution, though not too important for now - std::unordered_map m_main_pool; - // hashed by function->GetComparisonIndex and caller->GetComparisonIndex - std::unordered_map m_path_pool; // hashed by function->GetComparisonIndex - std::shared_mutex m_mutex; - }; -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/include/Structs.hpp b/cppmods/EventViewerMod/include/Structs.hpp deleted file mode 100644 index 0c395822..00000000 --- a/cppmods/EventViewerMod/include/Structs.hpp +++ /dev/null @@ -1,156 +0,0 @@ -#pragma once - -// EventViewerMod: Data model + UI state. -// -// This header defines: -// - String-view “bundles” (FunctionNameStringViews / AllNameStringViews) returned by StringPool. -// - Capture entries (CallStackEntry, CallFrequencyEntry). -// - Per-thread buffers and persistent UI state. -// -// Many fields are intentionally plain and public: these objects are moved through the queue and -// stored in large vectors/lists, so keeping them trivially movable matters. - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include - -namespace RC::EventViewerMod -{ - static_assert(std::is_same_v, - "EventViewerMod expects StringType to be std::wstring for ImGui encoding; needs refactor if that changes."); - - struct FunctionNameStringViews - { - // Stable identifier for the UFunction across frames. - // Used for fast frequency aggregation (no string compares in the hot path). - uint32_t function_hash; // FName.ComparisonIndex of UFunction - std::string_view function_name; - std::string_view lower_cased_function_name; - }; - - struct AllNameStringViews : FunctionNameStringViews - { - std::string_view full_name; - std::string_view lower_cased_full_name; - // Composite key for (caller, function). This avoids per-frame string concatenation. - // Layout: UFunction ComparisonIndex in upper 32 bits, caller UObject ComparisonIndex in lower 32 bits. - uint64_t full_hash; - }; - - // Note: these types are intentionally cheap-to-move so they can be passed through - // moodycamel::ConcurrentQueue by value (high throughput, minimal allocator churn). - struct EntryBase - { - EntryBase() = default; - explicit EntryBase(bool is_tick); - - bool is_tick = false; - - // Cached visibility bit (filters + tick toggle). - // Important: depth/indent is NOT recomputed when entries are hidden; callers may be hidden while - // deeper frames remain visible, to preserve the true call depth. - bool is_disabled = false; - }; - - // Stores both original-case and lower-cased string views (for case-insensitive filtering). - struct CallStackEntry : EntryBase, AllNameStringViews - { - CallStackEntry() = default; - CallStackEntry(EMiddlewareHookTarget hook_target, const AllNameStringViews& strings, uint32_t depth, std::thread::id thread_id, bool is_tick); - - auto render(int indent_delta, ECallStackEntryRenderFlags_ flags = ECallStackEntryRenderFlags_None) const -> void; - - // Provides a copy of the entry's key string fields. Don't use with ImGui, only use for logging/saving. - // Use the string_views for ImGui since they utilize the string pool. - auto to_string_with_prefix() const -> std::wstring; - - // Which hook produced this entry. This is captured at enqueue-time and later used as a *view filter* - // ("All" shows everything; other targets show only matching entries). - EMiddlewareHookTarget hook_target = EMiddlewareHookTarget::All; - - // Unified depth counter shared by all hooks; PE can call PI which can call PLSF, etc. - uint32_t depth = 0; - std::thread::id thread_id{}; - - private: - auto render_indents(int indent_delta) const -> void; - auto render_support_menus(ECallStackEntryRenderFlags_ flags) const -> void; - }; - - // Stores both original-case and lower-cased function name views (for case-insensitive filtering). - struct CallFrequencyEntry : EntryBase, FunctionNameStringViews - { - CallFrequencyEntry() = default; - CallFrequencyEntry(const FunctionNameStringViews& strings, bool is_tick); - auto render(ECallFrequencyEntryRenderFlags_ flags) const -> void; - uint64_t frequency = 1; - - // OR'd EMiddlewareHookTarget values that have invoked this function so far. - // This is used for filtering when a specific hook target is selected. - uint32_t source_flags = 0; - - private: - auto render_support_menus() const -> void; - }; - - struct ThreadInfo - { - explicit ThreadInfo(std::thread::id thread_id); - - const std::thread::id thread_id; - const bool is_game_thread; - - // High-throughput capture history (fast filtering/search due to contiguous storage). - std::vector call_stack; - - // Set of entries that should be rendered, respecting the client's text_temp_virtualization_count - std::set call_stack_render_set{}; - - // Aggregated frequency view; list allows O(1) reordering via splice without shifting elements. - std::list call_frequencies; - - auto id_string() -> const char*; - auto clear() -> void; - - private: - std::string m_id_string; - }; - - struct UIState - { - bool enabled = false; // [Savable] [Thread-ImGui] - bool started = false; // [Thread-ImGui] - bool show_tick = true; // [Savable] [Thread-ImGui] - bool disable_indent_colors = false; // [Savable] [Thread-ImGui] - bool thread_explicitly_chosen = false; // [Thread-ImGui] User selected a thread - bool thread_implicitly_set = false; // [Thread-ImGui] System set thread to game thread if not explicit - bool show_filter_counts = true; // [Savable] [Thread-ImGui] - EMiddlewareHookTarget hook_target = EMiddlewareHookTarget::All; // [Savable] [Thread-ImGui] - EMode mode = EMode::Stack; // [Savable] [Thread-ImGui] - uint16_t dequeue_max_ms = 10; // [Savable] [Thread-ImGui] - uint16_t text_virtualization_count = 100; // [Savable] [Thread-ImGui] - uint64_t text_temp_virtualization_count = text_virtualization_count; // [Thread-Imgui] - uint32_t dequeue_max_count = 100000; // [Savable] [Thread-ImGui] - std::string blacklist; // [Savable] [Thread-ImGui] - std::vector blacklist_tokens; // [Thread-ImGui] (lower-cased tokens) - std::string whitelist; // [Savable] [Thread-ImGui] - std::vector whitelist_tokens; // [Thread-ImGui] (lower-cased tokens) - std::vector threads{}; // [Thread-ImGui] - int current_thread = 0; // [Thread-ImGui] - std::atomic_flag needs_save = ATOMIC_FLAG_INIT; // [Thread-Any] - std::string last_save_path; // [Thread-ImGui] - }; -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/Client.cpp b/cppmods/EventViewerMod/src/Client.cpp deleted file mode 100644 index 380f634f..00000000 --- a/cppmods/EventViewerMod/src/Client.cpp +++ /dev/null @@ -1,1099 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -#include -#include -#include -#include -#include - -#include -#include - -// EventViewerMod: UI renderer and history consumer. -// -// This file drives the ImGui tab. High-level flow: -// 1) Draw config + view controls. -// 2) Dequeue a bounded amount of entries from Middleware each frame. -// 3) Merge new entries into per-thread histories (stack + frequency). -// 4) Apply view filters (hook target selection, whitelist/blacklist, tick toggle). -// -// Important: -// - The hook target combo is a *display filter only*. Depth is always computed by Middleware and -// remains unchanged even if callers are hidden. -// - Filtering is case-insensitive by comparing lower-cased strings (see to_lower_ascii_copy()). -// - -// Returns lower-cased tokens (copied strings). -static std::vector split_string_by_comma(const std::string& string) -{ - std::vector result; - if (string.empty()) - { - return result; - } - - const std::string_view sv{string}; - size_t start = 0; - - auto trim = [](std::string_view v) -> std::string_view { - const auto leading = v.find_first_not_of(" \t\n\r\f\v"); - if (leading == std::string_view::npos) - { - return {}; - } - v.remove_prefix(leading); - - const auto trailing = v.find_last_not_of(" \t\n\r\f\v"); - if (trailing == std::string_view::npos) - { - return {}; - } - v = v.substr(0, trailing + 1); - return v; - }; - - while (start <= sv.size()) - { - size_t end = sv.find(',', start); - if (end == std::string_view::npos) - { - end = sv.size(); - } - - auto token = trim(sv.substr(start, end - start)); - if (!token.empty()) - { - result.emplace_back(to_lower_case(token)); - } - - if (end == sv.size()) - { - break; - } - start = end + 1; - } - - return result; -} - -namespace RC::EventViewerMod -{ - using namespace std::literals::string_literals; - - Client::Client() : m_middleware(Middleware::GetInstance()) - { - const auto wd = std::filesystem::path{StringType{UE4SSProgram::get_program().get_working_directory()}}; - const auto mod_root = wd / "Mods" / "EventViewerMod"; - - m_cfg_path = mod_root / "config" / "settings.json"; - m_dump_dir = mod_root / "captures"; - - std::error_code ec; - std::filesystem::create_directories(m_cfg_path.parent_path(), ec); - std::filesystem::create_directories(m_dump_dir, ec); - - load_state(); - } - - auto Client::render() -> void - { - // Ensure middleware knows the correct ImGui thread - if (!m_imgui_thread_id_set) - { - m_middleware.set_imgui_thread_id(std::this_thread::get_id()); - m_imgui_thread_id_set = true; - } - - const auto saved = check_save_request(); - - if (ImGui::Checkbox("Enable", &m_state.enabled)) - { - request_save_state(); - - if (m_state.enabled) - { - // enabling - m_middleware.set_imgui_thread_id(std::this_thread::get_id()); - m_imgui_thread_id_set = true; - QueueProfiler::Reset(); - } - else - { - // disabling - m_middleware.stop(); - m_state.started = false; - m_state.needs_save.clear(std::memory_order_release); - clear_threads(); - if (!saved) - { - save_state(); - } - QueueProfiler::Reset(); - return; - } - } - - if (!m_state.enabled) return; - - render_cfg(); - - if (ImGui::TreeNode("Performance Options")) - { - render_perf_opts(); - ImGui::TreePop(); - } - - dequeue(); - - render_view(); - - if (ImGui::BeginPopupModal("Saved File", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::Text("Saved file to %s", m_state.last_save_path.c_str()); - ImGui::PopTextWrapPos(); - if (ImGui::Button("OK")) ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - } - } - - auto Client::render_cfg() -> void - { - int hook_target_idx = 0; - for (int i = 0; i < EMiddlewareHookTarget_Size; ++i) - { - if (EMiddlewareHookTarget_ValueArray[i] == m_state.hook_target) - { - hook_target_idx = i; - break; - } - } - - if (combo_with_flags("Target", &hook_target_idx, EMiddlewareHookTarget_NameArray, EMiddlewareHookTarget_Size, ImGuiComboFlags_WidthFitPreview)) - { - request_save_state(); - m_state.hook_target = EMiddlewareHookTarget_ValueArray[hook_target_idx]; - - // Hook target is an implicit filter for the stack view. - // Rebuild per-thread render sets immediately so the UI reflects the new selection. - // (Disabled state is controlled by whitelist/blacklist/tick and is handled elsewhere.) - for (auto& thread : m_state.threads) - { - thread.call_stack_render_set.clear(); - resize_render_set(thread, m_state.text_temp_virtualization_count); - } - } - HelpMarker(HelpStrings::HELP_TARGET); - ImGui::SameLine(); - - if (combo_with_flags("Mode", reinterpret_cast(&m_state.mode), EMode_NameArray, EMode_Size, ImGuiComboFlags_WidthFitPreview)) - { - request_save_state(); - } - HelpMarker(HelpStrings::HELP_MODE); - - bool whitelist_changed = false; - bool blacklist_changed = false; - bool tick_changed = false; - - // whitelist - const auto wl_input_changed = ImGui::InputText("Whitelist", &m_state.whitelist, ImGuiInputTextFlags_ElideLeft | ImGuiInputTextFlags_EnterReturnsTrue); - ImGui::SameLine(); - if (wl_input_changed || ImGui::Button("Apply##Whitelist")) - { - whitelist_changed = true; - request_save_state(); - } - ImGui::SameLine(); - if (ImGui::Button("Clear##Whitelist")) - { - if (!m_state.whitelist.empty()) - { - m_state.whitelist.clear(); - whitelist_changed = true; - request_save_state(); - } - } - HelpMarker(HelpStrings::HELP_LIST_FILTER); - - // blacklist - const auto bl_input_changed = ImGui::InputText("Blacklist", &m_state.blacklist, ImGuiInputTextFlags_ElideLeft | ImGuiInputTextFlags_EnterReturnsTrue); - ImGui::SameLine(); - if (bl_input_changed || ImGui::Button("Apply##Blacklist")) - { - blacklist_changed = true; - request_save_state(); - } - ImGui::SameLine(); - if (ImGui::Button("Clear##Blacklist")) - { - if (!m_state.blacklist.empty()) - { - m_state.blacklist.clear(); - blacklist_changed = true; - request_save_state(); - } - } - auto& threads = m_state.threads; - - if (!threads.empty()) - { - if (m_state.current_thread < 0) - { - m_state.current_thread = 0; - } - if (static_cast(m_state.current_thread) >= threads.size()) - { - m_state.current_thread = static_cast(threads.size() - 1); - } - - ThreadInfo* game_thread = nullptr; - if (ImGui::BeginCombo("Thread", threads[m_state.current_thread].id_string(), ImGuiComboFlags_WidthFitPreview)) - { - for (size_t idx = 0; idx < threads.size(); ++idx) - { - const bool selected = static_cast(idx) == m_state.current_thread; - auto& thread = threads[idx]; - if (ImGui::Selectable(thread.id_string(), selected)) - { - m_state.thread_explicitly_chosen = true; - m_state.current_thread = static_cast(idx); - } - if (selected) - { - ImGui::SetItemDefaultFocus(); - } - if (thread.is_game_thread) - { - game_thread = &thread; - } - } - ImGui::EndCombo(); - } - else if (!m_state.thread_implicitly_set) - { - for (auto& thread : threads) - { - if (thread.is_game_thread) game_thread = &thread; - } - - if (game_thread && !m_state.thread_explicitly_chosen) - { - m_state.current_thread = static_cast(game_thread - threads.data()); - ImGui::SetItemDefaultFocus(); - m_state.thread_implicitly_set = true; - } - } - HelpMarker(HelpStrings::HELP_THREAD); - } - - auto save_mode = ESaveMode::none; - - // controls - if (ImGui::Button(m_state.started ? "Stop" : "Start")) - { - m_state.started = !m_state.started; - if (m_state.started) - { - m_middleware.start(); - m_state.text_temp_virtualization_count = m_state.text_virtualization_count; - // shrink render sets - for (auto& thread : threads) - { - resize_render_set(thread, m_state.text_temp_virtualization_count); - } - } - else - { - m_middleware.stop(); - } - QueueProfiler::Reset(); - } - - ImGui::SameLine(); - if (ImGui::Button("Clear##CurrentThread") && !threads.empty()) - { - auto& thread = threads[m_state.current_thread]; - thread.clear(); - } - HelpMarker(HelpStrings::HELP_CLEAR); - ImGui::SameLine(); - if (ImGui::Button("Clear All##AllThreads") && !threads.empty()) - { - clear_threads(); - } - HelpMarker(HelpStrings::HELP_CLEAR_ALL); - ImGui::SameLine(); - if (ImGui::Button("Save##Current")) - { - save_mode = ESaveMode::current; - } - HelpMarker(HelpStrings::HELP_SAVE); - ImGui::SameLine(); - if (ImGui::Button("Save All##All")) - { - save_mode = ESaveMode::all; - } - HelpMarker(HelpStrings::HELP_SAVE_ALL); - ImGui::SameLine(); - if (ImGui::Checkbox("Show Builtin Tick Functions", &m_state.show_tick)) - { - tick_changed = true; - request_save_state(); - } - HelpMarker(HelpStrings::HELP_SHOW_BUILTIN_TICK); - ImGui::SameLine(); - if (ImGui::Checkbox("Disable Indent Colors", &m_state.disable_indent_colors)) - { - request_save_state(); - } - ImGui::SameLine(); - if (ImGui::Checkbox("Show Filter Counts", &m_state.show_filter_counts)) - { - request_save_state(); - } - - apply_filters_to_history(whitelist_changed, blacklist_changed, tick_changed); - save(save_mode); - } - - auto Client::render_perf_opts() -> void - { - static uint16_t step = 1; - if (ImGui::InputScalar("Max MS Read Time", ImGuiDataType_U16, &m_state.dequeue_max_ms, &step, 0, 0)) - { - request_save_state(); - if (!m_state.dequeue_max_ms) - { - m_state.dequeue_max_ms = 1; - } - } - HelpMarker(HelpStrings::HELP_MAX_MS_READ_TIME); - - if (ImGui::InputScalar("Max Count Per Iteration", ImGuiDataType_U32, &m_state.dequeue_max_count, &step)) - { - request_save_state(); - if (!m_state.dequeue_max_count) - { - m_state.dequeue_max_count = 1; - } - } - HelpMarker(HelpStrings::HELP_MAX_COUNT_PER_ITERATION); - - if (ImGui::InputScalar("Text Virtualization Count", ImGuiDataType_U16, &m_state.text_virtualization_count, &step)) - { - request_save_state(); - if (!m_state.text_virtualization_count) - { - m_state.text_virtualization_count = 1; - } - m_state.text_temp_virtualization_count = m_state.text_virtualization_count; - clear_threads(); - } - HelpMarker(HelpStrings::HELP_TEXT_VIRTUALIZATION_COUNT); - - ImGui::Text("Enqueue Avg: %f Dequeue Avg: %f Pending Avg: %f Time Slot Exceeded Count: %llu", - QueueProfiler::GetEnqueueAverage(), - QueueProfiler::GetDequeueAverage(), - QueueProfiler::GetPendingAverage(), - QueueProfiler::GetTimeExceededCount()); - HelpMarker(HelpStrings::HELP_QUEUE_PROFILE_VALUES); - } - - auto Client::render_view() -> void - { - auto& threads = m_state.threads; - if (threads.empty()) - { - return; - } - - if (m_state.current_thread < 0) - { - m_state.current_thread = 0; - } - if (static_cast(m_state.current_thread) >= threads.size()) - { - m_state.current_thread = static_cast(threads.size() - 1); - } - - auto& thread = threads[m_state.current_thread]; - - const auto selected_flags = static_cast(m_state.hook_target); - - auto area = ImGui::GetContentRegionAvail(); - auto& padding = ImGui::GetStyle().WindowPadding; - auto& scroll_size = ImGui::GetStyle().ScrollbarSize; - area.y -= ((padding.y + scroll_size) * 2); - area.x -= (padding.x + scroll_size); - ImGui::BeginChild("##view", area, ImGuiChildFlags_Borders | ImGuiChildFlags_FrameStyle | ImGuiChildFlags_AutoResizeY, ImGuiWindowFlags_HorizontalScrollbar); - if (m_state.mode == EMode::Stack) - { - if (thread.call_stack.empty()) - { - ImGui::EndChild(); - return; - } - - int prev_depth = 0; - bool have_prev = false; - int current_indent = 0; - int id = 0; - uint8_t entry_flags = 0; - if (!m_state.disable_indent_colors) entry_flags |= ECallStackEntryRenderFlags_IndentColors; - if (!m_state.started) entry_flags |= ECallStackEntryRenderFlags_WithSupportMenusCallStackModal; - - bool needs_scroll_here = false; - if (!m_state.started && thread.call_stack_render_set.size() == m_state.text_temp_virtualization_count) - { - if (ImGui::Button("Load More...")) - { - m_state.text_temp_virtualization_count *= 2; - // expand set - resize_render_set(thread, m_state.text_temp_virtualization_count); - needs_scroll_here = true; - } - } - - const bool show_filter_counts = m_state.show_filter_counts; - const auto set_begin = thread.call_stack_render_set.begin(); - for (auto entry_it = set_begin; entry_it != thread.call_stack_render_set.end(); ++entry_it) - { - auto& entry = thread.call_stack[*entry_it]; - if (show_filter_counts && entry_it != set_begin) [[likely]] - { - auto before_entry_it = entry_it; - --before_entry_it; - const auto gap = (*entry_it) - (*before_entry_it); - if (gap > 1) - { - FilterCountRenderer::render(gap - 1, !m_state.started); - } - } - - const int depth = static_cast(entry.depth); - const int delta = have_prev ? (depth - prev_depth) : depth; - ImGui::PushID(id++); - entry.render(delta, static_cast(entry_flags)); - ImGui::PopID(); - current_indent += delta; - prev_depth = depth; - have_prev = true; - } - - // Reset indent state for safety. - while (current_indent > 0) - { - ImGui::Unindent(); - --current_indent; - } - - // if (show_filter_counts) m_filter_count_renderer.render_and_reset(!m_state.show_filter_counts); - - if (m_state.started || needs_scroll_here) ImGui::SetScrollHereY(1.0f); - } - - else - { - if (ImGui::BeginTable("##frequency", 2, ImGuiTableFlags_ScrollY | ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersV)) - { - int id = 0; - for (const auto& entry : thread.call_frequencies) - { - if ((entry.source_flags & selected_flags) == 0) - { - continue; - } - - if (entry.is_disabled) - { - continue; - } - ImGui::PushID(id++); - entry.render(m_state.started ? ECallFrequencyEntryRenderFlags_None : ECallFrequencyEntryRenderFlags_WithSupportMenus); - ImGui::PopID(); - } - - ImGui::EndTable(); - } - } - - ImGui::EndChild(); - - if (m_entry_call_stack_renderer) - { - if (!m_entry_call_stack_renderer->render()) - { - m_entry_call_stack_renderer = nullptr; - } - } - } - - auto Client::combo_with_flags(const char* label, int* current_item, const char* const items[], const int items_count, const ImGuiComboFlags_ flags) -> bool - { - bool changed = false; - if (ImGui::BeginCombo(label, items[*current_item], flags)) - { - for (int i = 0; i < items_count; ++i) - { - const auto is_selected = i == *current_item; - if (ImGui::Selectable(items[i], is_selected)) - { - *current_item = i; - changed = true; - } - if (is_selected) ImGui::SetItemDefaultFocus(); - } - ImGui::EndCombo(); - } - return changed; - } - - auto Client::save_state() -> void - { - std::error_code ec; - std::filesystem::create_directories(m_cfg_path.parent_path(), ec); - - std::unordered_map state_map; - state_map.emplace("Enabled", std::to_string(m_state.enabled)); - state_map.emplace("ShowTick", std::to_string(m_state.show_tick)); - int hook_target_idx = 0; - for (int i = 0; i < EMiddlewareHookTarget_Size; ++i) - { - if (EMiddlewareHookTarget_ValueArray[i] == m_state.hook_target) - { - hook_target_idx = i; - break; - } - } - state_map.emplace("HookTarget", std::to_string(hook_target_idx)); - state_map.emplace("Mode", std::to_string(static_cast(m_state.mode))); - state_map.emplace("DequeueMaxMs", std::to_string(m_state.dequeue_max_ms)); - state_map.emplace("DequeueMaxCount", std::to_string(m_state.dequeue_max_count)); - state_map.emplace("Whitelist", m_state.whitelist); - state_map.emplace("Blacklist", m_state.blacklist); - state_map.emplace("DisableIndentColors", std::to_string(m_state.disable_indent_colors)); - state_map.emplace("TextVirtualizationCount", std::to_string(m_state.text_virtualization_count)); - state_map.emplace("ShowFilterCounts", std::to_string(m_state.show_filter_counts)); - (void)glz::write_file_json(state_map, m_cfg_path.string(), std::string{}); - } - - auto Client::load_state() -> void - { - if (!std::filesystem::exists(m_cfg_path) || !std::filesystem::is_regular_file(m_cfg_path)) - { - return; - } - - std::unordered_map state_map{}; - auto ec = glz::read_file_json(state_map, m_cfg_path.string(), std::string{}); - if (ec.ec != glz::error_code::none) - { - return; - } - - try - { - m_state.enabled = state_map.at("Enabled") != "0"; - m_state.show_tick = state_map.at("ShowTick") != "0"; - m_state.disable_indent_colors = state_map.at("DisableIndentColors") != "0"; - const int hook_target_idx = std::stoi(state_map.at("HookTarget")); - if (hook_target_idx >= 0 && hook_target_idx < EMiddlewareHookTarget_Size) - { - m_state.hook_target = EMiddlewareHookTarget_ValueArray[hook_target_idx]; - } - else - { - m_state.hook_target = EMiddlewareHookTarget::All; - } - m_state.mode = static_cast(std::stoi(state_map.at("Mode"))); - m_state.dequeue_max_ms = static_cast(std::stoi(state_map.at("DequeueMaxMs"))); - m_state.dequeue_max_count = static_cast(std::stoul(state_map.at("DequeueMaxCount"))); - m_state.whitelist = state_map.at("Whitelist"); - m_state.blacklist = state_map.at("Blacklist"); - m_state.whitelist_tokens = split_string_by_comma(m_state.whitelist); - m_state.blacklist_tokens = split_string_by_comma(m_state.blacklist); - m_state.text_virtualization_count = static_cast(std::stoi(state_map.at("TextVirtualizationCount"))); - m_state.text_temp_virtualization_count = m_state.text_virtualization_count; - m_state.show_filter_counts = state_map.at("ShowFilterCounts") != "0"; - clear_threads(); // just to be safe, since if text_virtualization_count changes while scrolling it could cause problems - } - catch (...) - { - Output::send(STR("[EventViewerMod] Failed to load state from file due to exception!")); - } - } - - auto Client::check_save_request() -> bool - { - if (m_state.needs_save.test(std::memory_order_acquire)) - { - save_state(); - m_state.needs_save.clear(std::memory_order_release); - return true; - } - return false; - } - - auto Client::apply_filters_to_history(const bool whitelist_changed, const bool blacklist_changed, const bool tick_changed) -> void - { - if (!(whitelist_changed || blacklist_changed || tick_changed)) - { - return; - } - if (whitelist_changed) - { - m_state.whitelist_tokens = split_string_by_comma(m_state.whitelist); - } - if (blacklist_changed) - { - m_state.blacklist_tokens = split_string_by_comma(m_state.blacklist); - } - - // Recompute disabled state for all history. This only runs when the user changes filters/tick setting. - const bool show_tick = m_state.show_tick; - - for (auto& thread : m_state.threads) - { - // Stack history can get very large; leverage parallel execution on random-access iterators. - std::for_each(std::execution::par_unseq, thread.call_stack.begin(), thread.call_stack.end(), [this, show_tick](CallStackEntry& entry) { - entry.is_disabled = (entry.is_tick && !show_tick) || !passes_filters(entry.lower_cased_full_name); - }); - - // Frequency view is smaller and is a list (non-random-access). - for (auto& entry : thread.call_frequencies) - { - entry.is_disabled = (entry.is_tick && !show_tick) || !passes_filters(entry.lower_cased_function_name); - } - - // Recalculate render set - thread.call_stack_render_set.clear(); - resize_render_set(thread, m_state.text_temp_virtualization_count); - } - } - - auto Client::dequeue() -> void - { - if (!m_state.enabled || !m_state.started) - { - return; - } - - m_middleware.dequeue(m_state.dequeue_max_ms, m_state.dequeue_max_count, [this](CallStackEntry&& entry) { - // Thread lookup/creation (unified across hook targets). - auto& threads = m_state.threads; - - const auto entry_thread = entry.thread_id; - auto thread_it = std::ranges::find_if(threads, [&entry_thread](const ThreadInfo& info) { - return info.thread_id == entry_thread; - }); - - ThreadInfo* thread_ptr = nullptr; - if (thread_it != threads.end()) - { - thread_ptr = &(*thread_it); - } - else - { - thread_ptr = &threads.emplace_back(entry_thread); - if (m_state.current_thread < 0) - { - m_state.current_thread = 0; - } - } - - auto& thread = *thread_ptr; - - // Determine disabled state under current filters. - // If it doesn't pass freq, it won't pass stack - const auto freq_disabled = (entry.is_tick && !m_state.show_tick) || !passes_filters(entry.lower_cased_function_name); - const auto stack_disabled = freq_disabled || ((entry.is_tick && !m_state.show_tick) || !passes_filters(entry.lower_cased_full_name)); - - // Frequency tracking: bump existing, or add. - auto freq_it = std::ranges::find_if(thread.call_frequencies, [&entry](const CallFrequencyEntry& freq_entry) -> bool { - return entry.function_hash == freq_entry.function_hash; - }); - - const auto entry_source_flags = static_cast(entry.hook_target); - - if (freq_it != thread.call_frequencies.end()) [[likely]] - { - auto& freq = *freq_it; - ++freq.frequency; - freq.is_disabled = freq_disabled; - freq.source_flags |= entry_source_flags; - - // Maintain descending order by frequency using list::splice (fast, no alloc). - auto new_pos = freq_it; - while (new_pos != thread.call_frequencies.begin()) - { - auto prev = std::prev(new_pos); - if (prev->frequency > freq.frequency) - { - break; - } - new_pos = prev; - } - if (new_pos != freq_it) - { - thread.call_frequencies.splice(new_pos, thread.call_frequencies, freq_it); - } - } - else - { - thread.call_frequencies.emplace_back(static_cast(entry), entry.is_tick); - auto& freq = thread.call_frequencies.back(); - freq.is_disabled = freq_disabled; - freq.source_flags = entry_source_flags; - } - - // Call stack history. - entry.is_disabled = stack_disabled; - if (can_render_entry(thread.call_stack.emplace_back(std::move(entry)))) - { - if (thread.call_stack_render_set.size() == m_state.text_temp_virtualization_count) [[likely]] - { - auto extracted = thread.call_stack_render_set.extract(thread.call_stack_render_set.begin()); - extracted.value() = thread.call_stack.size() - 1; - thread.call_stack_render_set.insert(thread.call_stack_render_set.end(), std::move(extracted)); - } - else [[unlikely]] - { - thread.call_stack_render_set.insert(thread.call_stack_render_set.end(), thread.call_stack.size() - 1); - } - } - }); - } - - // todo there's definitely room for improvement, like diffing white/blacklists to tell if test_str needs to be checked - // (would be done by callers probably) or keeping track of an 'enabled' and 'disabled' unordered_set of - // hashes (that the StringPool could be altered to provide) to skip string parsing, but this is good enough for now. - auto Client::passes_filters(const std::string_view test_str) const -> bool - { - bool passes_whitelist = m_state.whitelist_tokens.empty(); - for (const auto& token : m_state.whitelist_tokens) // any whitelist token present => pass - { - if (test_str.contains(token)) - { - passes_whitelist = true; - break; - } - } - if (!passes_whitelist) - { - return false; - } - - for (const auto& token : m_state.blacklist_tokens) // any blacklist token present => fail - { - if (test_str.contains(token)) - { - return false; - } - } - return true; - } - - auto Client::save(ESaveMode mode) -> void - { - if (mode == ESaveMode::none) - { - return; - } - - std::error_code ec; - std::filesystem::create_directories(m_dump_dir, ec); - - const auto now = std::chrono::system_clock::now(); - const std::time_t now_t = std::chrono::system_clock::to_time_t(now); - std::tm local_tm{}; - localtime_s(&local_tm, &now_t); - - std::ostringstream oss; - // Windows filenames cannot contain ':'. - oss << std::put_time(&local_tm, "%Y-%m-%d %H-%M-%S"); - - if (mode == ESaveMode::current) - { - auto& threads = m_state.threads; - if (threads.empty()) - { - return; - } - - if (m_state.current_thread < 0) - { - m_state.current_thread = 0; - } - if (static_cast(m_state.current_thread) >= threads.size()) - { - m_state.current_thread = static_cast(threads.size() - 1); - } - - const auto filename = "EventViewerMod Capture-"s + to_string(m_state.hook_target) + "-" + EMode_NameArray[static_cast(m_state.mode)] + " " + - oss.str() + ".txt"; - const auto path = m_dump_dir / filename; - std::ofstream file{path}; - if (!file.is_open()) - { - return; - } - - file << to_string(m_state.hook_target) << " "; - serialize_view(threads[m_state.current_thread], m_state.mode, m_state.hook_target, file); - file.close(); - m_state.last_save_path = path.string(); - ImGui::OpenPopup("Saved File"); - return; - } - - if (mode == ESaveMode::all) - { - if (m_state.threads.empty()) - { - return; - } - - const auto filename = "EventViewerMod Capture-All "s + oss.str() + ".txt"; - const auto path = m_dump_dir / filename; - std::ofstream file{path}; - if (!file.is_open()) - { - return; - } - - serialize_all_views(file); - file.close(); - m_state.last_save_path = path.string(); - ImGui::OpenPopup("Saved File"); - } - } - - auto Client::serialize_view(ThreadInfo& info, const EMode mode, const EMiddlewareHookTarget hook_target, std::ofstream& out) const -> void - { - out << fmt::format("Thread {} {}\n\n", info.id_string(), EMode_NameArray[static_cast(mode)]); - - const uint32_t selected_flags = static_cast(hook_target); - - if (mode == EMode::Stack) - { - if (info.call_stack.empty()) - { - out << "No captures.\n\n\n"; - return; - } - - for (const auto& entry : info.call_stack) - { - if ((static_cast(entry.hook_target) & selected_flags) == 0) - { - continue; - } - - if (entry.is_disabled) - { - continue; - } - for (auto i = 0u; i < entry.depth; ++i) - out << "\t"; - out << entry.full_name << '\n'; - } - - out << "\n\n\n"; - return; - } - - if (info.call_frequencies.empty()) - { - out << "No captures.\n\n\n"; - return; - } - - for (const auto& entry : info.call_frequencies) - { - if ((entry.source_flags & selected_flags) == 0) - { - continue; - } - - if (entry.is_disabled) - { - continue; - } - out << entry.function_name << '\t' << entry.frequency << '\n'; - } - - out << "\n\n\n"; - } - - auto Client::serialize_all_views(std::ofstream& out) -> void - { - for (auto& thread : m_state.threads) - { - out << to_string(EMiddlewareHookTarget::All) << " "; - serialize_view(thread, EMode::Stack, EMiddlewareHookTarget::All, out); - out << to_string(EMiddlewareHookTarget::All) << " "; - serialize_view(thread, EMode::Frequency, EMiddlewareHookTarget::All, out); - } - } - - auto Client::clear_threads() -> void - { - m_state.current_thread = 0; - m_state.threads.clear(); - m_state.thread_explicitly_chosen = false; - m_state.thread_implicitly_set = false; - } - - auto Client::can_render_entry(const CallStackEntry& entry) const -> bool - { - return !(entry.is_disabled || ((static_cast(entry.hook_target) & static_cast(m_state.hook_target)) == 0)); - } - - auto Client::resize_render_set(ThreadInfo& thread, const size_t max_size) const -> void - { - auto& stack = thread.call_stack; - auto& set = thread.call_stack_render_set; - - if (!max_size) return set.clear(); - if (set.size() == max_size) return; - - if (set.size() < max_size) - { - const auto cs_begin = stack.begin(); - for (auto entry_it = set.empty() ? stack.rbegin() : std::make_reverse_iterator(stack.begin() + *set.begin()); entry_it != stack.rend(); ++entry_it) - { - if (can_render_entry(*entry_it)) - { - set.insert(set.begin(), static_cast((entry_it.base() - cs_begin) - 1)); - if (set.size() == max_size) break; - } - } - - return; - } - - // set.size() > max_size - while (set.size() != max_size) - set.erase(set.begin()); - } - - auto Client::request_save_state() -> void - { - m_state.needs_save.test_and_set(std::memory_order_release); - } - - auto Client::add_to_white_list(const std::string_view item) -> void - { - if (m_state.whitelist.empty()) - { - m_state.whitelist += item; - } - else - { - m_state.whitelist += ", "; - m_state.whitelist += item; - } - request_save_state(); - apply_filters_to_history(true, false, false); - } - - auto Client::add_to_black_list(std::string_view item) -> void - { - if (m_state.blacklist.empty()) - { - m_state.blacklist += item; - } - else - { - m_state.blacklist += ", "; - m_state.blacklist += item; - } - request_save_state(); - apply_filters_to_history(false, true, false); - } - - auto Client::render_entry_stack_modal(const CallStackEntry* entry) -> void - { - if (m_entry_call_stack_renderer) return; - // find root entry and next root entry - // finding the root entry also reveals all callers, so go ahead and bookkeep it - const auto& stack = m_state.threads[m_state.current_thread].call_stack; - const size_t target_abs_idx = entry - stack.data(); - size_t root_abs_idx = target_abs_idx; - std::vector idxs_relevant_to_target{}; // added in reverse-view order - if (entry->depth) - { - uint32_t last_lowest_depth = entry->depth; - for (auto idx = target_abs_idx - 1; idx >= 0; --idx) - { - auto this_depth = stack[idx].depth; - if (this_depth < last_lowest_depth) - { - idxs_relevant_to_target.push_back(idx); - last_lowest_depth = this_depth; - if (this_depth == 0) - { - root_abs_idx = idx; - break; - } - } - } - } - - size_t next_root_abs_idx = target_abs_idx + 1; - bool out_of_target_scope = false; - for (; next_root_abs_idx < stack.size(); ++next_root_abs_idx) // find callers and callees of target - { - const auto this_entry_depth = stack[next_root_abs_idx].depth; - if (this_entry_depth == 0) break; - - if (this_entry_depth <= entry->depth) out_of_target_scope = true; - if (!out_of_target_scope) idxs_relevant_to_target.push_back(next_root_abs_idx); - } - - std::vector context{stack.begin() + root_abs_idx, stack.begin() + next_root_abs_idx}; // copy entries - for (auto& abs_idx : idxs_relevant_to_target) // make idxs_relevant_to_target relative to context - { - abs_idx -= root_abs_idx; - } - const auto target_rel_idx = target_abs_idx - root_abs_idx; // find context index for target - idxs_relevant_to_target.push_back(target_rel_idx); - - // make any irrelevant entry disabled by default, and assert that any relevant entry is enabled. the modal won't change them, but - // will use is_disabled as a flag to indicate its relevance for the checkbox. - for (size_t context_idx = 0; context_idx < context.size(); ++context_idx) - { - if (std::ranges::find(idxs_relevant_to_target, static_cast(context_idx)) != idxs_relevant_to_target.end()) - { - context[context_idx].is_disabled = false; - continue; - } - context[context_idx].is_disabled = true; - } - - m_entry_call_stack_renderer = std::make_unique(target_rel_idx, std::move(context)); - } - - auto Client::GetInstance() -> Client& - { - static Client client{}; - return client; - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/EntryCallStackRenderer.cpp b/cppmods/EventViewerMod/src/EntryCallStackRenderer.cpp deleted file mode 100644 index f7473ddf..00000000 --- a/cppmods/EventViewerMod/src/EntryCallStackRenderer.cpp +++ /dev/null @@ -1,144 +0,0 @@ -#include - -#include -#include -#include -#include - -// EventViewerMod: rendering for the call stack/context modal. -// -// The context vector is prepared by Client (based on the selected history entry). This renderer -// is intentionally simple: it only worries about ImGui state management and printing rows. -namespace RC::EventViewerMod -{ - using namespace std::literals::string_literals; - - EntryCallStackRenderer::EntryCallStackRenderer(const size_t target_idx, std::vector context) - : m_target_idx(target_idx), m_context(std::move(context)) - { - m_target_ptr = &m_context[m_target_idx]; - } - - auto EntryCallStackRenderer::render() -> bool - { - ImVec2 center = ImGui::GetMainViewport()->GetCenter(); - ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); - - // BeginPopupModal() will *never* return true unless the popup has been opened. - // Because the renderer is created from a context-menu click (possibly in a different - // frame / popup), we request opening once here in the stable render path. - if (!m_requested_open) - { - ImGui::OpenPopup("Entry Call Stack##entrycallstackmodal"); - m_requested_open = true; - } - - if (ImGui::BeginPopupModal("Entry Call Stack##entrycallstackmodal", nullptr, ImGuiWindowFlags_HorizontalScrollbar)) - { - ImGui::Checkbox("Show Full Context", &m_show_full_context); - HelpMarker(HelpStrings::HELP_CEMODAL_SHOW_FULL_CONTEXT); - ImGui::Checkbox("Disable Indent Colors", &m_disable_indent_colors); - - const auto btn_height = ImGui::GetFrameHeightWithSpacing(); - auto view_area = ImGui::GetContentRegionAvail(); - view_area.y -= btn_height; - ImGui::BeginChild("##entrycallstackview", view_area, ImGuiChildFlags_Borders | ImGuiChildFlags_FrameStyle, ImGuiWindowFlags_HorizontalScrollbar); - - int prev_depth = 0; - bool have_prev = false; - int current_indent = 0; - int id = 0; - uint8_t flags = m_disable_indent_colors ? ECallStackEntryRenderFlags_None : ECallStackEntryRenderFlags_IndentColors; - flags |= ECallStackEntryRenderFlags_WithSupportMenus; - for (const auto& entry : m_context) - { - if (entry.is_disabled && !m_show_full_context) continue; - const int depth = static_cast(entry.depth); - const int delta = have_prev ? (depth - prev_depth) : depth; - ImGui::PushID(id++); - &entry != m_target_ptr ? entry.render(delta, static_cast(flags)) - : entry.render(delta, static_cast(flags | ECallStackEntryRenderFlags_Highlight)); - ImGui::PopID(); - current_indent += delta; - prev_depth = depth; - have_prev = true; - } - - // Reset indent state so the rest of the popup doesn't inherit the last depth. - while (current_indent > 0) - { - ImGui::Unindent(); - --current_indent; - } - - ImGui::EndChild(); - - if (ImGui::Button("Save##entrycallstacksave")) - { - save(); - } - HelpMarker(HelpStrings::HELP_CEMODAL_SAVE); - ImGui::SameLine(); - // NOTE: - // Do *not* early-return before EndPopup(). - // ImGui maintains multiple internal stacks (window stack, ID stack, etc.). - // Skipping EndPopup() will eventually trip assertions like "Calling PopId() too many times". - bool keep_open = true; - if (ImGui::Button("Close##entrycallstackclose")) - { - ImGui::CloseCurrentPopup(); - keep_open = false; - } - - if (ImGui::BeginPopupModal("Saved Entry File", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) - { - ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); - ImGui::Text("Saved file to %s", m_last_save_path.c_str()); - ImGui::PopTextWrapPos(); - if (ImGui::Button("OK")) ImGui::CloseCurrentPopup(); - ImGui::EndPopup(); - } - - ImGui::EndPopup(); - - if (!keep_open) - { - return false; - } - } - - return true; - } - - auto EntryCallStackRenderer::save() -> void - { - static const auto wd = std::filesystem::path{StringType{UE4SSProgram::get_program().get_working_directory()}}; - static const auto captures_root = wd / "Mods" / "EventViewerMod" / "captures"; - std::error_code ec; - std::filesystem::create_directories(captures_root, ec); - - const auto now = std::chrono::system_clock::now(); - const std::time_t now_t = std::chrono::system_clock::to_time_t(now); - std::tm local_tm{}; - localtime_s(&local_tm, &now_t); - - std::ostringstream oss; - // Windows filenames cannot contain ':'. - oss << std::put_time(&local_tm, "%Y-%m-%d %H-%M-%S"); - - const auto filename = "EventViewerMod Capture-Entry "s + std::string(m_context[m_target_idx].function_name) + " " + oss.str() + ".txt"; - const auto path = captures_root / filename; - std::wofstream out{path}; - - for (const auto& entry : m_context) - { - if (entry.is_disabled && !m_show_full_context) continue; - auto str = entry.to_string_with_prefix(); - out << str; - } - - out.close(); - m_last_save_path = path.string(); - ImGui::OpenPopup("Saved Entry File"); - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/EventViewer.cpp b/cppmods/EventViewerMod/src/EventViewer.cpp deleted file mode 100644 index 62b1077f..00000000 --- a/cppmods/EventViewerMod/src/EventViewer.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include - -// EventViewerMod: UE4SS mod entry point and ImGui tab wiring. -// -// This is the glue between UE4SS' mod lifecycle (start_mod/on_unreal_init) and our -// ImGui renderer + middleware. - -#include -#include -#include -#include - -namespace RC::EventViewerMod -{ - EventViewerMod::EventViewerMod() - { - ModName = STR("EventViewerMod"); - ModAuthors = STR("wildcherry"); - ModDescription = STR("Lets you view a live call stack of ProcessEvent and ProcessInternal, with additional call frequency tracking."); - ModVersion = STR("1.0.0"); - - register_tab(STR("EventViewer"), [](CppUserModBase* mod) { - UE4SS_ENABLE_IMGUI(); - - // Avoid unnecessary expensive dynamic_cast - ImGui::BeginDisabled(!static_cast(mod)->m_unreal_loaded.test()); // NOLINT(*-pro-type-static-cast-downcast) - auto& style = ImGui::GetStyle(); - const auto old_size = style.GrabMinSize; - style.GrabMinSize = 30.0f; - Client::GetInstance().render(); - style.GrabMinSize = old_size; - ImGui::EndDisabled(); - }); - } - - void EventViewerMod::on_unreal_init() - { - Unreal::Hook::RegisterEngineTickPreCallback( - [this](auto&, Unreal::UEngine*, float, bool) { - m_unreal_loaded.test_and_set(); - }, - {true, true, STR("EventViewerMod"), STR("InstallHook")}); - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/FilterCountRenderer.cpp b/cppmods/EventViewerMod/src/FilterCountRenderer.cpp deleted file mode 100644 index 5c280cb8..00000000 --- a/cppmods/EventViewerMod/src/FilterCountRenderer.cpp +++ /dev/null @@ -1,29 +0,0 @@ -#include -#include - -namespace RC::EventViewerMod -{ - auto FilterCountRenderer::add() -> void - { - ++m_count; - } - - auto FilterCountRenderer::render_and_reset(const bool show_tooltip) -> void - { - if (!m_count) return; - static ImVec4 text_color{1.0f, 1.0f, 0.0f, 1.0f}; - ImGui::TextColored(text_color, "<%llu Calls Filtered>", m_count); - if (show_tooltip) - ImGui::SetItemTooltip("The call above and the call below may not be related. Right click an entry -> Show Call Stack to see related calls."); - m_count = 0; - } - - auto FilterCountRenderer::render(const size_t count, const bool show_tooltip) -> void - { - if (!count) return; - static ImVec4 text_color{1.0f, 1.0f, 0.0f, 1.0f}; - ImGui::TextColored(text_color, "<%llu Calls Filtered>", count); - if (show_tooltip) - ImGui::SetItemTooltip("The call above and the call below may not be related. Right click an entry -> Show Call Stack to see related calls."); - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/Middleware.cpp b/cppmods/EventViewerMod/src/Middleware.cpp deleted file mode 100644 index 670944d6..00000000 --- a/cppmods/EventViewerMod/src/Middleware.cpp +++ /dev/null @@ -1,279 +0,0 @@ -#include - -#include -#include - -#include -#include - -#include - -// note if using fname index as a hash, games that implement name recycling can be problematic/inaccurate for context objects, and right clicking entries -// FIXME trailing spaces on copy? -// EventViewerMod: hook interception + enqueue backend. -// -// Middleware installs the UE hooks and turns each callback into a lightweight CallStackEntry -// that can be consumed by the UI thread later. -// -// Key design points: -// - All supported hooks are installed and intercepted concurrently. -// - Depth is computed with a single counter so nested PE/PI/PLSF chains share indentation. -// - Enqueue is guarded by m_allow_queue to avoid capturing half-installed state. -// - The queue is moodycamel::ConcurrentQueue with per-thread ProducerTokens. -// - -namespace RC::EventViewerMod -{ - using RC::Unreal::FFrame; - using RC::Unreal::UFunction; - using RC::Unreal::UObject; - - using RC::Unreal::Hook::ERROR_ID; - using RC::Unreal::Hook::GlobalCallbackId; - using RC::Unreal::Hook::RegisterProcessEventPostCallback; - using RC::Unreal::Hook::RegisterProcessEventPreCallback; - using RC::Unreal::Hook::RegisterProcessInternalPostCallback; - using RC::Unreal::Hook::RegisterProcessInternalPreCallback; - using RC::Unreal::Hook::UnregisterCallback; - - using moodycamel::ConsumerToken; - using moodycamel::ProducerToken; - - auto Middleware::GetInstance() -> Middleware& - { - static Middleware s_instance; - return s_instance; - } - - Middleware::Middleware() - { - Unreal::UObjectGlobals::ForEachUObject([this](UObject* object, ...) -> LoopAction { - if (object && Unreal::Cast(object) && object->GetName().contains(STR("Tick"))) - { - m_tick_fns.insert(object); - } - return LoopAction::Continue; - }); - - Output::send(L"[EventViewerMod] Found {} engine tick functions!", m_tick_fns.size()); - - m_pe_controller = {.register_prehook_fn = &RC::Unreal::Hook::RegisterProcessEventPreCallback, - .register_posthook_fn = &RC::Unreal::Hook::RegisterProcessEventPostCallback, - .m_pre_callback = - [this](auto&, UObject* context, UFunction* function, void*) { - return enqueue(EMiddlewareHookTarget::ProcessEvent, context, function); - }, - .m_post_callback = - [](auto&, UObject*, UFunction*, void*) { - m_depth = (m_depth == 0) ? 0 : (m_depth - 1); - }}; - - m_pi_controller = {.register_prehook_fn = &RC::Unreal::Hook::RegisterProcessInternalPreCallback, - .register_posthook_fn = &RC::Unreal::Hook::RegisterProcessInternalPostCallback, - .m_pre_callback = - [this](auto&, UObject* context, FFrame& stack, void*) { - auto fn = stack.Node(); - if (!fn) fn = stack.CurrentNativeFunction(); - return enqueue(EMiddlewareHookTarget::ProcessInternal, context, fn); - }, - .m_post_callback = - [](auto&, UObject*, FFrame&, void*) { - m_depth = (m_depth == 0) ? 0 : (m_depth - 1); - }}; - - m_plsf_controller = {.register_prehook_fn = &RC::Unreal::Hook::RegisterProcessLocalScriptFunctionPreCallback, - .register_posthook_fn = &RC::Unreal::Hook::RegisterProcessLocalScriptFunctionPostCallback, - .m_pre_callback = - [this](auto&, UObject* context, FFrame& stack, void*) { - auto fn = stack.Node(); - if (!fn) fn = stack.CurrentNativeFunction(); - return enqueue(EMiddlewareHookTarget::ProcessLocalScriptFunction, context, fn); - }, - .m_post_callback = - [](auto&, UObject*, FFrame&, void*) { - m_depth = (m_depth == 0) ? 0 : (m_depth - 1); - }}; - QueueProfiler::Reset(); - } - - Middleware::~Middleware() - { - stop_impl(false); - } - - auto Middleware::assert_on_imgui_thread() const -> void - { - if (std::this_thread::get_id() != m_imgui_id) - { - throw std::runtime_error("EventViewerMod middleware: must be called from ImGui thread"); - } - } - - auto Middleware::is_tick_fn(const UFunction* fn) const -> bool - { - return fn && m_tick_fns.contains(const_cast(fn)); - } - - auto Middleware::set_imgui_thread_id(std::thread::id id) -> void - { - m_imgui_id = id; - } - - auto Middleware::get_imgui_thread_id() const -> std::thread::id - { - return m_imgui_id; - } - - auto Middleware::get_average_enqueue_time() const -> double - { - return QueueProfiler::GetEnqueueAverage(); - } - - auto Middleware::get_average_dequeue_time() const -> double - { - return QueueProfiler::GetDequeueAverage(); - } - - auto Middleware::stop_impl(const bool do_assert) -> bool - { - if (do_assert) - { - assert_on_imgui_thread(); - } - if (m_paused) - { - return true; - } - - m_pe_controller.unhook(); - m_pi_controller.unhook(); - m_plsf_controller.unhook(); - - // Causes all thread_local depths to be reset the next time the prehook runs. - m_depth_reset_counter.fetch_add(1, std::memory_order_release); - m_allow_queue.clear(std::memory_order_release); - m_paused = true; - return true; - } - - auto Middleware::stop() -> bool - { - if (!stop_impl(true)) - { - return false; - } - - // Drain remaining items (discard). - if (m_buffer.empty()) - { - m_buffer.resize(256); - } - - for (;;) - { - const auto count = m_queue.try_dequeue_bulk(m_imgui_consumer_token, m_buffer.data(), m_buffer.size()); - if (count == 0) - { - break; - } - } - - return true; - } - - auto Middleware::is_paused() const -> bool - { - assert_on_imgui_thread(); - return m_paused; - } - - auto Middleware::start() -> bool - { - assert_on_imgui_thread(); - if (!m_paused) - { - return true; - } - - if (!Unreal::FName::ToStringInternal.is_ready()) - { - Output::send(L"[EventViewerMod] Mod requires FName.toString to be known!"); - } - - if (!(m_plsf_controller.install_posthook() && m_pi_controller.install_posthook() && m_pe_controller.install_posthook() && - m_plsf_controller.install_prehook() && m_pi_controller.install_prehook() && m_pe_controller.install_prehook())) - { - m_pe_controller.unhook(); - m_pi_controller.unhook(); - m_plsf_controller.unhook(); - return false; - } - - m_paused = false; - m_allow_queue.test_and_set(std::memory_order_acq_rel); - return true; - } - - auto Middleware::enqueue(const EMiddlewareHookTarget hook_target, UObject* context, UFunction* function) -> void - { - thread_local std::thread::id thread_id = std::this_thread::get_id(); - thread_local uint64_t local_reset_counter = m_depth_reset_counter.load(std::memory_order_acquire); - if (!m_allow_queue.test(std::memory_order_acquire)) return; - const auto current_counter = m_depth_reset_counter.load(std::memory_order_acquire); - if (current_counter != local_reset_counter) - { - m_depth = 0; - local_reset_counter = current_counter; - } - - const auto is_tick = is_tick_fn(function); - - // Middleware is a singleton and lives for the mod lifetime, so a simple thread_local token is safe here. - thread_local ProducerToken tls_token{m_queue}; - - auto strings = StringPool::GetInstance().get_strings(context, function); - - QueueProfiler::BeginEnqueue(); - m_queue.enqueue(tls_token, CallStackEntry{hook_target, strings, m_depth++, thread_id, is_tick}); - QueueProfiler::EndEnqueue(); - } - - auto Middleware::dequeue(const uint16_t max_ms, const uint16_t max_count_per_iteration, const std::function& on_dequeue) -> void - { - assert_on_imgui_thread(); - - const auto start_time = std::chrono::steady_clock::now(); - if (m_buffer.size() < max_count_per_iteration) - { - m_buffer.resize(max_count_per_iteration); - } - auto now = std::chrono::steady_clock::now(); - for (; std::chrono::duration_cast(now - start_time).count() < max_ms; now = std::chrono::steady_clock::now()) - { - QueueProfiler::BeginDequeue(); - const auto amount = m_queue.try_dequeue_bulk(m_imgui_consumer_token, m_buffer.data(), max_count_per_iteration); - QueueProfiler::EndDequeue(); - - if (amount == 0) - { - break; - } - - for (size_t i = 0; i < amount; ++i) - { - on_dequeue(std::move(m_buffer[i])); - } - - if (m_queue.size_approx() == 0) - { - break; - } - } - - QueueProfiler::AddPendingCount(m_queue.size_approx()); - if (std::chrono::duration_cast(now - start_time).count() >= max_ms) - { - QueueProfiler::AddTimeExceededCount(); - } - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/StringPool.cpp b/cppmods/EventViewerMod/src/StringPool.cpp deleted file mode 100644 index aa0dfa4f..00000000 --- a/cppmods/EventViewerMod/src/StringPool.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include - -#include - -#include - -// EventViewerMod: string interning and cached lowercase views. -// -// Hooks run on game threads and must be cheap. Instead of allocating/formatting strings for -// every callback, we intern names and cache their lowercase forms once. -// -// The returned std::string_views remain valid until StringPool::clear() is called. - -auto RC::EventViewerMod::StringPool::get_strings(RC::Unreal::UObject* caller, RC::Unreal::UFunction* function) -> AllNameStringViews -{ - if (!caller || !function) return AllNameStringViews{}; - // StringPool combines the caller's ComparisonIndex and the function's ComparisonIndex - // to generate a unique hash for the full name string. - const uint32_t function_hash = function->GetNamePrivate().GetComparisonIndex(); - uint64_t hash = function_hash; - hash = hash << 32; - hash |= caller->GetNamePrivate().GetComparisonIndex(); - - { - std::shared_lock lock(m_mutex); - auto string_info_it = m_main_pool.find(hash); - if (string_info_it != m_main_pool.end()) - { - auto& string_info = string_info_it->second; - std::string_view full_name = string_info.full_name; - std::string_view lower_full = string_info.lower_cased_full_name; - - std::string_view func_name = full_name; - func_name.remove_prefix(string_info.function_begin); - - std::string_view lower_func_name = lower_full; - lower_func_name.remove_prefix(string_info.function_begin); - - return AllNameStringViews{FunctionNameStringViews{function_hash, func_name, lower_func_name}, full_name, lower_full, hash}; - } - } - - const auto caller_str = RC::to_string(caller->GetName()); - const auto func_str = RC::to_string(function->GetName()); - - // Build once, store both original-case and lower-cased versions. - auto full = caller_str + "." + func_str; - auto lower_full = to_lower_case(full); - auto path_str = RC::to_string(function->GetPathName()); - - { - std::unique_lock lock(m_mutex); - auto& string_info = m_main_pool.emplace(hash, StringInfo{caller_str.size() + 1, std::move(full), std::move(lower_full)}).first->second; - m_path_pool.emplace(function_hash, std::move(path_str)); - - std::string_view full_name = string_info.full_name; - std::string_view lower_full_name = string_info.lower_cased_full_name; - - std::string_view func_name = full_name; - func_name.remove_prefix(string_info.function_begin); - - std::string_view lower_func_name = lower_full_name; - lower_func_name.remove_prefix(string_info.function_begin); - - return AllNameStringViews{FunctionNameStringViews{function_hash, func_name, lower_func_name}, full_name, lower_full_name, hash}; - } -} - -auto RC::EventViewerMod::StringPool::get_path_name(const uint32_t function_hash) -> std::string_view -{ - std::shared_lock lock(m_mutex); - auto path_it = m_path_pool.find(function_hash); - if (path_it == m_path_pool.end()) return ""; - return path_it->second; -} - -auto RC::EventViewerMod::StringPool::clear() -> void -{ - std::unique_lock lock(m_mutex); - m_path_pool.clear(); -} - -auto RC::EventViewerMod::StringPool::GetInstance() -> StringPool& -{ - static StringPool string_pool; - return string_pool; -} diff --git a/cppmods/EventViewerMod/src/Structs.cpp b/cppmods/EventViewerMod/src/Structs.cpp deleted file mode 100644 index b27b98b0..00000000 --- a/cppmods/EventViewerMod/src/Structs.cpp +++ /dev/null @@ -1,203 +0,0 @@ -#include -#include -#include - -#include -#include - -#include -#include - -#include -#include - -inline constexpr static unsigned ALPHA = 200; -inline constexpr static std::array COLORS = { - IM_COL32(255, 0, 0, ALPHA), // red - IM_COL32(0, 0, 255, ALPHA), // blue - IM_COL32(0, 255, 0, ALPHA), // green - IM_COL32(255, 176, 0, ALPHA), // orange - IM_COL32(176, 255, 0, ALPHA), // lime - IM_COL32(0, 255, 255, ALPHA), // cyan - IM_COL32(255, 255, 0, ALPHA), // yellow -}; -inline constexpr static auto SELECTED_COLOR = ImVec4{1.0f, 1.0f, 0.0f, 1.0f}; - -// EventViewerMod: small UI-facing helpers for entries and context menus. -// -// The heavy lifting (hooks, queueing, string pooling) happens elsewhere. This file is mostly -// convenience methods used by the ImGui layer: context menu attachment management, clipboard -// helpers, and per-entry "support menu" behavior. -// -namespace RC::EventViewerMod -{ - auto copy_to_clipboard(const std::string_view& string) -> void - { - ImGui::LogToClipboard(); - ImVec2 dummy{0, 0}; - ImGui::LogRenderedText(&dummy, string.data(), string.data() + string.size()); - ImGui::LogFinish(); - }; - - EntryBase::EntryBase(const bool is_tick) : is_tick(is_tick) - { - } - - CallStackEntry::CallStackEntry( - const EMiddlewareHookTarget hook_target, const AllNameStringViews& strings, const uint32_t depth, const std::thread::id thread_id, const bool is_tick) - : EntryBase(is_tick), hook_target(hook_target), depth(depth), thread_id(thread_id) - { - // Inheritance is used to avoid extra indirection/caches misses. - // (StringPool owns the backing storage for these views.) - function_hash = strings.function_hash; - function_name = strings.function_name; - lower_cased_function_name = strings.lower_cased_function_name; - full_name = strings.full_name; - lower_cased_full_name = strings.lower_cased_full_name; - full_hash = strings.full_hash; - } - - auto CallStackEntry::render(const int indent_delta, const ECallStackEntryRenderFlags_ flags) const -> void - { - render_indents(indent_delta); - if (flags & ECallStackEntryRenderFlags_IndentColors) - { - const auto indent_width = ImGui::GetStyle().IndentSpacing * static_cast(depth); - auto min = ImGui::GetCursorScreenPos(); - auto max = min; - min.x -= indent_width; - max.y += ImGui::GetTextLineHeight(); - ImGui::GetWindowDrawList()->AddRectFilled(min, max, COLORS[depth % COLORS.size()]); - } - if (flags & ECallStackEntryRenderFlags_Highlight) [[unlikely]] - { - ImGui::TextColored(SELECTED_COLOR, to_prefix_string(hook_target)); - ImGui::SameLine(); - ImGui::TextColored(SELECTED_COLOR, full_name.data()); - } - else [[likely]] - { - ImGui::TextUnformatted(to_prefix_string(hook_target)); - ImGui::SameLine(); - ImGui::TextUnformatted(full_name.data()); - } - - if (flags & ECallStackEntryRenderFlags_WithSupportMenus) - { - render_support_menus(flags); - } - } - - auto CallStackEntry::to_string_with_prefix() const -> std::wstring - { - std::wstring out; - for (uint32_t i = 0; i < depth; ++i) - out += L"\t"; - out += ensure_str(to_prefix_string(hook_target)) + ensure_str(full_name) + L"\n"; - return out; - } - - auto CallStackEntry::render_indents(const int indent_delta) const -> void - { - if (indent_delta > 0) - { - for (int i = 0; i < indent_delta; ++i) - { - ImGui::Indent(); - } - } - else if (indent_delta < 0) - { - for (int i = indent_delta; i != 0; ++i) - { - ImGui::Unindent(); - } - } - } - - auto CallStackEntry::render_support_menus(const ECallStackEntryRenderFlags_ flags) const -> void - { - ImGui::SetItemTooltip("Right click for options"); - if (ImGui::BeginPopupContextItem("EntryPopup##ep", ImGuiPopupFlags_MouseButtonRight)) - { - if (flags & ECallStackEntryRenderFlags_WithSupportMenusCallStackModal & ~ECallStackEntryRenderFlags_WithSupportMenus) - { - if (ImGui::MenuItem("Show Call Stack")) Client::GetInstance().render_entry_stack_modal(this); // need to do this outside - ImGui::Separator(); - } - if (ImGui::MenuItem("Copy Function Full Name")) copy_to_clipboard(StringPool::GetInstance().get_path_name(function_hash)); - if (ImGui::MenuItem("Copy Function Name##cfn")) copy_to_clipboard(function_name); - if (ImGui::MenuItem("Add Function to Whitelist##fwl")) Client::GetInstance().add_to_white_list(function_name); - if (ImGui::MenuItem("Add Function to Blacklist##fbl")) Client::GetInstance().add_to_black_list(function_name); - ImGui::Separator(); - if (ImGui::MenuItem("Copy Caller Name##ccn")) copy_to_clipboard({full_name.begin(), function_name.begin() - 1}); - if (ImGui::MenuItem("Add Caller to Whitelist##cwl")) Client::GetInstance().add_to_white_list({full_name.begin(), function_name.begin() - 1}); - HelpMarker(HelpStrings::HELP_ADD_CALLER_AND_FUNC_NAME_WARNING); - if (ImGui::MenuItem("Add Caller to Blacklist##cbl")) Client::GetInstance().add_to_black_list({full_name.begin(), function_name.begin() - 1}); - HelpMarker(HelpStrings::HELP_ADD_CALLER_AND_FUNC_NAME_WARNING); - ImGui::Separator(); - if (ImGui::MenuItem("Copy Caller + Function Name##cfln")) copy_to_clipboard(full_name); - if (ImGui::MenuItem("Add Caller + Function to Whitelist##fnwl")) Client::GetInstance().add_to_white_list(full_name); - HelpMarker(HelpStrings::HELP_ADD_CALLER_AND_FUNC_NAME_WARNING); - if (ImGui::MenuItem("Add Caller + Function to Blacklist##fnb")) Client::GetInstance().add_to_black_list(full_name); - HelpMarker(HelpStrings::HELP_ADD_CALLER_AND_FUNC_NAME_WARNING); - ImGui::EndPopup(); - } - } - - CallFrequencyEntry::CallFrequencyEntry(const FunctionNameStringViews& strings, const bool is_tick) : EntryBase(is_tick) - { - function_hash = strings.function_hash; - function_name = strings.function_name; - lower_cased_function_name = strings.lower_cased_function_name; - } - - auto CallFrequencyEntry::render(const ECallFrequencyEntryRenderFlags_ flags) const -> void - { - ImGui::TableNextRow(); - ImGui::TableSetColumnIndex(0); - ImGui::TextUnformatted(function_name.data()); - if (flags & ECallFrequencyEntryRenderFlags_WithSupportMenus) render_support_menus(); - ImGui::TableSetColumnIndex(1); - ImGui::Text("%llu", static_cast(frequency)); - } - - auto CallFrequencyEntry::render_support_menus() const -> void - { - ImGui::SetItemTooltip("Right click for options"); - if (ImGui::BeginPopupContextItem("EntryPopup##fep", ImGuiPopupFlags_MouseButtonRight)) - { - if (ImGui::MenuItem("Copy Function Full Name")) copy_to_clipboard(StringPool::GetInstance().get_path_name(function_hash)); - if (ImGui::MenuItem("Copy Function Name##cfn")) copy_to_clipboard(function_name); - if (ImGui::MenuItem("Add Function to Whitelist##fwl")) Client::GetInstance().add_to_white_list(function_name); - if (ImGui::MenuItem("Add Function to Blacklist##fbl")) Client::GetInstance().add_to_black_list(function_name); - ImGui::EndPopup(); - } - } - - ThreadInfo::ThreadInfo(const std::thread::id thread_id) : thread_id(thread_id), is_game_thread(RC::Unreal::GetGameThreadId() == thread_id) - { - } - - auto ThreadInfo::id_string() -> const char* - { - if (m_id_string.empty()) - { - std::stringstream ss; - ss << thread_id; - m_id_string = ss.str(); - if (is_game_thread) - { - m_id_string += " (Game)"; - } - } - return m_id_string.c_str(); - } - - auto ThreadInfo::clear() -> void - { - call_frequencies.clear(); - call_stack.clear(); - call_stack_render_set.clear(); - } -} // namespace RC::EventViewerMod diff --git a/cppmods/EventViewerMod/src/dllmain.cpp b/cppmods/EventViewerMod/src/dllmain.cpp deleted file mode 100644 index c86a94c0..00000000 --- a/cppmods/EventViewerMod/src/dllmain.cpp +++ /dev/null @@ -1,19 +0,0 @@ -// EventViewerMod: Windows DLL entry point (UE4SS loads this module). -// -// The actual mod logic lives in EventViewerMod (see EventViewer.cpp). This file exists to -// satisfy the DLL entry requirements on Windows. - -#include - -extern "C" -{ - __declspec(dllexport) RC::CppUserModBase* start_mod() - { - return new EventViewerMod::EventViewerMod(); - } - - __declspec(dllexport) void uninstall_mod(RC::CppUserModBase* mod) - { - delete mod; - } -} \ No newline at end of file diff --git a/cppmods/KismetDebuggerMod/CMakeLists.txt b/cppmods/KismetDebuggerMod/CMakeLists.txt deleted file mode 100644 index c474290e..00000000 --- a/cppmods/KismetDebuggerMod/CMakeLists.txt +++ /dev/null @@ -1,14 +0,0 @@ -project(KismetDebuggerMod) - -set(TARGET KismetDebuggerMod) -project(${TARGET}) - -set(${TARGET}_Sources - "${CMAKE_CURRENT_SOURCE_DIR}/src/dllmain.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/src/KismetDebugger.cpp" - ) - -add_library(${TARGET} SHARED ${${TARGET}_Sources}) -target_include_directories(${TARGET} PRIVATE "include") -target_link_libraries(${TARGET} PRIVATE ImGui) -target_link_libraries(${TARGET} PUBLIC UE4SS) diff --git a/cppmods/KismetDebuggerMod/include/KismetDebugger.hpp b/cppmods/KismetDebuggerMod/include/KismetDebugger.hpp deleted file mode 100644 index 68a99983..00000000 --- a/cppmods/KismetDebuggerMod/include/KismetDebugger.hpp +++ /dev/null @@ -1,127 +0,0 @@ -#pragma once - -#include -#include - -#include -#include -#include - -namespace RC::GUI::KismetDebuggerMod -{ - using namespace RC::Unreal; - - auto expr_to_string(EExprToken expr) -> const char*; - - struct PausedContext - { - EExprToken expr{}; - UObject* context{}; - FFrame* stack{}; - }; - - class BreakpointStore - { - public: - BreakpointStore(); - ~BreakpointStore(); - - auto load(std::filesystem::path& path) -> void; - auto save() -> void; - - auto has_breakpoint(UFunction* fn, size_t index) -> bool; - auto add_breakpoint(UFunction* fn, size_t index) -> void; - auto add_breakpoint(const StringType& fn, size_t index) -> void; - auto remove_breakpoint(UFunction* fn, size_t index) -> void; - - private: - typedef std::unordered_set FunctionBreakpoints; - - std::unordered_map > m_breakpoints_by_function{}; - std::unordered_map > m_breakpoints_by_name{}; - }; - - class Debugger - { - public: - Debugger(); - ~Debugger(); - - auto enable() -> void; - auto disable() -> void; - - auto render() -> void; - auto render_nav_bar(float width) -> void; - - auto nav_to_function(UFunction* fn) -> void; - auto nav_to_function(std::string full_name) -> void; - - private: - bool m_paused{}; - - // std::string because it's used primarily with ImGui - std::unordered_map m_function_name_map{}; - std::vector m_nav_history{}; - int m_nav_history_index{-1}; - std::string m_nav_function{""}; - - // layout - float m_split_right{400.0}; - float m_split_left{400.0}; - - uint8_t* m_last_code{nullptr}; // pointer to last stack instruction, used to know if it's advanced since last frame - BreakpointStore& m_breakpoints; - - public: - static inline std::filesystem::path m_save_path; - }; - - class ScriptRenderContext - { - public: - ScriptRenderContext(std::optional& paused_context, UFunction* fn, bool scroll_to_active, BreakpointStore& breakpoints); - ~ScriptRenderContext(); - - auto render() -> void; - - private: - auto render_property(); - auto render_expr() -> EExprToken; - - template - auto read() -> T - { - T t; - memcpy(&t, &m_script[m_index], sizeof(T)); - m_index += sizeof(T); - return t; - } - - auto read_object() -> UObject* - { - return (UObject*)this->read(); - } - auto read_name() -> FName - { - FName n = read(); - read(); // not sure what this is for - return n; - } - - - private: - std::optional& m_paused_context; - UFunction* m_fn{}; - - bool m_scroll_to_active{}; - BreakpointStore& m_breakpoints; - - int m_indent{0}; - - uint8_t* m_script{}; - int m_script_size{}; - int m_index{}; - int m_cur{}; // or -1 - EExprToken m_current_expr{}; - }; -} diff --git a/cppmods/KismetDebuggerMod/src/KismetDebugger.cpp b/cppmods/KismetDebuggerMod/src/KismetDebugger.cpp deleted file mode 100644 index 67bdc500..00000000 --- a/cppmods/KismetDebuggerMod/src/KismetDebugger.cpp +++ /dev/null @@ -1,1437 +0,0 @@ -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define IMGUI_DEFINE_MATH_OPERATORS -#include -#include -#include -#include - -#include "Profiler/Profiler.hpp" - -#include - -namespace RC::GUI::KismetDebuggerMod -{ - - using namespace RC::Unreal; - - FNativeFuncPtr GNativesOriginal[EExprToken::EX_Max]; - volatile bool is_hooked = false; // cannot hook *immediately* as GNatives is populated at runtime - - volatile bool should_pause = false; - volatile bool should_next = false; - std::optional context; - std::mutex context_mutex; - - BreakpointStore g_breakpoints; - - void hook_expr_internal(UObject* Context, FFrame& Stack, void* RESULT_DECL, EExprToken N) { - UFunction* fn = Stack.Node(); - StringType name = Stack.Node()->GetFullName(); - ProfilerTransientScopeNamed(scope, to_string(name).c_str(), true); - - size_t index = Stack.Code() - fn->GetScript().GetData() - 1; - if (should_pause || g_breakpoints.has_breakpoint(fn, index)) - { - should_pause = true; - std::unique_lock lock_a(context_mutex); - PausedContext ctx{ - .expr = N, - .context = Context, - .stack = &Stack, - }; - context = std::optional(ctx); - lock_a.unlock(); - while (should_pause && !should_next) - { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - std::unique_lock lock_b(context_mutex); - context = std::nullopt; - lock_b.unlock(); - should_next = false; - } - - GNativesOriginal[N](Context, Stack, RESULT_DECL); - } - - template - void hook_expr(UObject* Context, FFrame& Stack, void* RESULT_DECL) { - hook_expr_internal(Context, Stack, RESULT_DECL, static_cast(N)); - } - - template void hook_all() { - GNativesOriginal[N - 1] = GNatives_Internal[N - 1]; - GNatives_Internal[N - 1] = &hook_expr; - hook_all(); - } - template <> void hook_all<0>() {} - - typedef std::unordered_map> JsonBreakpoints; - - BreakpointStore::BreakpointStore() - { - } - BreakpointStore::~BreakpointStore() - { - } - auto BreakpointStore::load(std::filesystem::path& path) -> void - { - JsonBreakpoints breakpoints{}; - auto ec = glz::read_file_json(breakpoints, path.string(), std::string{}); - - for (const auto& [fn, bps] : breakpoints) - { - auto wfn = ensure_str(fn); - for (const auto& bp : bps) - { - add_breakpoint(wfn, bp); - } - } - } - auto BreakpointStore::save() -> void - { - JsonBreakpoints breakpoints{}; - for (const auto& [fn, bps] : m_breakpoints_by_name) { - if (bps) breakpoints[to_string(fn)] = *bps; - } - auto ec = glz::write_file_json(breakpoints, Debugger::m_save_path.string(), std::string{}); - - } - - auto BreakpointStore::has_breakpoint(UFunction* fn, size_t index) -> bool - { - auto [it_fn, inserted] = m_breakpoints_by_function.emplace(fn, nullptr); - if (!inserted) - { - if (it_fn->second) - return it_fn->second->contains(index); - } - else - { - // insert null so we know we've already inserted the fn ptr into the name map - auto [it_name, inserted] = m_breakpoints_by_name.emplace(fn->GetFullName(), nullptr); - if (!inserted) - { - if (it_name->second) - { - it_fn->second = it_name->second; - return it_name->second->contains(index); - } - } - } - return false; - } - auto BreakpointStore::add_breakpoint(UFunction* fn, size_t index) -> void - { - std::shared_ptr bps; - auto [it_fn, inserted_fn] = m_breakpoints_by_function.emplace(fn, nullptr); - auto [it_name, inserted_name] = m_breakpoints_by_name.emplace(fn->GetFullName(), nullptr); - if (!inserted_fn && it_fn->second) bps = it_fn->second; - if (!inserted_name && it_name->second) bps = it_name->second; - - if (!bps) - bps = it_fn->second = it_name->second = std::make_shared(); - - bps->emplace(index); - - save(); - - /* - std::cout << "all breakpoints by fn ptr:" << std::endl; - for (const auto& fns : m_breakpoints_by_function) { - std::cout << (void*) fns.first << " = "; - if (fns.second) - for (const auto& bps : *fns.second) { - std::cout << bps << " "; - } - else - std::cout << "null"; - std::cout << std::endl; - } - - std::cout << "all breakpoints by fn name:" << std::endl; - for (const auto& fns : m_breakpoints_by_name) { - std::wcout << fns.first << " = "; - if (fns.second) - for (const auto& bps : *fns.second) { - std::cout << bps << " "; - } - else - std::cout << "null"; - std::cout << std::endl; - } - - std::cout << "all non-null breakpoints by fn name:" << std::endl; - for (const auto& fns : m_breakpoints_by_name) { - if (fns.second) - { - std::wcout << fns.first << " = "; - for (const auto& bps : *fns.second) { - std::cout << bps << " "; - } - std::cout << std::endl; - } - } - */ - } - auto BreakpointStore::add_breakpoint(const StringType& fn, size_t index) -> void - { - std::shared_ptr bps; - auto [it_name, inserted_name] = m_breakpoints_by_name.emplace(fn, nullptr); - if (!inserted_name && it_name->second) bps = it_name->second; - - if (!bps) - bps = it_name->second = std::make_shared(); - - bps->emplace(index); - - save(); - } - auto BreakpointStore::remove_breakpoint(UFunction* fn, size_t index) -> void - { - std::shared_ptr bps; - auto [it_fn, inserted_fn] = m_breakpoints_by_function.emplace(fn, nullptr); - auto [it_name, inserted_name] = m_breakpoints_by_name.emplace(fn->GetFullName(), nullptr); - if (!inserted_fn && it_fn->second) bps = it_fn->second; - if (!inserted_name && it_fn->second) bps = it_name->second; - - if (bps) - bps->erase(index); - - save(); - } - - Debugger::Debugger() : m_breakpoints(g_breakpoints) - { - m_save_path = StringType{UE4SSProgram::get_program().get_working_directory()} + fmt::format(STR("\\Mods\\KismetDebugger\\config\\breakpoints.json")); - } - Debugger::~Debugger() - { - if (is_hooked) - { - for (int i = 0; i < EExprToken::EX_Max; i++) - { - GNatives_Internal[i] = GNativesOriginal[i]; - } - is_hooked = false; - should_pause = false; - should_next = false; - - // Give main thread some time to exit hooked function. This can - // possibly be called to unload the DLL, in which case bad things - // will happen if the main thread is still inside the hook when it - // disappears. - // TODO: Need to find a way to guarantee the main thread has exited - // the hook before continuing. - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - } - - auto ImGui_Splitter(bool split_vertically, float thickness, float* size1, float* size2, float min_size1, float min_size2, float splitter_long_axis_size, const char* id_str = "##Splitter") -> bool - { - using namespace ImGui; - ImGuiContext& g = *GImGui; - ImGuiWindow* window = g.CurrentWindow; - ImGuiID id = window->GetID(id_str); - ImRect bb; - bb.Min = window->DC.CursorPos + (split_vertically ? ImVec2(*size1, 0.0f) : ImVec2(0.0f, *size1)); - bb.Max = bb.Min + CalcItemSize(split_vertically ? ImVec2(thickness, splitter_long_axis_size) : ImVec2(splitter_long_axis_size, thickness), 0.0f, 0.0f); - return SplitterBehavior(bb, id, split_vertically ? ImGuiAxis_X : ImGuiAxis_Y, size1, size2, min_size1, min_size2, 0.0f); - } - - auto Debugger::enable() -> void - { - // do a bunch of setup on enable rather than mod init because a lot of things aren't ready at mod init - - // hack to delay breakpoint loading because working directory changes at some point during load - try - { - m_breakpoints.load(m_save_path); - } - catch (std::exception& e) - { - Output::send(STR("[KismetDebugger]: Failed to load breakpoints: {}\n"), ensure_str(e.what())); - } - - if (GNatives_Internal != nullptr) - { - // finally actually enable the debugger - hook_all(); - is_hooked = true; - return; - } - Output::send(STR("[KismetDebugger]: GNatives not found.\n")); - } - auto Debugger::disable() -> void - { - for (int i = 0; i < EExprToken::EX_Max; i++) - { - GNatives_Internal[i] = GNativesOriginal[i]; - } - is_hooked = false; - should_pause = false; - should_next = false; - } - - auto get_object_address(FProperty* property, EExprToken expr, auto in_context) -> void* - { - void* container{}; - if constexpr (std::is_same_v, std::optional>) - { - if (!in_context.has_value()) { return nullptr; }; - const auto& context = in_context.value(); - if (expr == EX_InstanceVariable) - { - container = context.context; - } - else if (expr == EX_LocalVariable) - { - container = context.stack->Locals(); - } - else - { - return nullptr; - } - } - else - { - container = in_context; - } - return *property->ContainerPtrToValuePtr(container); - } - - static auto try_rendering_property_context_menu(UFunction* fn, int index, FProperty* property, EExprToken expr, auto context) -> void - { - if (property->IsA()) - { - if (auto data_ptr = get_object_address(property, expr, context); data_ptr) - { - auto popup_context_name = to_string(fmt::format(STR("{}-{}-{}"), static_cast(fn), index, static_cast(property))); - if (ImGui::BeginPopupContextItem(popup_context_name.c_str())) - { - if (ImGui::MenuItem("Copy address")) - { - ImGui::SetClipboardText(fmt::format("{}", data_ptr).c_str()); - } - ImGui::EndPopup(); - } - if (ImGui::IsItemHovered()) - { - ImGui::BeginTooltip(); - ImGui::Text("0x%p", data_ptr); - ImGui::EndTooltip(); - } - } - } - } - - auto Debugger::render() -> void - { - std::scoped_lock lock(context_mutex); - - bool position_updated = context && m_last_code != context->stack->Code(); - if (position_updated) - nav_to_function(context->stack->Node()); - - UFunction* current_fn = nullptr; - if (m_nav_history_index >= 0) - { - auto name = m_nav_history[m_nav_history_index]; - if (auto it = m_function_name_map.find(name); it != m_function_name_map.end()) - current_fn = it->second; - } - - - float m_split_right = (ImGui::GetContentRegionMax().x - m_split_left); - - ImGui_Splitter(true, 4.0f, &m_split_left, &m_split_right, 200.0f, 200.0f, -24.0f, "KismetDebugger_split"); - - ImGui::BeginChild("KismetDebugger_Controls", {m_split_left - 2, -24.0f}, true); - - if (ImGui::Button(is_hooked ? "disable" : "enable")) - { - if (is_hooked) - { - disable(); - } - else - { - enable(); - } - } - if (is_hooked) - { - if (auto ctx = context) - { - if (ImGui::Button("continue")) - { - should_pause = false; - } - ImGui::SameLine(); - if (ImGui::Button("next")) - { - should_next = true; - } - - UFunction* node = ctx->stack->Node(); - size_t index = ctx->stack->Code() - node->GetScript().GetData() - 1; - ImGui::Text("paused @ %s", expr_to_string(ctx->expr)); - ImGui::Text("index @ %zi", index); - ImGui::Text("object context = %s", to_string(ctx->context->GetFullName()).c_str()); - - if (ImGui::CollapsingHeader("Call stack", ImGuiTreeNodeFlags_DefaultOpen)) - { - int lines = 0; - FFrame* current = ctx->stack; - while (current != nullptr) - { - current = current->PreviousFrame(); - lines++; - } - ImGui::BeginChild("KismetDebugger_CallStack", {0, ImGui::GetStyle().ScrollbarSize + lines * ImGui::GetTextLineHeightWithSpacing()}, false, ImGuiWindowFlags_HorizontalScrollbar); - current = ctx->stack; - for (int i = 0; current != nullptr; ++i) - { - ImGui::Text("%i: %s", i, to_string(current->Node()->GetFullName()).c_str()); - current = current->PreviousFrame(); - } - ImGui::EndChild(); - } - } - else - { - if (ImGui::Button("pause")) - { - should_pause = true; - } - } - } - - if (current_fn) - { - if (ImGui::CollapsingHeader("Locals", ImGuiTreeNodeFlags_DefaultOpen)) - { - ImGui::BeginChild("KismetDebugger_Locals", {0, 0}, false, ImGuiWindowFlags_HorizontalScrollbar); - if (context && context->stack->Node() == current_fn) - { - UFunction* node = context->stack->Node(); - int property_count{}; - for (FProperty* property : TFieldRange(current_fn, EFieldIterationFlags::IncludeDeprecated)) - { - FString text{}; - auto container_ptr = property->ContainerPtrToValuePtr(context->stack->Locals()); - property->ExportTextItem(text, container_ptr, container_ptr, static_cast(node), NULL); - - ImGui::Text("%s = %S", to_string(property->GetName()).c_str(), *text); - try_rendering_property_context_menu(node, property_count, property, EX_Nothing, context->stack->Locals()); - ++property_count; - } - } - else - { - for (FProperty* property : TFieldRange(current_fn, EFieldIterationFlags::IncludeDeprecated)) - { - ImGui::Text("%s", to_string(property->GetName()).c_str()); - } - } - ImGui::EndChild(); - } - } - ImGui::EndChild(); - - // left pane - ImGui::SameLine(0, 8); - - ImGui::BeginGroup(); - - render_nav_bar(m_split_right - 20); - - ImGui::BeginChild("KismetDebugger_Disassembly", {m_split_right - 20 , -24.0f}, true); - - - if (current_fn) - { - ScriptRenderContext render_ctx{context, current_fn, position_updated, m_breakpoints}; - render_ctx.render(); - } - - ImGui::EndChild(); - - ImGui::EndGroup(); // left pane - - - if (is_hooked) - { - if (auto ctx = context) - { - m_last_code = ctx->stack->Code(); - } - } - - if (!is_hooked || !context) - m_last_code = nullptr; - } - - auto Debugger::render_nav_bar(float width) -> void - { - float start = ImGui::GetCursorPosX(); - - bool disable_back = m_nav_history_index < 0; - if (disable_back) - ImGui::BeginDisabled(); - if (ImGui::Button("back")) - { - m_nav_history_index--; - m_nav_function = m_nav_history_index < 0 ? "" : m_nav_history[m_nav_history_index]; - } - if (disable_back) - ImGui::EndDisabled(); - - ImGui::SameLine(); - bool disable_forward = m_nav_history_index + 1 >= m_nav_history.size(); - if (disable_forward) - ImGui::BeginDisabled(); - if (ImGui::Button("forward")) - { - m_nav_history_index++; - m_nav_function = m_nav_history[m_nav_history_index]; - } - if (disable_forward) - ImGui::EndDisabled(); - - - // https://github.com/ocornut/imgui/issues/718#issuecomment-1249822993 - ImGui::SameLine(); - ImGui::SetNextItemWidth(width - (ImGui::GetCursorPosX() - start)); - const bool is_input_text_enter_pressed = ImGui::InputText("##input", &m_nav_function, ImGuiInputTextFlags_EnterReturnsTrue); - const bool is_input_text_active = ImGui::IsItemActive(); - const bool is_input_text_activated = ImGui::IsItemActivated(); - - if (is_input_text_activated) - ImGui::OpenPopup("##popup"); - - { - ImGui::SetNextWindowPos(ImVec2(ImGui::GetItemRectMin().x, ImGui::GetItemRectMax().y)); - //ImGui::SetNextWindowSize({ ImGui::GetItemRectSize().x, 0 }); - if (ImGui::BeginPopup("##popup", ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_ChildWindow)) - { - int n = 0; - - UObjectGlobals::ForEachUObject([&](UObject* object, ...) { - if (object->IsA()) - { - std::string full_name = to_string(object->GetFullName()); - - auto it = std::search( - full_name.begin(), full_name.end(), - m_nav_function.begin(), m_nav_function.end(), - [](auto ch1, auto ch2) { return std::toupper(ch1) == std::toupper(ch2); } - ); - - if (it != full_name.end()) - { - const char* name_c_str = full_name.c_str(); - - if (ImGui::Selectable(name_c_str)) - { - ImGui::ClearActiveID(); - m_function_name_map[full_name] = static_cast(object); - m_nav_function = full_name; - } - - if (n++ > 10) - { - ImGui::Text("..."); - return LoopAction::Break; - } - } - } - return LoopAction::Continue; - }); - - if (!n) - ImGui::Text("[nothing]"); - - if (is_input_text_enter_pressed || (!is_input_text_active && !ImGui::IsWindowFocused())) - { - ImGui::CloseCurrentPopup(); - // TODO check valid - - nav_to_function(m_nav_function); - } - - ImGui::EndPopup(); - } - } - } - - auto Debugger::nav_to_function(UFunction* fn) -> void - { - std::string full_name = to_string(fn->GetFullName()); - - m_function_name_map[full_name] = fn; - - nav_to_function(full_name); - } - - // function name must already be in name map - auto Debugger::nav_to_function(std::string full_name) -> void - { - if (m_nav_history_index >= 0 && m_nav_history[m_nav_history_index] == full_name) - return; // already at fn, do not add another entry to the stack - - m_nav_function = full_name; - - m_nav_history_index++; - if (m_nav_history_index >= m_nav_history.size()) - m_nav_history.push_back(m_nav_function); - else - m_nav_history[m_nav_history_index] = m_nav_function; - } - - - auto expr_to_string(EExprToken expr) -> const char* - { - switch (expr) - { - case EX_LocalVariable: - return "EX_LocalVariable"; - case EX_InstanceVariable: - return "EX_InstanceVariable"; - case EX_DefaultVariable: - return "EX_DefaultVariable"; - case EX_Return: - return "EX_Return"; - case EX_Jump: - return "EX_Jump"; - case EX_JumpIfNot: - return "EX_JumpIfNot"; - case EX_Assert: - return "EX_Assert"; - case EX_Nothing: - return "EX_Nothing"; - case EX_NothingInt32: - return "EX_NothingInt32"; - case EX_Let: - return "EX_Let"; - case EX_BitFieldConst: - return "EX_BitFieldConst"; - case EX_ClassContext: - return "EX_ClassContext"; - case EX_MetaCast: - return "EX_MetaCast"; - case EX_LetBool: - return "EX_LetBool"; - case EX_EndParmValue: - return "EX_EndParmValue"; - case EX_EndFunctionParms: - return "EX_EndFunctionParms"; - case EX_Self: - return "EX_Self"; - case EX_Skip: - return "EX_Skip"; - case EX_Context: - return "EX_Context"; - case EX_Context_FailSilent: - return "EX_Context_FailSilent"; - case EX_VirtualFunction: - return "EX_VirtualFunction"; - case EX_FinalFunction: - return "EX_FinalFunction"; - case EX_IntConst: - return "EX_IntConst"; - case EX_FloatConst: - return "EX_FloatConst"; - case EX_StringConst: - return "EX_StringConst"; - case EX_ObjectConst: - return "EX_ObjectConst"; - case EX_NameConst: - return "EX_NameConst"; - case EX_RotationConst: - return "EX_RotationConst"; - case EX_VectorConst: - return "EX_VectorConst"; - case EX_Vector3fConst: - return "EX_Vector3fConst"; - case EX_ByteConst: - return "EX_ByteConst"; - case EX_IntZero: - return "EX_IntZero"; - case EX_IntOne: - return "EX_IntOne"; - case EX_True: - return "EX_True"; - case EX_False: - return "EX_False"; - case EX_TextConst: - return "EX_TextConst"; - case EX_NoObject: - return "EX_NoObject"; - case EX_TransformConst: - return "EX_TransformConst"; - case EX_IntConstByte: - return "EX_IntConstByte"; - case EX_NoInterface: - return "EX_NoInterface"; - case EX_DynamicCast: - return "EX_DynamicCast"; - case EX_StructConst: - return "EX_StructConst"; - case EX_EndStructConst: - return "EX_EndStructConst"; - case EX_SetArray: - return "EX_SetArray"; - case EX_EndArray: - return "EX_EndArray"; - case EX_PropertyConst: - return "EX_PropertyConst"; - case EX_UnicodeStringConst: - return "EX_UnicodeStringConst"; - case EX_Int64Const: - return "EX_Int64Const"; - case EX_UInt64Const: - return "EX_UInt64Const"; - case EX_DoubleConst: - return "EX_DoubleConst"; - case EX_Cast: // EX_PrimitiveCast in 4.27 - return "EX_Cast"; - case EX_SetSet: - return "EX_SetSet"; - case EX_EndSet: - return "EX_EndSet"; - case EX_SetMap: - return "EX_SetMap"; - case EX_EndMap: - return "EX_EndMap"; - case EX_SetConst: - return "EX_SetConst"; - case EX_EndSetConst: - return "EX_EndSetConst"; - case EX_MapConst: - return "EX_MapConst"; - case EX_EndMapConst: - return "EX_EndMapConst"; - case EX_StructMemberContext: - return "EX_StructMemberContext"; - case EX_LetMulticastDelegate: - return "EX_LetMulticastDelegate"; - case EX_LetDelegate: - return "EX_LetDelegate"; - case EX_LocalVirtualFunction: - return "EX_LocalVirtualFunction"; - case EX_LocalFinalFunction: - return "EX_LocalFinalFunction"; - case EX_LocalOutVariable: - return "EX_LocalOutVariable"; - case EX_DeprecatedOp4A: - return "EX_DeprecatedOp4A"; - case EX_InstanceDelegate: - return "EX_InstanceDelegate"; - case EX_PushExecutionFlow: - return "EX_PushExecutionFlow"; - case EX_PopExecutionFlow: - return "EX_PopExecutionFlow"; - case EX_ComputedJump: - return "EX_ComputedJump"; - case EX_PopExecutionFlowIfNot: - return "EX_PopExecutionFlowIfNot"; - case EX_Breakpoint: - return "EX_Breakpoint"; - case EX_InterfaceContext: - return "EX_InterfaceContext"; - case EX_ObjToInterfaceCast: - return "EX_ObjToInterfaceCast"; - case EX_EndOfScript: - return "EX_EndOfScript"; - case EX_CrossInterfaceCast: - return "EX_CrossInterfaceCast"; - case EX_InterfaceToObjCast: - return "EX_InterfaceToObjCast"; - case EX_WireTracepoint: - return "EX_WireTracepoint"; - case EX_SkipOffsetConst: - return "EX_SkipOffsetConst"; - case EX_AddMulticastDelegate: - return "EX_AddMulticastDelegate"; - case EX_ClearMulticastDelegate: - return "EX_ClearMulticastDelegate"; - case EX_Tracepoint: - return "EX_Tracepoint"; - case EX_LetObj: - return "EX_LetObj"; - case EX_LetWeakObjPtr: - return "EX_LetWeakObjPtr"; - case EX_BindDelegate: - return "EX_BindDelegate"; - case EX_RemoveMulticastDelegate: - return "EX_RemoveMulticastDelegate"; - case EX_CallMulticastDelegate: - return "EX_CallMulticastDelegate"; - case EX_LetValueOnPersistentFrame: - return "EX_LetValueOnPersistentFrame"; - case EX_ArrayConst: - return "EX_ArrayConst"; - case EX_EndArrayConst: - return "EX_EndArrayConst"; - case EX_SoftObjectConst: - return "EX_SoftObjectConst"; - case EX_CallMath: - return "EX_CallMath"; - case EX_SwitchValue: - return "EX_SwitchValue"; - case EX_InstrumentationEvent: - return "EX_InstrumentationEvent"; - case EX_ArrayGetByRef: - return "EX_ArrayGetByRef"; - case EX_ClassSparseDataVariable: - return "EX_ClassSparseDataVariable"; - case EX_FieldPathConst: - return "EX_FieldPathConst"; - case EX_AutoRtfmTransact: - return "EX_AutoRtfmTransact"; - case EX_AutoRtfmStopTransact: - return "EX_AutoRtfmStopTransact"; - case EX_AutoRtfmAbortIfNot: - return "EX_AutoRtfmAbortIfNot"; - case EX_Max: - return "EX_Max"; - } - return "unknown"; - }; - - ScriptRenderContext::ScriptRenderContext(std::optional& paused_context, UFunction* fn, bool scroll_to_active, BreakpointStore& breakpoints) - : m_paused_context(paused_context), - m_fn(fn), - m_scroll_to_active(scroll_to_active), - m_breakpoints(breakpoints), - - m_script(fn->GetScript().GetData()), - m_script_size(fn->GetScript().Num()), - m_cur((paused_context && paused_context->stack->Node() == fn) ? paused_context->stack->Code() - paused_context->stack->Node()->GetScript().GetData() - 1 : -1) - { - } - ScriptRenderContext::~ScriptRenderContext() - { - } - - auto ScriptRenderContext::render() -> void - { - while (m_index < m_script_size) render_expr(); - } - - auto ScriptRenderContext::render_property() - { - FProperty* property = (FProperty*)read_object(); - if (property) - { - ImGui::Text("%s", to_string(property->GetName()).c_str()); - if (m_paused_context.has_value()) - { - try_rendering_property_context_menu(m_fn, m_index, property, m_current_expr, m_paused_context); - } - /* - if (ImGui::IsItemHovered()) - { - ImGui::BeginTooltip(); - - // TODO: this depends on context and isn't always a local - FString text{}; - auto container_ptr = property->ContainerPtrToValuePtr(stack->Locals()); - property->ExportTextItem(text, container_ptr, container_ptr, static_cast(stack->Node()), NULL); - - ImGui::Text("= %S", *text); - - ImGui::EndTooltip(); - } - */ - } - else - { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255,0,0,255)); - ImGui::Text("NULL property"); - ImGui::PopStyleColor(); - } - } - auto ScriptRenderContext::render_expr() -> EExprToken - { - bool active = m_index == m_cur; - size_t expr_index = m_index; - - m_current_expr = static_cast(read()); - - //std::cout << "rendering (" << std::hex << unsigned(m_current_expr) << std::dec << ") @ " << (index - 1) << " " << expr_to_string(m_current_expr) << std::endl; - - - ImGui::SetCursorPosX(ImGui::GetCursorStartPos().x); - auto label = std::format("{}", expr_index); - ImGui::PushStyleVar(ImGuiStyleVar_SelectableTextAlign, ImVec2(1.0f, 0.5f)); - bool is_breakpoint = m_breakpoints.has_breakpoint(m_fn, expr_index); - if (ImGui::Selectable(label.c_str(), is_breakpoint, 0, {30, 0})) - { - if (is_breakpoint) - m_breakpoints.remove_breakpoint(m_fn, expr_index); - else - m_breakpoints.add_breakpoint(m_fn, expr_index); - } - ImGui::PopStyleVar(); - ImGui::SameLine(); - ImGui::SetCursorPosX(50 + m_indent * 20.0f); - - if (active) - { - ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(255, 255, 0, 255)); - if (m_scroll_to_active) - ImGui::SetScrollHereY(); - } - ImGui::Text("%s", expr_to_string(m_current_expr)); - if (active) - ImGui::PopStyleColor(); - - m_indent++; - // Note: we use the ImGui::Dummy() lines below to avoid a warning related to setting cursor pos without following with an imgui item - ImGui::SetCursorPosX(50 + m_indent * 20.0f); - - switch (m_current_expr) - { - case EX_Cast: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - render_expr(); - break; - } - case EX_ObjToInterfaceCast: - case EX_CrossInterfaceCast: - case EX_InterfaceToObjCast: - { - ImGui::Dummy(ImVec2(0, 0)); - (UClass*)read_object(); - render_expr(); - break; - } - case EX_Let: - { - render_property(); - } - case EX_LetObj: - case EX_LetWeakObjPtr: - case EX_LetBool: - case EX_LetDelegate: - case EX_LetMulticastDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - render_expr(); - break; - } - case EX_LetValueOnPersistentFrame: - { - render_property(); - render_expr(); - break; - } - case EX_StructMemberContext: - { - ImGui::Dummy(ImVec2(0, 0)); - (FProperty*)read_object(); // struct member expr. - render_expr(); - break; - } - case EX_Jump: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - break; - } - case EX_ComputedJump: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - case EX_LocalVariable: - case EX_InstanceVariable: - case EX_DefaultVariable: - case EX_LocalOutVariable: - case EX_ClassSparseDataVariable: - case EX_PropertyConst: - { - this->render_property(); - break; - } - case EX_InterfaceContext: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - case EX_PushExecutionFlow: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - break; - } - case EX_NothingInt32: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - break; - } - case EX_Nothing: - case EX_EndOfScript: - case EX_EndFunctionParms: - case EX_EndStructConst: - case EX_EndArray: - case EX_EndArrayConst: - case EX_EndSet: - case EX_EndMap: - case EX_EndSetConst: - case EX_EndMapConst: - case EX_IntZero: - case EX_IntOne: - case EX_True: - case EX_False: - case EX_NoObject: - case EX_NoInterface: - case EX_Self: - case EX_EndParmValue: - case EX_PopExecutionFlow: - case EX_DeprecatedOp4A: - { - ImGui::Dummy(ImVec2(0, 0)); - break; - } - case EX_WireTracepoint: - case EX_Tracepoint: - { - ImGui::Dummy(ImVec2(0, 0)); - break; - } - case EX_Breakpoint: - { - ImGui::Dummy(ImVec2(0, 0)); - break; - } - /* - case EX_InstrumentationEvent: TODO ?????? - { - if (Script[iCode] == EScriptInstrumentation::InlineEvent) - { - iCode += sizeof(FScriptName); - } - iCode += sizeof(uint8); - break; - } - */ - case EX_Return: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - case EX_CallMath: - case EX_LocalFinalFunction: - case EX_FinalFunction: - { - ImGui::Dummy(ImVec2(0, 0)); - (UFunction*)read_object(); - while (render_expr() != EX_EndFunctionParms); - break; - } - case EX_LocalVirtualFunction: - case EX_VirtualFunction: - { - //std::cout << "reading " << sizeof(FName) << " bytes @ " << index << std::endl; - - /* - for (int i = 0; i < script_size; ++i) - std::cout << script[i] << " "; - std::cout << std::endl; - */ - - /* - for (int i = 0; i < script_size; ++i) - std::cout << std::hex << std::setfill('0') << std::setw(2) << unsigned(script[i]) << " "; - std::cout << std::endl; - */ - - ImGui::Dummy(ImVec2(0, 0)); - FName n = read_name(); - while (render_expr() != EX_EndFunctionParms); // Parms. - break; - } - case EX_CallMulticastDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - (UFunction*)read_object(); - while (render_expr() != EX_EndFunctionParms); // Parms. - break; - } - case EX_BitFieldConst: - { - ImGui::Dummy(ImVec2(0, 0)); - (FProperty*)read_object(); - read(); - break; - } - case EX_ClassContext: - case EX_Context: - case EX_Context_FailSilent: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // Object expression. - read(); - (FField*)read_object(); // Property corresponding to the r-value data, in case the l-value needs to be mem-zero'd - render_expr(); // Context expression. - break; - } - case EX_AddMulticastDelegate: - case EX_RemoveMulticastDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // Delegate property to assign to - render_expr(); // Delegate to add to the MC delegate for broadcast - break; - } - case EX_ClearMulticastDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // Delegate property to clear - break; - } - case EX_IntConst: - { - ImGui::Text("%d", read()); - break; - } - case EX_Int64Const: - { - ImGui::Text("%lld", read()); - break; - } - case EX_UInt64Const: - { - ImGui::Text("%llu", read()); - break; - } - case EX_DoubleConst: - { - ImGui::Text("%f", read()); - break; - } - case EX_SkipOffsetConst: - { - ImGui::Text("%u", read()); // TODO jump - break; - } - case EX_FloatConst: - { - ImGui::Text("%f", read()); - break; - } - case EX_StringConst: - { - ImGui::Dummy(ImVec2(0, 0)); - while (read()); - break; - } - case EX_UnicodeStringConst: - { - ImGui::Dummy(ImVec2(0, 0)); - while (read()); - break; - } - case EX_TextConst: - { - ImGui::Dummy(ImVec2(0, 0)); - switch (read()) - { - case 0: // Empty - break; - case 1: // LocalizedText - render_expr(); - render_expr(); - render_expr(); - break; - case 2: // InvariantText - render_expr(); - break; - case 3: // LiteralString - render_expr(); - break; - case 4: // StringTableEntry - read_object(); - render_expr(); - render_expr(); - break; - } - break; - } - case EX_ObjectConst: - { - ImGui::Dummy(ImVec2(0, 0)); - read_object(); - break; - } - case EX_SoftObjectConst: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - case EX_FieldPathConst: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - case EX_NameConst: - { - ImGui::Dummy(ImVec2(0, 0)); - FName n = read_name(); - break; - } - case EX_RotationConst: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - read(); - read(); - break; - } - case EX_Vector3fConst: - case EX_VectorConst: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - read(); - read(); - break; - } - case EX_TransformConst: - { - ImGui::Dummy(ImVec2(0, 0)); - // Rotation - read(); - read(); - read(); - read(); - // Translation - read(); - read(); - read(); - // Scale - read(); - read(); - read(); - break; - } - case EX_StructConst: - { - ImGui::Dummy(ImVec2(0, 0)); - (UScriptStruct*)read_object(); // Struct. - read(); - while (render_expr() != EX_EndStructConst); - break; - } - case EX_SetArray: - { - ImGui::Dummy(ImVec2(0, 0)); - // If not loading, or its a newer version - //if((!GetLinker()) || !Ar.IsLoading() || (Ar.UE4Ver() >= VER_UE4_CHANGE_SETARRAY_BYTECODE)) - //{ - // Array property to assign to - EExprToken TargetToken = render_expr(); - //} - //else - //{ - // Array Inner Prop - //(FProperty*)read_object(); - //} - - while (render_expr() != EX_EndArray); - break; - } - case EX_SetSet: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // set property - read(); - while (render_expr() != EX_EndSet); - break; - } - case EX_SetMap: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // map property - read(); - while (render_expr() != EX_EndMap); - break; - } - case EX_ArrayConst: - { - ImGui::Dummy(ImVec2(0, 0)); - (FProperty*)read_object(); // Inner property - read(); - while (render_expr() != EX_EndArrayConst); - break; - } - case EX_SetConst: - { - ImGui::Dummy(ImVec2(0, 0)); - (FProperty*)read_object(); // Inner property - read(); - while (render_expr() != EX_EndSetConst); - break; - } - case EX_MapConst: - { - ImGui::Dummy(ImVec2(0, 0)); - (FProperty*)read_object(); // Key property - (FProperty*)read_object(); // Val property - read(); - while (render_expr() != EX_EndMapConst); - break; - } - case EX_ByteConst: - case EX_IntConstByte: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - break; - } - case EX_MetaCast: - { - ImGui::Dummy(ImVec2(0, 0)); - (UClass*)read_object(); - render_expr(); - break; - } - case EX_DynamicCast: - { - ImGui::Dummy(ImVec2(0, 0)); - (UClass*)read_object(); - render_expr(); - break; - } - case EX_JumpIfNot: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - render_expr(); // Boolean expr. - break; - } - case EX_PopExecutionFlowIfNot: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); // Boolean expr. - break; - } - case EX_Assert: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - read(); - render_expr(); // Assert expr. - break; - } - case EX_Skip: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - render_expr(); // Expression to possibly skip. - break; - } - case EX_InstanceDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - FName n = read_name(); - break; - } - case EX_BindDelegate: - { - ImGui::Dummy(ImVec2(0, 0)); - FName n = read_name(); - render_expr(); // Delegate property to assign to - render_expr(); - break; - } - case EX_SwitchValue: - { - ImGui::Dummy(ImVec2(0, 0)); - auto cases = read(); // number of cases, without default one - auto end = read(); // Code offset, go to it, when done. - render_expr(); //index term - - for (uint16 i = 0; i < cases; ++i) - { - render_expr(); // case index value term - auto next_case = read(); // offset to the next case - render_expr(); // case term - } - - render_expr(); //default term - break; - } - case EX_ArrayGetByRef: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - render_expr(); - break; - } - case EX_AutoRtfmTransact: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - read(); - while (render_expr() != EX_AutoRtfmStopTransact); - break; - } - case EX_AutoRtfmStopTransact: - { - ImGui::Dummy(ImVec2(0, 0)); - read(); - read(); - break; - } - case EX_AutoRtfmAbortIfNot: - { - ImGui::Dummy(ImVec2(0, 0)); - render_expr(); - break; - } - default: - { - // This should never occur. - //UE_LOG(LogScriptSerialization, Warning, TEXT("Error: Unknown bytecode 0x%02X; ignoring it"), (uint8)Expr ); - std::cout << "unknown expr (" << unsigned(m_current_expr) << ") " << expr_to_string(m_current_expr) << std::endl; - ImGui::Text("unknown expr (%i) %s", unsigned(m_current_expr), expr_to_string(m_current_expr)); - break; - } - } - m_indent--; - return m_current_expr; - } - -} diff --git a/cppmods/KismetDebuggerMod/src/dllmain.cpp b/cppmods/KismetDebuggerMod/src/dllmain.cpp deleted file mode 100644 index 8db1a204..00000000 --- a/cppmods/KismetDebuggerMod/src/dllmain.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include - -#include -#include -#include -#include - -class KismetDebuggerMod : public RC::CppUserModBase -{ -private: - RC::GUI::KismetDebuggerMod::Debugger m_debugger{}; - -public: - KismetDebuggerMod() : CppUserModBase() - { - ModName = STR("KismetDebugger"); - ModVersion = STR("1.0"); - ModDescription = STR("Debugging interface for kismet bytecode"); - ModAuthors = STR("truman"); - - register_tab(STR("Kismet Debugger"), [](CppUserModBase* mod) { - UE4SS_ENABLE_IMGUI() - dynamic_cast(mod)->m_debugger.render(); - }); - } - - ~KismetDebuggerMod() override = default; -}; - -#define KISMET_DEBUGGER_MOD_API __declspec(dllexport) -extern "C" -{ - KISMET_DEBUGGER_MOD_API RC::CppUserModBase* start_mod() - { - return new KismetDebuggerMod(); - } - - KISMET_DEBUGGER_MOD_API void uninstall_mod(RC::CppUserModBase* mod) - { - delete mod; - } -} - diff --git a/cppmods/KismetDebuggerMod/xmake.lua b/cppmods/KismetDebuggerMod/xmake.lua deleted file mode 100644 index 22c3289b..00000000 --- a/cppmods/KismetDebuggerMod/xmake.lua +++ /dev/null @@ -1,6 +0,0 @@ -local projectName = "KismetDebuggerMod" - -target(projectName) - add_rules("ue4ss.mod") - add_includedirs("include") - add_files("src/dllmain.cpp", "src/KismetDebugger.cpp") \ No newline at end of file diff --git a/cppmods/xmake.lua b/cppmods/xmake.lua deleted file mode 100644 index fa55abae..00000000 --- a/cppmods/xmake.lua +++ /dev/null @@ -1 +0,0 @@ -includes("KismetDebuggerMod") diff --git a/docs/feature-overview/dumpers.md b/docs/feature-overview/dumpers.md index 3f80d75c..876d5412 100644 --- a/docs/feature-overview/dumpers.md +++ b/docs/feature-overview/dumpers.md @@ -115,8 +115,9 @@ Thanks to [OutTheShade](https://github.com/OutTheShade/UnrealMappingsDumper) for Dump all loaded actors to the file `ue4ss_static_mesh_data.csv` to generate `.umaps` in-editor. -Two prerequisites are required to load the dumped actors in-editor to reconstruct the `.umap`: -- All dumped actors (static meshes, their materials/textures) must be reconstructed in the editor -- Download `zMapGenBP.zip` from the Releases page and follow the instructions in the Readme file inside of it +Reconstructing the resulting `.umap` still requires the dumped actors, meshes, +materials, and textures to be recreated in Unreal Editor. The editor-side +reconstruction assets previously bundled upstream are not distributed by this +Linux downstream. The keybind to dump mappings is by default `Ctrl` + `Numpad 7`, and can be changed in `Mods/Keybinds/Scripts/main.lua`. diff --git a/tools/buildscripts/build.ps1 b/tools/buildscripts/build.ps1 deleted file mode 100644 index 34ad7ded..00000000 --- a/tools/buildscripts/build.ps1 +++ /dev/null @@ -1,353 +0,0 @@ -# Unified build script for RE-UE4SS on Windows -# Supports native and cross-compilation builds with various configurations - -param( - [Alias("g")] - [string]$Generator = "", # Default will be set based on what's available - [Alias("c")] - [string]$Compiler = "", - [Alias("t")] - [string]$Toolchain = "", - [Alias("b")] - [string]$BuildConfig = "Game__Shipping__Win64", - [string]$Target = "UE4SS", - [Alias("v")] - [switch]$Verbose, - [switch]$Clean, - [Alias("h")] - [switch]$Help -) - -# Colors for output -function Write-Error-Custom { Write-Host "Error: $args" -ForegroundColor Red } -function Write-Success { Write-Host $args -ForegroundColor Green } -function Write-Info { Write-Host $args -ForegroundColor Yellow } - -# Function to display usage -function Show-Usage { - @" -Usage: .\build.ps1 [OPTIONS] - -Options: - -g, --generator GENERATOR Build system generator (ninja, vs2022, vs2019) [default: auto-detect VS2022/VS2019, fallback to ninja] - -c, --compiler COMPILER Compiler to use (msvc, clang) [default: msvc when using ninja, VS default when using VS generator] - -t, --toolchain TOOLCHAIN CMake toolchain file name (without path/extension) - Example: windows-clang-cl - -b, --build-config CONFIG Build configuration [default: Game__Shipping__Win64] - --target TARGET Build target [default: UE4SS] - -v, --verbose Enable verbose output - --clean Clean build directory before building - -h, --help Display this help message - -Note: PowerShell also accepts single dash (-) prefix for all parameters - -Environment Variables: - CMAKE_PREFIX_PATH Additional CMake search paths - -Examples: - # Native build with system compiler - .\build.ps1 - - # Native build with Visual Studio 2022 - .\build.ps1 --generator vs2022 - - # Build with clang-cl - .\build.ps1 --compiler clang - - # Build specific configuration - .\build.ps1 --build-config Game__Debug__Win64 - - # Clean build with verbose output - .\build.ps1 --clean --verbose -"@ - exit 0 -} - -if ($Help) { Show-Usage } - -# Function to check if a tool is available -function Test-Tool { - param([string]$Tool) - - $command = Get-Command $Tool -ErrorAction SilentlyContinue - if (-not $command) { - Write-Error-Custom "$Tool is not installed or not in PATH" - return $false - } - return $true -} - -# Get script and project directories -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$ProjectRoot = (Get-Item "$ScriptDir\..\..").FullName - -# Check for CMake -if (-not (Test-Tool "cmake")) { exit 1 } - -# Set default generator if not specified -if (-not $Generator) { - # Check for Visual Studio installations first - $VSWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (Test-Path $VSWhere) { - $VSInstances = & $VSWhere -latest -property installationVersion - if ($VSInstances) { - $VSVersion = $VSInstances | Select-Object -First 1 - if ($VSVersion -like "17.*") { - $Generator = "vs2022" - Write-Info "Detected Visual Studio 2022, using as default generator" - } elseif ($VSVersion -like "16.*") { - $Generator = "vs2019" - Write-Info "Detected Visual Studio 2019, using as default generator" - } - } - } - - # Fall back to ninja if no VS found - if (-not $Generator) { - if (Get-Command "ninja" -ErrorAction SilentlyContinue) { - $Generator = "ninja" - Write-Info "Using Ninja as default generator" - # When using ninja on Windows without a compiler specified, default to MSVC - if (-not $Compiler -and -not $Toolchain) { - $Compiler = "msvc" - Write-Info "Using MSVC as default compiler with Ninja" - } - } else { - Write-Error-Custom "No suitable build generator found. Please install Visual Studio or Ninja." - exit 1 - } - } -} - -# Validate and convert generator to CMake format -$CMakeGenerator = switch ($Generator) { - "ninja" { - if (-not (Test-Tool "ninja")) { exit 1 } - "Ninja" - } - "vs2022" { "Visual Studio 17 2022" } - "vs2019" { "Visual Studio 16 2019" } - default { - Write-Error-Custom "Invalid generator: $Generator" - exit 1 - } -} - -# Warn if using VS generator with clang compiler -if ($Compiler -eq "clang" -and $Generator -match "vs") { - Write-Info "Note: Visual Studio generators work best with MSVC. For clang builds, consider using Ninja." - Write-Info "Example: .\build.ps1 -g ninja -c clang" -} - -# Set up build directory name -$BuildDirPrefix = "build" -if ($Toolchain) { - $BuildDirPrefix = "${BuildDirPrefix}_${Toolchain}" -} -if ($Compiler) { - $BuildDirPrefix = "${BuildDirPrefix}_${Compiler}" -} -$BuildDir = "${BuildDirPrefix}_${BuildConfig}" - -# Set up CMake arguments -$CMakeArgs = @( - $ProjectRoot, - "-G", $CMakeGenerator, - "-DCMAKE_BUILD_TYPE=$BuildConfig", - "-DCMAKE_EXPORT_COMPILE_COMMANDS=ON" -) - -# Add architecture for Visual Studio generators -if ($Generator -like "vs*") { - $CMakeArgs += "-A", "x64" - - # For VS generators with clang, use ClangCL toolset - if ($Compiler -eq "clang") { - # Check if ClangCL toolset is available - $ClangCLAvailable = $false - $VSInstallDir = & "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" -latest -property installationPath - if ($VSInstallDir) { - $ClangCLPath = Join-Path $VSInstallDir "VC\Tools\Llvm\x64\bin\clang-cl.exe" - if (Test-Path $ClangCLPath) { - $ClangCLAvailable = $true - } - } - - if ($ClangCLAvailable) { - $CMakeArgs += "-T", "ClangCL" - Write-Info "Using ClangCL toolset with Visual Studio generator" - } else { - Write-Info "ClangCL toolset not found in Visual Studio. Make sure you have installed the 'Clang compiler for Windows' component." - Write-Info "Attempting to use system clang-cl instead..." - # Don't set toolset, but specify compilers directly - if (Test-Tool "clang-cl") { - $CMakeArgs += "-DCMAKE_C_COMPILER=clang-cl", "-DCMAKE_CXX_COMPILER=clang-cl" - } else { - Write-Error-Custom "Neither VS ClangCL toolset nor system clang-cl found" - exit 1 - } - } - } -} - -# Set up compiler if specified -if ($Compiler) { - switch ($Compiler) { - "msvc" { - # Explicitly set MSVC compiler - if ($Generator -notlike "vs*") { - if (Test-Tool "cl") { - $CMakeArgs += "-DCMAKE_C_COMPILER=cl", "-DCMAKE_CXX_COMPILER=cl" - Write-Info "Using MSVC (cl) compiler" - } else { - # Try to find cl.exe in Visual Studio installation - $VSWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" - if (Test-Path $VSWhere) { - $VSPath = & $VSWhere -latest -property installationPath - if ($VSPath) { - $CLPath = Get-ChildItem -Path "$VSPath\VC\Tools\MSVC" -Recurse -Filter "cl.exe" | - Where-Object { $_.FullName -like "*Hostx64\x64*" } | - Select-Object -First 1 - if ($CLPath) { - $CMakeArgs += "-DCMAKE_C_COMPILER=`"$($CLPath.FullName)`"", "-DCMAKE_CXX_COMPILER=`"$($CLPath.FullName)`"" - Write-Info "Using MSVC from: $($CLPath.DirectoryName)" - } else { - Write-Error-Custom "MSVC cl.exe not found in Visual Studio installation" - exit 1 - } - } - } else { - Write-Error-Custom "MSVC not found. Please ensure Visual Studio is installed." - exit 1 - } - } - } - } - "clang" { - # For non-VS generators, specify compiler directly - if ($Generator -notlike "vs*") { - if (Test-Tool "clang-cl") { - $CMakeArgs += "-DCMAKE_C_COMPILER=clang-cl", "-DCMAKE_CXX_COMPILER=clang-cl" - Write-Info "Using clang-cl for native Windows build" - } elseif (Test-Tool "clang") { - $CMakeArgs += "-DCMAKE_C_COMPILER=clang", "-DCMAKE_CXX_COMPILER=clang++" - Write-Info "Using clang/clang++ for build" - } else { - Write-Error-Custom "Clang not found" - exit 1 - } - } - } - default { - Write-Error-Custom "Invalid compiler: $Compiler" - exit 1 - } - } -} - -# Set up toolchain if specified -if ($Toolchain) { - $ToolchainFile = "$ProjectRoot\cmake\toolchains\${Toolchain}-toolchain.cmake" - if (-not (Test-Path $ToolchainFile)) { - Write-Error-Custom "Toolchain file not found: $ToolchainFile" - Write-Info "Available toolchains:" - Get-ChildItem "$ProjectRoot\cmake\toolchains\" -Filter "*.cmake" | - ForEach-Object { " " + $_.BaseName -replace '-toolchain$', '' } - exit 1 - } - $CMakeArgs += "-DCMAKE_TOOLCHAIN_FILE=$ToolchainFile" -} - -# Print build configuration -Write-Host "========================================" -Write-Info "Building RE-UE4SS" -Write-Host "========================================" -Write-Host "Generator: $Generator" -Write-Host "Compiler: $(if ($Compiler) { $Compiler } else { 'system default' })" -Write-Host "Toolchain: $(if ($Toolchain) { $Toolchain } else { 'none' })" -Write-Host "Configuration: $BuildConfig" -Write-Host "Target: $Target" -Write-Host "Build dir: $BuildDir" -Write-Host "========================================" - -# Clean build directory if requested -if ($Clean) { - Write-Info "Cleaning build directory..." - if (Test-Path $BuildDir) { - Remove-Item -Recurse -Force $BuildDir - Write-Success "Build directory cleaned successfully!" - } else { - Write-Info "Build directory does not exist, nothing to clean" - } -} - -# Create and enter build directory -New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null -Push-Location $BuildDir - -# Create .gitignore to prevent accidental commits of build artifacts -$GitIgnorePath = ".gitignore" -if (-not (Test-Path $GitIgnorePath)) { - @" -# Automatically generated .gitignore for build directory -# This prevents accidental commits of build artifacts - -# Ignore everything in this directory -* -"@ | Out-File -FilePath $GitIgnorePath -Encoding UTF8 - Write-Info "Created .gitignore in build directory" -} - -try { - # Configure with CMake - Write-Info "Configuring with CMake..." - if ($Verbose) { - Write-Host "CMake command: cmake $($CMakeArgs -join ' ')" - } - - & cmake $CMakeArgs - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "CMake configuration failed!" - exit 1 - } - - # Build - Write-Info "Building target: $Target..." - $BuildArgs = @("--build", ".", "--target", $Target) - - # For multi-configuration generators (Visual Studio), specify the configuration - if ($Generator -match "vs") { - $BuildArgs += "--config", $BuildConfig - } - - if ($Verbose) { - # The '--' argument tells cmake to pass subsequent flags to the native build tool (Ninja, MSBuild, etc.) - $BuildArgs += "--" - - # Add the generator-specific verbosity flag - $BuildArgs += switch ($Generator) { - "ninja" { "-v" } - default { "/verbosity:detailed" } - } - } - - & cmake $BuildArgs - if ($LASTEXITCODE -ne 0) { - Write-Error-Custom "Build failed!" - exit 1 - } -} -finally { - # Return to original directory - Pop-Location -} - -Write-Host "" -Write-Success "========================================" -Write-Success "Build completed successfully!" -Write-Success "Configuration: $BuildConfig" -Write-Success "Output is in: $BuildDir" -if (Test-Path "$BuildDir\compile_commands.json") { - Write-Success "Compile commands: $BuildDir\compile_commands.json" -} -Write-Success "========================================" \ No newline at end of file diff --git a/tools/buildscripts/build.sh b/tools/buildscripts/build.sh deleted file mode 100644 index 2b7931e9..00000000 --- a/tools/buildscripts/build.sh +++ /dev/null @@ -1,305 +0,0 @@ -#!/bin/bash - -# Unified build script for RE-UE4SS -# Supports native and cross-compilation builds with various configurations - -set -e - -# Default values -GENERATOR="ninja" -COMPILER="" -TOOLCHAIN="" -BUILD_CONFIG="Game__Shipping__Win64" -TARGET="UE4SS" -VERBOSE=0 -CLEAN=0 - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Function to print colored output -print_error() { echo -e "${RED}Error: $1${NC}" >&2; } -print_success() { echo -e "${GREEN}$1${NC}"; } -print_info() { echo -e "${YELLOW}$1${NC}"; } - -# Function to display usage -usage() { - cat << EOF -Usage: $0 [OPTIONS] - -Options: - -g, --generator GENERATOR Build system generator (ninja, make) [default: ninja] - -c, --compiler COMPILER Compiler to use (gcc, clang, msvc) [default: system default] - -t, --toolchain TOOLCHAIN CMake toolchain file name (without path/extension) - Examples: xwin-clang, xwin-clang-cl, msvc-wine, clang-wine - -b, --build-config CONFIG Build configuration [default: Game__Shipping__Win64] - --target TARGET Build target [default: UE4SS] - -v, --verbose Enable verbose output - --clean Clean build directory before building - -h, --help Display this help message - -Environment Variables: - XWIN_DIR Path to xwin directory (for xwin toolchains) - WINE_PREFIX Wine prefix path (for wine toolchains) - CMAKE_PREFIX_PATH Additional CMake search paths - -Examples: - # Native build with system compiler - $0 - - # Native build with clang - $0 --compiler clang - - # Cross-compile with xwin clang-cl - $0 --toolchain xwin-clang-cl - - # Cross-compile with MSVC under Wine - $0 --toolchain msvc-wine - - # Build specific configuration - $0 --build-config Game__Debug__Win64 - - # Clean build with make generator - $0 --generator make --clean -EOF - exit 0 -} - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - -g|--generator) - GENERATOR="$2" - shift 2 - ;; - -c|--compiler) - COMPILER="$2" - shift 2 - ;; - -t|--toolchain) - TOOLCHAIN="$2" - shift 2 - ;; - -b|--build-config) - BUILD_CONFIG="$2" - shift 2 - ;; - --target) - TARGET="$2" - shift 2 - ;; - -v|--verbose) - VERBOSE=1 - shift - ;; - --clean) - CLEAN=1 - shift - ;; - -h|--help) - usage - ;; - *) - print_error "Unknown option: $1" - usage - ;; - esac -done - -# Function to check if a tool is available -check_tool() { - if ! command -v "$1" &> /dev/null; then - print_error "$1 is not installed or not in PATH" - return 1 - fi - return 0 -} - -# Validate generator -case $GENERATOR in - ninja) - check_tool ninja || exit 1 - CMAKE_GENERATOR="Ninja" - ;; - make) - check_tool make || exit 1 - CMAKE_GENERATOR="Unix Makefiles" - ;; - *) - print_error "Invalid generator: $GENERATOR" - exit 1 - ;; -esac - -# Get script and project directories -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" - -# Use custom CMake if available -if [ -x "$HOME/.local/bin/cmake" ]; then - CMAKE_BIN="$HOME/.local/bin/cmake" -else - CMAKE_BIN="cmake" -fi -check_tool "$CMAKE_BIN" || exit 1 - -# Set up build directory name -BUILD_DIR_PREFIX="build" -if [ -n "$TOOLCHAIN" ]; then - BUILD_DIR_PREFIX="${BUILD_DIR_PREFIX}_${TOOLCHAIN}" -fi -if [ -n "$COMPILER" ]; then - BUILD_DIR_PREFIX="${BUILD_DIR_PREFIX}_${COMPILER}" -fi -BUILD_DIR="${BUILD_DIR_PREFIX}_${BUILD_CONFIG}" - -# Set up CMake arguments -CMAKE_ARGS=( - "$PROJECT_ROOT" - -G "$CMAKE_GENERATOR" - -DCMAKE_BUILD_TYPE="$BUILD_CONFIG" - -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -) - -# Set up compiler if specified -if [ -n "$COMPILER" ]; then - case $COMPILER in - gcc) - check_tool gcc || exit 1 - check_tool g++ || exit 1 - export CC=gcc - export CXX=g++ - ;; - clang) - check_tool clang || exit 1 - check_tool clang++ || exit 1 - export CC=clang - export CXX=clang++ - ;; - msvc) - if [ -z "$TOOLCHAIN" ]; then - print_error "MSVC requires a toolchain (e.g., --toolchain msvc-wine)" - exit 1 - fi - ;; - *) - print_error "Invalid compiler: $COMPILER" - exit 1 - ;; - esac -fi - -# Set up toolchain if specified -if [ -n "$TOOLCHAIN" ]; then - TOOLCHAIN_FILE="$PROJECT_ROOT/cmake/toolchains/${TOOLCHAIN}-toolchain.cmake" - if [ ! -f "$TOOLCHAIN_FILE" ]; then - print_error "Toolchain file not found: $TOOLCHAIN_FILE" - print_info "Available toolchains:" - ls -1 "$PROJECT_ROOT/cmake/toolchains/" | grep -E '\.cmake$' | sed 's/-toolchain\.cmake$//' | sed 's/^/ /' - exit 1 - fi - CMAKE_ARGS+=(-DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN_FILE") - - # Handle toolchain-specific requirements - case $TOOLCHAIN in - xwin-*) - # Check xwin directory - XWIN_DIR="${XWIN_DIR:-$PROJECT_ROOT/xwin}" - if [ ! -d "$XWIN_DIR" ]; then - print_error "xwin directory not found at $XWIN_DIR" - print_info "Please run: xwin --accept-license splat --output $XWIN_DIR" - print_info "Or set XWIN_DIR environment variable" - exit 1 - fi - export XWIN_DIR - - # Don't set UE4SS_PROXY_PATH - let CMake use its default behavior - ;; - *wine*) - # Check Wine - check_tool wine || exit 1 - - # Set Wine prefix if not already set - export WINE_PREFIX="${WINE_PREFIX:-$HOME/.wine}" - - # Disable Wine debug output - export WINEDEBUG="-all" - ;; - esac -fi - -# Print build configuration -echo "========================================" -print_info "Building RE-UE4SS" -echo "========================================" -echo "Generator: $GENERATOR" -echo "Compiler: ${COMPILER:-system default}" -echo "Toolchain: ${TOOLCHAIN:-none}" -echo "Configuration: $BUILD_CONFIG" -echo "Target: $TARGET" -echo "Build dir: $BUILD_DIR" -echo "========================================" - -# Clean build directory if requested -if [ $CLEAN -eq 1 ]; then - print_info "Cleaning build directory..." - rm -rf "$BUILD_DIR" -fi - -# Create and enter build directory -mkdir -p "$BUILD_DIR" -cd "$BUILD_DIR" - -# Create .gitignore to prevent accidental commits of build artifacts -if [ ! -f ".gitignore" ]; then - cat > .gitignore << 'EOF' -# Automatically generated .gitignore for build directory -# This prevents accidental commits of build artifacts - -# Ignore everything in this directory -* -EOF - print_info "Created .gitignore in build directory" -fi - -# Configure with CMake -print_info "Configuring with CMake..." -if [ $VERBOSE -eq 1 ]; then - echo "CMake command: $CMAKE_BIN ${CMAKE_ARGS[@]}" -fi - -if ! "$CMAKE_BIN" "${CMAKE_ARGS[@]}"; then - print_error "CMake configuration failed!" - exit 1 -fi - -# Build -print_info "Building target: $TARGET..." -BUILD_ARGS=() -if [ $VERBOSE -eq 1 ]; then - case $GENERATOR in - ninja) BUILD_ARGS+=(-v) ;; - make) BUILD_ARGS+=(VERBOSE=1) ;; - esac -fi - -if ! "$CMAKE_BIN" --build . --target "$TARGET" -- "${BUILD_ARGS[@]}"; then - print_error "Build failed!" - exit 1 -fi - -# Return to original directory -cd "$PROJECT_ROOT" - -echo -print_success "========================================" -print_success "Build completed successfully!" -print_success "Configuration: $BUILD_CONFIG" -print_success "Output is in: $BUILD_DIR" -if [ -f "$BUILD_DIR/compile_commands.json" ]; then - print_success "Compile commands: $BUILD_DIR/compile_commands.json" -fi -print_success "========================================" \ No newline at end of file diff --git a/tools/buildscripts/internal_build_tools/set_version.bat b/tools/buildscripts/internal_build_tools/set_version.bat deleted file mode 100644 index c2da43e4..00000000 --- a/tools/buildscripts/internal_build_tools/set_version.bat +++ /dev/null @@ -1,52 +0,0 @@ -set set_version_param_major=%1% -if not defined set_version_param_major ( - echo Was unable to call 'set_version' because no major version was passed - pause - exit -) - -set set_version_param_minor=%2% -if not defined set_version_param_minor ( - echo Was unable to call 'set_version' because no minor version was passed - pause - exit -) - -set set_version_param_hotfix=%3% -if not defined set_version_param_hotfix ( - echo Was unable to call 'set_version' because no hotfix version was passed - pause - exit -) - -set set_version_param_prerelease=%4% -if not defined set_version_param_prerelease ( - echo Was unable to call 'set_version' because no pre-release version was passed - pause - exit -) - -set set_version_param_beta=%5% -if not defined set_version_param_beta ( - echo Was unable to call 'set_version' because no beta version was passed - pause - exit -) - -if not exist generated_src ( - echo Unable to find project directory, 'generated_src'. - pause - exit /b -) - -if not exist generated_src\version.cache ( - echo Unable to find version cache file, 'generated_src\version.cache'. - pause - exit /b -) - -echo Setting version to: %set_version_param_major%.%set_version_param_minor%.%set_version_param_hotfix%.%set_version_param_prerelease%.%set_version_param_beta% - -Rem Caching the incremented version -set full_version_string=%set_version_param_major%.%set_version_param_minor%.%set_version_param_hotfix%.%set_version_param_prerelease%.%set_version_param_beta% -echo %full_version_string%> generated_src\version.cache diff --git a/tools/buildscripts/release.py b/tools/buildscripts/release.py deleted file mode 100644 index 5544b1a7..00000000 --- a/tools/buildscripts/release.py +++ /dev/null @@ -1,382 +0,0 @@ -#!/usr/bin/env python3 - -import re -import os -import shutil -import subprocess -import argparse -from datetime import datetime -import sys -import json - -class ReleaseHandler: - def __init__(self, is_dev_release, is_experimental, release_output='release'): - self.is_dev_release = is_dev_release - self.is_experimental = is_experimental - self.release_output = release_output - self.staging_dir = os.path.join(self.release_output, 'StagingDev') if self.is_dev_release else os.path.join(self.release_output, 'StagingRelease') - self.ue4ss_dir = os.path.join(self.staging_dir, 'ue4ss') - - # TODO: Move all these hardcoded values into a release config file or similar to pass into the script - # List of CPP Mods with flags indicating if they need a config folder and if they should be included in release builds - self.cpp_mods = { - 'KismetDebuggerMod': {'create_config': True, 'include_in_release': False}, - 'EventViewerMod': {'create_config': False, 'include_in_release': False}, - } - - # Lua mods to exclude from the non-dev/release version of the zip - # And remove their entries from mods files - self.lua_mods_to_exclude_from_release = [ - 'ActorDumperMod', - 'jsbLuaProfilerMod', - ] - - # Lua mods to disable (but keep) in the non-dev/release version - self.lua_mods_to_disable_in_release = [ - 'LineTraceMod', - ] - - # Files in root/assets to exclude from the non-dev/release version of the zip - self.files_to_exclude_from_release = [ - 'Mods/shared/Types.lua', - 'Mods/shared/jsbProfiler', - 'UE4SS_Signatures', - 'VTableLayoutTemplates', - 'MemberVarLayoutTemplates', - 'CustomGameConfigs', - 'MapGenBP', - 'Changelog.md', - ] - - # Settings to change in the release. The default settings in assets/UE4SS-settings.ini are for dev - self.settings_to_modify_in_release = { - 'GuiConsoleVisible': 0, - 'GuiConsoleEnabled': 0, - 'ConsoleEnabled': 0, - 'EnableHotReloadSystem': 0, - 'MaxMemoryUsageDuringAssetLoading': 80, - 'bUseUObjectArrayCache': "false", - } - - def make_staging_dirs(self): - shutil.copytree('assets', self.ue4ss_dir) - shutil.copy('LICENSE', os.path.join(self.ue4ss_dir, 'LICENSE')) - - if not self.is_dev_release: - for file in self.files_to_exclude_from_release: - path = os.path.join(self.ue4ss_dir, file) - if os.path.exists(path): - if os.path.isfile(path): - os.remove(path) - elif os.path.isdir(path): - shutil.rmtree(path) - self.modify_settings(self.settings_to_modify_in_release) - - # Remove lua mods from the Mods directory - for mod in self.lua_mods_to_exclude_from_release: - mod_path = os.path.join(self.ue4ss_dir, 'Mods', mod) - if os.path.exists(mod_path): - if os.path.isfile(mod_path): - os.remove(mod_path) - elif os.path.isdir(mod_path): - shutil.rmtree(mod_path) - else: - # only include README file in dev releases - shutil.copy('README.md', os.path.join(self.ue4ss_dir, 'README.md')) - - ue4ss_dll_path, ue4ss_pdb_path, dwmapi_dll_path, cpp_mods_paths = self.scan_directories() - - self.copy_cpp_mods(cpp_mods_paths) - self.modify_mods_txt() # can only run this after copy mods just in case we are missing mod dlls - self.modify_mods_json() - self.copy_executables(dwmapi_dll_path, ue4ss_dll_path, ue4ss_pdb_path) - self.copy_docs() - - # needs to be run inside staging as we don't want to modify the original - # only run this if we are not doing an experimental release, as we don't want the date to be set in experimental releases - if not self.is_experimental: self.modify_changelog() - - def modify_settings(self, settings_to_modify): - config_path = os.path.join(self.ue4ss_dir, 'UE4SS-settings.ini') - with open(config_path, mode='r', encoding='utf-8') as file: - content = file.read() - - for key, value in settings_to_modify.items(): - pattern = rf'(^{key}\s*=).*?$' - content = re.sub(pattern, rf'\1 {value}', content, flags=re.MULTILINE) - - with open(config_path, mode='w', encoding='utf-8') as file: - file.write(content) - - def scan_directories(self): - ue4ss_dll_path = '' - ue4ss_pdb_path = '' - dwmapi_dll_path = '' - cpp_mods_paths = {mod: '' for mod in self.cpp_mods if self.is_dev_release or self.cpp_mods[mod]['include_in_release']} - scan_start_dir = '.' - - for root, _, files in os.walk(scan_start_dir): - for file in files: - if file.lower() == "ue4ss.dll": - ue4ss_dll_path = os.path.join(root, file) - if file.lower() == "ue4ss.pdb": - ue4ss_pdb_path = os.path.join(root, file) - if file.lower() == "dwmapi.dll": - dwmapi_dll_path = os.path.join(root, file) - for mod_name in cpp_mods_paths: - if file.lower() == mod_name.lower() + '.dll': - cpp_mods_paths[mod_name] = os.path.join(root, file) - - return ue4ss_dll_path, ue4ss_pdb_path, dwmapi_dll_path, cpp_mods_paths - - def copy_cpp_mods(self, cpp_mods_paths): - for mod_name, dll_path in cpp_mods_paths.items(): - if dll_path: - mod_dir = os.path.join(self.ue4ss_dir, 'Mods', mod_name, 'dlls') - os.makedirs(mod_dir, exist_ok=True) - shutil.copy(dll_path, os.path.join(mod_dir, 'main.dll')) - if self.is_dev_release: - pdb_path = dll_path.replace('.dll', '.pdb') - if os.path.exists(pdb_path): - shutil.copy(pdb_path, os.path.join(mod_dir, 'main.pdb')) - else: - print(f'Error: {mod_name}.dll not found, build has failed.') - sys.exit(1) - - for mod_name, mod_info in self.cpp_mods.items(): - if self.is_dev_release or mod_info['include_in_release']: - if mod_info['create_config']: - os.makedirs(os.path.join(self.ue4ss_dir, 'Mods', mod_name, 'config'), exist_ok=True) - - def modify_mods_txt(self): - mods_to_remove_from_release = self.lua_mods_to_exclude_from_release.copy() - for mod_name, mod_info in self.cpp_mods.items(): - if not mod_info['include_in_release']: - mods_to_remove_from_release.append(mod_name) - - mods_path = os.path.join(self.ue4ss_dir, 'Mods', 'mods.txt') - with open(mods_path, mode='r', encoding='utf-8') as file: - content = file.readlines() - - if self.cpp_mods: - content = [f'{mod} : 1\n' for mod in self.cpp_mods] + content - - if not self.is_dev_release: - content = [line for line in content if not any(mod in line for mod in mods_to_remove_from_release)] - # Disable mods that should be kept but disabled in release - new_content = [] - for line in content: - disabled = False - for mod in self.lua_mods_to_disable_in_release: - if mod in line: - new_content.append(f'{mod} : 0\n') - disabled = True - break - if not disabled: - new_content.append(line) - content = new_content - - with open(mods_path, mode='w', encoding='utf-8') as file: - file.writelines(content) - - def modify_mods_json(self): - mods_path = os.path.join(self.ue4ss_dir, 'Mods', 'mods.json') - with open(mods_path, mode='r', encoding='utf-8') as file: - content = json.load(file) - - if self.cpp_mods: - for mod_name, mod_info in self.cpp_mods.items(): - if mod_name not in [mod['mod_name'] for mod in content]: - if not self.is_dev_release: - content.append({'mod_name': mod_name, 'mod_enabled': mod_info['include_in_release']}) - else: - content.append({'mod_name': mod_name, 'mod_enabled': True}) - - if not self.is_dev_release: - content = [ - mod for mod in content - if mod['mod_name'] not in self.lua_mods_to_exclude_from_release - and self.cpp_mods.get(mod['mod_name'], {}).get('include_in_release', True) - ] - # Disable mods that should be kept but disabled in release - for mod in content: - if mod['mod_name'] in self.lua_mods_to_disable_in_release: - mod['mod_enabled'] = False - - with open(mods_path, mode='w', encoding='utf-8') as file: - json.dump(content, file, indent=4) - - def copy_executables(self, dwmapi_dll_path, ue4ss_dll_path, ue4ss_pdb_path): - shutil.copy(dwmapi_dll_path, self.staging_dir) - shutil.copy(ue4ss_dll_path, self.ue4ss_dir) - if self.is_dev_release: shutil.copy(ue4ss_pdb_path, self.ue4ss_dir) - - def copy_docs(self): - if self.is_dev_release and os.path.exists('docs'): - shutil.copytree('docs', os.path.join(self.ue4ss_dir, 'Docs')) - - def modify_changelog(self): - changelog_path = os.path.join(self.ue4ss_dir, 'Changelog.md') - if os.path.exists(changelog_path): ready_changelog_for_release(changelog_path) - - def package_release(self): - try: - version = subprocess.check_output(['git', 'describe', '--tags', '--exclude', '*experimental*']).decode('utf-8').strip() - except subprocess.CalledProcessError: - print('Error: git describe failed. Make sure the release has been tagged.') - sys.exit(1) - main_zip_name = f'zDEV-UE4SS_{version}' if self.is_dev_release else f'UE4SS_{version}' - output = os.path.join(self.release_output, main_zip_name) - shutil.make_archive(output, 'zip', self.staging_dir) - print(f'Created package {output}.zip') - - def cleanup(self): - shutil.rmtree(self.staging_dir) - -class Packager: - """ - "Release" refers to the zip that is distributed to the end users. It should be in the format UE4SS_.zip and contains only the necessary files. - "Dev" refers to the zip that is used for development. It should be in the format zDEV-UE4SS_.zip and contains all the files, pdbs and docs. - """ - - def __init__(self, args): - self.args = args - self.release_output = 'release' - self.setup() - - def setup(self): - if os.path.exists(self.release_output): shutil.rmtree(self.release_output) - os.mkdir(self.release_output) - - def run(self): - dev_handler = ReleaseHandler(is_dev_release=True, is_experimental=self.args.e, release_output=self.release_output) - release_handler = ReleaseHandler(is_dev_release=False, is_experimental=self.args.e, release_output=self.release_output) - - dev_handler.make_staging_dirs() - dev_handler.package_release() - dev_handler.cleanup() - - release_handler.make_staging_dirs() - release_handler.package_release() - release_handler.cleanup() - - shutil.make_archive(os.path.join(self.release_output, 'zCustomGameConfigs'), 'zip', 'assets/CustomGameConfigs') - shutil.make_archive(os.path.join(self.release_output, 'zMapGenBP'), 'zip', 'assets/MapGenBP') - - changelog = parse_changelog(self.args.changelog_path) - with open(os.path.join(self.release_output, 'release_notes.md'), 'w') as file: - file.write(changelog[0]['notes']) - - print('Done') - -def ready_changelog_for_release(changelog_path): - with open(changelog_path, 'r') as file: - lines = file.readlines() - version = lines[0].strip() - if lines[2] != 'TBD\n': - raise Exception('date is not "TBD"') - lines[2] = datetime.today().strftime('%Y-%m-%d') + '\n' - with open(changelog_path, 'w') as file: - file.writelines(lines) - return version - -def parse_changelog(changelog_path): - with open(changelog_path, 'r') as file: - lines = file.readlines() - delimeters = [index - 1 for index, value in enumerate(lines) if value == '==============\n'] - delimeters.append(len(lines) + 1) - return [{ - 'tag': lines[index[0]].strip(), - 'date': lines[index[0] + 2].strip(), - 'notes': ''.join(lines[index[0] + 3:index[1]]).strip(), - } for index in zip(delimeters, delimeters[1:])] - -def package(args): - packager = Packager(args) - packager.run() - -def release_commit(args): - version = ready_changelog_for_release(args.changelog_path) - message = f'Release {version}' - subprocess.run(['git', 'add', args.changelog_path], check=True) - if args.username: - subprocess.run(['git', '-c', f'user.name="{args.username}"', '-c', f'user.email="{args.username}@users.noreply.github.com"', 'commit', '-m', message], check=True) - else: - subprocess.run(['git', 'commit', '-m', message], check=True) - subprocess.run(['git', 'tag', version], check=True) - - # Outputs to GitHub env if present - def github_output(name, value): - if 'GITHUB_OUTPUT' in os.environ: - with open(os.environ['GITHUB_OUTPUT'], 'a') as env: - env.write(f'{name}={value}\n') - - github_output('release_tag', version) - -if __name__ == "__main__": - # Change dir to repo root - os.chdir(os.path.join(os.path.dirname(__file__), '..', '..')) - - parser = argparse.ArgumentParser() - parser.add_argument('--changelog_path', default='assets/Changelog.md', required=False) - - subparsers = parser.add_subparsers(dest='command', required=True) - - package_parser = subparsers.add_parser('package') - package_parser.add_argument('-e', action='store_true') - - release_commit_parser = subparsers.add_parser('release_commit') - release_commit_parser.add_argument('username', nargs='?') - - args = parser.parse_args() - commands = {f.__name__: f for f in [ - package, - release_commit, - ]} - commands[args.command](args) - -""" -How to run this script: -1. Running the 'package' command: - Usage: python release.py package - - --changelog_path : Optional argument to specify the path to the changelog file. Default is 'assets/Changelog.md' - -e : Argument used when running the script for experimental release - - Examples: - - python release.py package - - python release.py package -e --changelog_path custom/Changelog.md - -2. Running the 'release_commit' command: - Usage: python release.py release_commit [username] - - username : Optional argument to specify a github username - --changelog_path : Optional argument to specify the path to the changelog file. Default is 'assets/Changelog.md' - - Examples: - - python release.py release_commit - - python release.py release_commit ${{ github.actor }} - - python release.py release_commit ${{ github.actor }} custom/Changelog.md -""" - -""" -To locally debug this script in vscode, make launch.json in .vscode and add the following configurations: - -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Debug Release Script", - "type": "debugpy", - "request": "launch", - "program": "${workspaceFolder}/tools/buildscripts/release.py", - "console": "integratedTerminal", - "args": [ - "package", - "-e" - ], - "justMyCode": true - } - ] -} -""" diff --git a/xmake.lua b/xmake.lua index 73dac9ec..39cde7bd 100644 --- a/xmake.lua +++ b/xmake.lua @@ -56,10 +56,6 @@ on_install(function(target) end) includes("deps") includes("UE4SS") -if is_plat("windows") then - includes("cppmods") -end - -- TODO: Remove this before the next release. It only exists to maintain backwards compat -- warnings for older mod templates. set_config("scriptsRoot", path.join(os.scriptdir(), "tools/xmakescripts"))