#pragma once #include #include #include #include #include #include #include #include #include namespace PalUIExtension::Features { using namespace RC; using namespace RC::Unreal; class IUIFeature { public: using StringType = std::basic_string; virtual ~IUIFeature() = default; virtual auto Name() const -> const TCHAR* = 0; virtual auto InstallHooks() -> void = 0; virtual auto OnWorldTeardown() -> void {} virtual auto SetEnabled(bool enabled) -> void {} virtual auto GetEnabled() -> bool { return false; } // ---- logging --------------------------------------------------- template auto SendLog(LogLevel::LogLevel Level, const TCHAR* Fmt, Args&&... Fargs) const -> void { Emit(Level, Fmt, std::forward(Fargs)...); } template auto SendLogOnce(const TCHAR* Key, LogLevel::LogLevel Level, const TCHAR* Fmt, Args&&... Fargs) const -> void { if (!Key) return; if (!m_log_once_keys.insert(StringType{ Key }).second) return; Emit(Level, Fmt, std::forward(Fargs)...); } template auto SendLogThrottled(const TCHAR* Key, int64_t IntervalMs, LogLevel::LogLevel Level, const TCHAR* Fmt, Args&&... Fargs) const -> void { if (!Key) return; using Clock = std::chrono::steady_clock; const auto Now = Clock::now(); auto& Last = m_log_throttle[StringType{ Key }]; if (Last.time_since_epoch().count() != 0 && std::chrono::duration_cast(Now - Last).count() < IntervalMs) { return; } Last = Now; Emit(Level, Fmt, std::forward(Fargs)...); } template auto SendVerbose(const TCHAR* Fmt, Args&&... Fargs) const -> void { if constexpr (Static::kVerboseLogging) { Emit(LogLevel::Verbose, Fmt, std::forward(Fargs)...); } } auto ResetLogOnce() const -> void { m_log_once_keys.clear(); m_log_throttle.clear(); } // ---- argument helpers ------------------------------------------- static auto NameOf(UObject* Obj) -> StringType { return Obj ? Obj->GetName() : StringType{ STR("") }; } static auto FullNameOf(UObject* Obj) -> StringType { return Obj ? Obj->GetFullName() : StringType{ STR("") }; } static auto ClassNameOf(UObject* Obj) -> StringType { if (!Obj) return StringType{ STR("") }; UClass* Cls = Obj->GetClassPrivate(); return Cls ? Cls->GetName() : StringType{ STR("") }; } static auto AddressOf(const void* P) -> uint64_t { return static_cast(reinterpret_cast(P)); } private: template auto Emit(LogLevel::LogLevel Level, const TCHAR* Fmt, Args&&... Fargs) const -> void { if (!Fmt) return; try { switch (Level) { case LogLevel::Error: Output::send(STR("PalUIExt [{}] "), Name()); Output::send(Fmt, std::forward(Fargs)...); Output::send(STR("\n")); break; case LogLevel::Warning: Output::send(STR("PalUIExt [{}] "), Name()); Output::send(Fmt, std::forward(Fargs)...); Output::send(STR("\n")); break; case LogLevel::Verbose: Output::send(STR("PalUIExt [{}] "), Name()); Output::send(Fmt, std::forward(Fargs)...); Output::send(STR("\n")); break; default: Output::send(STR("PalUIExt [{}] "), Name()); Output::send(Fmt, std::forward(Fargs)...); Output::send(STR("\n")); break; } } catch (const std::exception&) { try { Output::send(STR("PalUIExt: malformed log format string\n")); } catch (...) {} } catch (...) { } } mutable std::unordered_set m_log_once_keys; mutable std::unordered_map m_log_throttle; }; class ScopedHotPathTimer { public: ScopedHotPathTimer(const IUIFeature& Feature, const TCHAR* Label, int64_t ReportAboveMicros = 250) : m_feature(Feature), m_label(Label), m_threshold(ReportAboveMicros) { if constexpr (Static::kProfileHotPath) { m_start = std::chrono::steady_clock::now(); } } ~ScopedHotPathTimer() { if constexpr (Static::kProfileHotPath) { const auto Micros = std::chrono::duration_cast( std::chrono::steady_clock::now() - m_start).count(); if (Micros >= m_threshold) { m_feature.SendLogThrottled(m_label, 1000, LogLevel::Warning, STR("slow path: {} took {}us"), m_label, Micros); } } } ScopedHotPathTimer(const ScopedHotPathTimer&) = delete; ScopedHotPathTimer& operator=(const ScopedHotPathTimer&) = delete; private: const IUIFeature& m_feature; const TCHAR* m_label; int64_t m_threshold; std::chrono::steady_clock::time_point m_start{}; }; }