diff --git a/.gitignore b/.gitignore index 533fd34..86b4ca5 100644 --- a/.gitignore +++ b/.gitignore @@ -419,7 +419,7 @@ FodyWeavers.xsd # VS Code files for those working on multiple tools **/.vscode/* .vscode/* -!.vscode/settings.json +# !.vscode/settings.json # !.vscode/tasks.json # !.vscode/launch.json # !.vscode/extensions.json diff --git a/include/MaaUtils/StaticEventBus.h b/include/MaaUtils/StaticEventBus.h new file mode 100644 index 0000000..25c08fb --- /dev/null +++ b/include/MaaUtils/StaticEventBus.h @@ -0,0 +1,182 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "MaaUtils/Conf.h" +#include "MaaUtils/Port.h" + +MAA_NS_BEGIN + +class Event +{ +public: + virtual ~Event() = default; +}; + +class CancellableEvent : public Event +{ +public: + void cancel() { cancelled_ = true; } + + bool is_cancelled() const { return cancelled_; } + +private: + bool cancelled_ = false; +}; + +template +concept IsEvent = std::derived_from; + +class EventStorageBase +{ +public: + virtual ~EventStorageBase() = default; +}; + +template +struct EventStorage : public EventStorageBase +{ + struct Subscription + { + int priority; + bool has_owner; + std::function callback; + std::weak_ptr owner; + + bool operator<(const Subscription& other) const + { + // Higher priority subscriptions come first + return priority > other.priority; + } + }; + + std::vector subscriptions; +}; + +class EventStorageRegistry +{ +private: + std::unordered_map> storages_; + +public: + template + EventStorage& get_storage() + { + std::type_index type_idx(typeid(EventT)); + auto it = storages_.find(type_idx); + if (it == storages_.end()) { + auto storage = std::make_unique>(); + auto* storage_ptr = storage.get(); + storages_[type_idx] = std::move(storage); + return *storage_ptr; + } + else { + return *static_cast*>(it->second.get()); + } + } +}; + +MAA_UTILS_API EventStorageRegistry& get_event_storage_registry(); + +class StaticEventManager +{ +private: + template + inline static EventStorage& get_event_storage() + { + static auto* cached_storage = &get_event_storage_registry().get_storage(); + return *cached_storage; + } + +public: + // Free function subscription + template + static void subscribe(Callable&& callback, int priority = 0) + { + auto& storage = get_event_storage(); + + auto wrapper = [callback = std::forward(callback)](EventT& event) mutable { + if constexpr (std::is_invocable_v) { + callback(event); + } + else if constexpr (std::is_invocable_v) { + callback(event); + } + else { + static_assert(false, "Callback must be invocable with EventT& or const EventT&"); + } + }; + + storage.subscriptions.insert( + std::lower_bound( + storage.subscriptions.begin(), + storage.subscriptions.end(), + typename EventStorage::Subscription { priority, false, {}, {} }), + { priority, false, std::move(wrapper), {} }); + } + + // Member function subscription + template + static void subscribe(const std::shared_ptr& owner, Callable&& callback, int priority = 0) + { + auto weak_owner = std::weak_ptr(owner); + auto wrapper = [weak_owner, callback = std::forward(callback)](EventT& event) mutable { + if (auto shared_owner = weak_owner.lock()) { + if constexpr (std::is_invocable_v) { + callback(event); + } + else if constexpr (std::is_invocable_v) { + callback(event); + } + else if constexpr (std::is_invocable_v) { + (shared_owner.get()->*callback)(event); + } + else if constexpr (std::is_invocable_v) { + (shared_owner.get()->*callback)(event); + } + else { + static_assert(false, "Callback must be invocable with EventT& or const EventT&"); + } + } + }; + auto& storage = get_event_storage(); + storage.subscriptions.insert( + std::lower_bound( + storage.subscriptions.begin(), + storage.subscriptions.end(), + typename EventStorage::Subscription { priority, true, {}, {} }), + { priority, true, std::move(wrapper), weak_owner }); + } + + template + static void publish(EventT& event) + { + auto& storage = get_event_storage(); + bool need_erase = false; + for (const auto& subscription : storage.subscriptions) { + if (subscription.has_owner && subscription.owner.expired()) { + need_erase = true; + continue; + } + subscription.callback(event); + if constexpr (std::derived_from) { + if (event.is_cancelled()) { + break; + } + } + } + if (need_erase) { + std::erase_if(storage.subscriptions, [](const auto& sub) { return sub.has_owner && sub.owner.expired(); }); + } + } + + StaticEventManager() = delete; +}; + +MAA_NS_END diff --git a/source/StaticEventBus/StaticEventBus.cpp b/source/StaticEventBus/StaticEventBus.cpp new file mode 100644 index 0000000..970fa6a --- /dev/null +++ b/source/StaticEventBus/StaticEventBus.cpp @@ -0,0 +1,10 @@ +#include "MaaUtils/StaticEventBus.h" + +MAA_NS_BEGIN + +EventStorageRegistry& get_event_storage_registry() { + static EventStorageRegistry registry; + return registry; +} + +MAA_NS_END \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..ce7b9e2 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,5 @@ +set(CMAKE_CXX_STANDARD 20) + +add_executable(StaticEventBusTest StaticEventBusTest.cpp ../source/StaticEventBus/StaticEventBus.cpp) + +target_include_directories(StaticEventBusTest PRIVATE ../include) \ No newline at end of file diff --git a/test/StaticEventBusTest.cpp b/test/StaticEventBusTest.cpp new file mode 100644 index 0000000..48a503d --- /dev/null +++ b/test/StaticEventBusTest.cpp @@ -0,0 +1,275 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "MaaUtils/StaticEventBus.h" + +using namespace MAA_NS; + +// Test event types - each test uses its own event type to avoid interference +class Event1 : public Event { +public: + int value; + explicit Event1(int v) : value(v) {} +}; + +class Event2 : public Event { +public: + int value; + explicit Event2(int v) : value(v) {} +}; + +class Event3 : public Event { +public: + int value; + explicit Event3(int v) : value(v) {} +}; + +class Event4 : public Event { +public: + int value; + explicit Event4(int v) : value(v) {} +}; + +class Event5 : public Event { +public: + int value; + explicit Event5(int v) : value(v) {} +}; + +class Event6 : public CancellableEvent { +public: + int value; + explicit Event6(int v) : value(v) {} +}; + +class Event7 : public Event { +public: + int value; + explicit Event7(int v) : value(v) {} +}; + +class Event8 : public Event { +public: + int depth; + explicit Event8(int d) : depth(d) {} +}; + +class Event9 : public Event { +public: + int value; + explicit Event9(int v) : value(v) {} +}; + +// Test subscriber class +class TestSubscriber { +public: + int call_count = 0; + int last_value = 0; + + void on_event(const Event4& event) { + call_count++; + last_value = event.value; + } + + void on_cancellable_event(const Event6& event) { + call_count++; + last_value = event.value; + } +}; + +// Test 1: Basic subscription and publish +void test_basic_subscription() { + std::cout << "Test 1: Basic subscription and publish... "; + + int call_count = 0; + int received_value = 0; + + StaticEventManager::subscribe( + [&](const Event1& event) { + call_count++; + received_value = event.value; + } + ); + + Event1 event(42); + StaticEventManager::publish(event); + + assert(call_count == 1); + assert(received_value == 42); + + std::cout << "PASSED\n"; +} + +// Test 2: Multiple subscribers +void test_multiple_subscribers() { + std::cout << "Test 2: Multiple subscribers... "; + + std::atomic call_count{0}; + + StaticEventManager::subscribe( + [&](const Event2&) { call_count++; } + ); + StaticEventManager::subscribe( + [&](const Event2&) { call_count++; } + ); + StaticEventManager::subscribe( + [&](const Event2&) { call_count++; } + ); + + Event2 event(100); + StaticEventManager::publish(event); + + assert(call_count == 3); + + std::cout << "PASSED\n"; +} + +// Test 3: Priority ordering +void test_priority_ordering() { + std::cout << "Test 3: Priority ordering... "; + + std::vector call_order; + std::mutex order_mutex; + + // Subscribe with different priorities + StaticEventManager::subscribe( + [&](const Event3&) { + std::lock_guard lock(order_mutex); + call_order.push_back(2); + }, + -100 + ); + + StaticEventManager::subscribe( + [&](const Event3&) { + std::lock_guard lock(order_mutex); + call_order.push_back(0); + }, + 100 + ); + + StaticEventManager::subscribe( + [&](const Event3&) { + std::lock_guard lock(order_mutex); + call_order.push_back(1); + }, + 0 + ); + + Event3 event(1); + StaticEventManager::publish(event); + + assert(call_order.size() == 3); + assert(call_order[0] == 0); + assert(call_order[1] == 1); + assert(call_order[2] == 2); + + std::cout << "PASSED\n"; +} + +// Test 4: Member function subscription +void test_member_function_subscription() { + std::cout << "Test 4: Member function subscription... "; + + auto subscriber = std::make_shared(); + + StaticEventManager::subscribe( + subscriber, + &TestSubscriber::on_event, + 100 + ); + + Event4 event(999); + StaticEventManager::publish(event); + + assert(subscriber->call_count == 1); + assert(subscriber->last_value == 999); + + std::cout << "PASSED\n"; +} + +// Test 5: Weak pointer cleanup +void test_weak_pointer_cleanup() { + std::cout << "Test 5: Weak pointer cleanup... "; + + { + auto subscriber = std::make_shared(); + StaticEventManager::subscribe( + subscriber, + &TestSubscriber::on_event + ); + + Event4 event(111); + StaticEventManager::publish(event); + assert(subscriber->call_count == 1); + } + // subscriber is destroyed here + + // Publish again - should trigger cleanup of expired weak_ptr + Event4 event(222); + StaticEventManager::publish(event); + + // If we get here without crash, cleanup worked + std::cout << "PASSED\n"; +} + +// Test 6: Cancellable event +void test_cancellable_event() { + std::cout << "Test 6: Cancellable event... "; + + int first_callback_count = 0; + int second_callback_count = 0; + + StaticEventManager::subscribe( + [&](Event6& event) { + first_callback_count++; + event.cancel(); + }, + 100 + ); + + StaticEventManager::subscribe( + [&](const Event6&) { + second_callback_count++; + }, + -100 + ); + + Event6 event(42); + StaticEventManager::publish(event); + + assert(first_callback_count == 1); + assert(second_callback_count == 0); // Should not be called due to cancellation + assert(event.is_cancelled()); + + std::cout << "PASSED\n"; +} + +int main() { + std::cout << "Running StaticEventBus tests...\n\n"; + + try { + test_basic_subscription(); + test_multiple_subscribers(); + test_priority_ordering(); + test_member_function_subscription(); + test_weak_pointer_cleanup(); + test_cancellable_event(); + + std::cout << "\n✓ All tests passed!\n"; + return 0; + } + catch (const std::exception& e) { + std::cerr << "\n✗ Test failed with exception: " << e.what() << "\n"; + return 1; + } + catch (...) { + std::cerr << "\n✗ Test failed with unknown exception\n"; + return 1; + } +}