Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
182 changes: 182 additions & 0 deletions include/MaaUtils/StaticEventBus.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#pragma once

#include <algorithm>
#include <concepts>
#include <functional>
#include <memory>
#include <typeindex>
#include <unordered_map>
#include <vector>

#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 <typename T>
concept IsEvent = std::derived_from<T, Event>;

class EventStorageBase
{
public:
virtual ~EventStorageBase() = default;
};

template <IsEvent EventT>
struct EventStorage : public EventStorageBase
{
struct Subscription
{
int priority;
bool has_owner;
std::function<void(EventT&)> callback;
std::weak_ptr<void> owner;

bool operator<(const Subscription& other) const
{
// Higher priority subscriptions come first
return priority > other.priority;
}
};

std::vector<Subscription> subscriptions;
};

class EventStorageRegistry
{
private:
std::unordered_map<std::type_index, std::unique_ptr<EventStorageBase>> storages_;

public:
template <IsEvent EventT>
EventStorage<EventT>& get_storage()
{
std::type_index type_idx(typeid(EventT));
auto it = storages_.find(type_idx);
if (it == storages_.end()) {
auto storage = std::make_unique<EventStorage<EventT>>();
auto* storage_ptr = storage.get();
storages_[type_idx] = std::move(storage);
return *storage_ptr;
}
else {
return *static_cast<EventStorage<EventT>*>(it->second.get());
}
}
Comment on lines +69 to +82
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The get_storage() method modifies storages_ without synchronization. If multiple threads call subscribe() or publish() concurrently with different event types, this can lead to race conditions during map modification. Consider adding mutex protection around storages_ access.

Copilot uses AI. Check for mistakes.
};

MAA_UTILS_API EventStorageRegistry& get_event_storage_registry();

class StaticEventManager
{
private:
template <IsEvent EventT>
inline static EventStorage<EventT>& get_event_storage()
{
static auto* cached_storage = &get_event_storage_registry().get_storage<EventT>();
return *cached_storage;
}

public:
// Free function subscription
template <IsEvent EventT, typename Callable>
static void subscribe(Callable&& callback, int priority = 0)
{
auto& storage = get_event_storage<EventT>();

auto wrapper = [callback = std::forward<Callable>(callback)](EventT& event) mutable {
if constexpr (std::is_invocable_v<Callable&, EventT&>) {
callback(event);
}
else if constexpr (std::is_invocable_v<Callable&, const EventT&>) {
callback(event);
}
else {
static_assert(false, "Callback must be invocable with EventT& or const EventT&");
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using static_assert(false, ...) is ill-formed in C++. It will always fail even when not instantiated. Use a type-dependent expression like static_assert(sizeof(Callable) == 0, ...) or static_assert(!sizeof(Callable*), ...) to ensure it only fires when the template is actually instantiated with an invalid type.

Suggested change
static_assert(false, "Callback must be invocable with EventT& or const EventT&");
static_assert(
!std::is_invocable_v<Callable&, EventT&> &&
!std::is_invocable_v<Callable&, const EventT&>,
"Callback must be invocable with EventT& or const EventT&"
);

Copilot uses AI. Check for mistakes.
}
};

storage.subscriptions.insert(
std::lower_bound(
storage.subscriptions.begin(),
storage.subscriptions.end(),
typename EventStorage<EventT>::Subscription { priority, false, {}, {} }),
{ priority, false, std::move(wrapper), {} });
Comment on lines +116 to +121
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subscribe() method modifies storage.subscriptions without synchronization. Concurrent calls to subscribe() or simultaneous publish() operations on the same event type can cause data races. Consider adding mutex protection for subscription vector modifications.

Copilot uses AI. Check for mistakes.
}

// Member function subscription
template <IsEvent EventT, typename T, typename Callable>
static void subscribe(const std::shared_ptr<T>& owner, Callable&& callback, int priority = 0)
{
auto weak_owner = std::weak_ptr<T>(owner);
auto wrapper = [weak_owner, callback = std::forward<Callable>(callback)](EventT& event) mutable {
if (auto shared_owner = weak_owner.lock()) {
if constexpr (std::is_invocable_v<Callable&, EventT&>) {
callback(event);
}
else if constexpr (std::is_invocable_v<Callable&, const EventT&>) {
callback(event);
}
else if constexpr (std::is_invocable_v<Callable&, T*, EventT&>) {
(shared_owner.get()->*callback)(event);
}
else if constexpr (std::is_invocable_v<Callable&, T*, const EventT&>) {
(shared_owner.get()->*callback)(event);
}
else {
static_assert(false, "Callback must be invocable with EventT& or const EventT&");
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using static_assert(false, ...) is ill-formed in C++. It will always fail even when not instantiated. Use a type-dependent expression like static_assert(sizeof(Callable) == 0, ...) or static_assert(!sizeof(Callable*), ...) to ensure it only fires when the template is actually instantiated with an invalid type.

Suggested change
static_assert(false, "Callback must be invocable with EventT& or const EventT&");
static_assert(sizeof(Callable) == 0, "Callback must be invocable with EventT& or const EventT&");

Copilot uses AI. Check for mistakes.
}
}
};
auto& storage = get_event_storage<EventT>();
storage.subscriptions.insert(
std::lower_bound(
storage.subscriptions.begin(),
storage.subscriptions.end(),
typename EventStorage<EventT>::Subscription { priority, true, {}, {} }),
{ priority, true, std::move(wrapper), weak_owner });
}

template <IsEvent EventT>
static void publish(EventT& event)
{
auto& storage = get_event_storage<EventT>();
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<EventT, CancellableEvent>) {
if (event.is_cancelled()) {
break;
}
}
}
if (need_erase) {
std::erase_if(storage.subscriptions, [](const auto& sub) { return sub.has_owner && sub.owner.expired(); });
}
Comment on lines +162 to +176
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The publish() method iterates over and potentially modifies storage.subscriptions without synchronization. If another thread calls subscribe() during publication, this creates a data race. The iteration and cleanup operations need to be protected with the same mutex used in subscribe().

Copilot uses AI. Check for mistakes.
}

StaticEventManager() = delete;
};

MAA_NS_END
10 changes: 10 additions & 0 deletions source/StaticEventBus/StaticEventBus.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#include "MaaUtils/StaticEventBus.h"

MAA_NS_BEGIN

EventStorageRegistry& get_event_storage_registry() {
static EventStorageRegistry registry;
return registry;
}

MAA_NS_END
5 changes: 5 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
set(CMAKE_CXX_STANDARD 20)

add_executable(StaticEventBusTest StaticEventBusTest.cpp ../source/StaticEventBus/StaticEventBus.cpp)

target_include_directories(StaticEventBusTest PRIVATE ../include)
Loading