Add event topic (#3010)

* Add event topic

Add ability to subscribe to events via js-api

* Apply suggestions from code review

Co-authored-by: Alexander Bock <alexander.bock@liu.se>

* Output an error if no status or event object is passed to payload

* fixed typo

* Report a warning if user tries to unsubscribe to a non registered event/type combination

---------

Co-authored-by: Alexander Bock <alexander.bock@liu.se>
This commit is contained in:
Andreas Engberg
2024-01-24 13:51:55 +01:00
committed by GitHub
parent ef52155e83
commit 04ff09814d
9 changed files with 286 additions and 7 deletions

View File

@@ -80,7 +80,8 @@ struct Event {
CameraPathStarted,
CameraPathFinished,
CameraMovedPosition,
Custom
Custom,
Last // sentinel value
};
constexpr explicit Event(Type type_) : type(type_) {}

View File

@@ -36,6 +36,8 @@ namespace events { struct Event; }
class EventEngine {
public:
using ScriptCallback = std::function<void(ghoul::Dictionary)>;
struct ActionInfo {
events::Event::Type type;
uint32_t id = std::numeric_limits<uint32_t>::max();
@@ -44,6 +46,11 @@ public:
std::optional<ghoul::Dictionary> filter;
};
struct TopicInfo {
uint32_t id = std::numeric_limits<uint32_t>::max();
ScriptCallback callback;
};
/**
* This function returns the first event stored in the EventEngine, or `nullptr` if
* no event exists. To navigate the full list of events, you can access the returned
@@ -87,6 +94,16 @@ public:
void registerEventAction(events::Event::Type type, std::string identifier,
std::optional<ghoul::Dictionary> filter = std::nullopt);
/**
* Registers a new topic for a specific event type.
*
* \param topicId The id of the topic that will be triggered
* \param type The type for which a new topic is registered
* \param callback The callback function that will be called on triggered event
*/
void registerEventTopic(size_t topicId, events::Event::Type type,
ScriptCallback callback);
/**
* Removing registration for a type/action combination.
*
@@ -105,6 +122,15 @@ public:
*/
void unregisterEventAction(uint32_t identifier);
/**
* Removing registration for a topic/type combination, does nothing if topicId or type
* combination does not exist
*
* \param topicId The id of the topic that should be unregistered
* \param type The type of the topic that should be unregistered
*/
void unregisterEventTopic(size_t topicId, events::Event::Type type);
/**
* Returns the list of all registered actions, sorted by their identifiers.
*
@@ -134,6 +160,12 @@ public:
*/
void triggerActions() const;
/**
* Triggers all topics that are registered for events that are in the current event
* queue.
*/
void triggerTopics() const;
static scripting::LuaLibrary luaLibrary();
private:
@@ -149,6 +181,8 @@ private:
/// the lookup really fast. So having this extra wasted memory is probably worth it
std::unordered_map<events::Event::Type, std::vector<ActionInfo>> _eventActions;
std::unordered_map<events::Event::Type, std::vector<TopicInfo>> _eventTopics;
static uint32_t nextRegisteredEventId;
#ifdef _DEBUG

View File

@@ -37,6 +37,7 @@ set(HEADER_FILES
include/topics/cameratopic.h
include/topics/documentationtopic.h
include/topics/enginemodetopic.h
include/topics/eventtopic.h
include/topics/flightcontrollertopic.h
include/topics/getpropertytopic.h
include/topics/luascripttopic.h
@@ -65,6 +66,7 @@ set(SOURCE_FILES
src/topics/cameratopic.cpp
src/topics/documentationtopic.cpp
src/topics/enginemodetopic.cpp
src/topics/eventtopic.cpp
src/topics/flightcontrollertopic.cpp
src/topics/getpropertytopic.cpp
src/topics/luascripttopic.cpp

View File

@@ -0,0 +1,53 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2023 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#ifndef __OPENSPACE_MODULE_SERVER___EVENT_TOPIC___H__
#define __OPENSPACE_MODULE_SERVER___EVENT_TOPIC___H__
#include <modules/server/include/topics/topic.h>
#include <openspace/events/event.h>
namespace openspace::properties { class Property; }
namespace openspace {
class EventTopic : public Topic {
public:
EventTopic() = default;
~EventTopic() override = default;
void handleJson(const nlohmann::json& json) override;
bool isDone() const override;
private:
// Returns true if there is at least one subscription active, false otherwise
bool isSubscribed() const;
std::unordered_map<events::Event::Type, bool> _subscribedEvents;
};
} // namespace openspace
#endif // __OPENSPACE_MODULE_SERVER___EVENT_TOPIC___H__

View File

@@ -30,6 +30,7 @@
#include <modules/server/include/topics/cameratopic.h>
#include <modules/server/include/topics/documentationtopic.h>
#include <modules/server/include/topics/enginemodetopic.h>
#include <modules/server/include/topics/eventtopic.h>
#include <modules/server/include/topics/flightcontrollertopic.h>
#include <modules/server/include/topics/getpropertytopic.h>
#include <modules/server/include/topics/luascripttopic.h>
@@ -101,6 +102,7 @@ Connection::Connection(std::unique_ptr<ghoul::io::Socket> s, std::string address
_topicFactory.registerClass<SkyBrowserTopic>("skybrowser");
_topicFactory.registerClass<CameraTopic>("camera");
_topicFactory.registerClass<CameraPathTopic>("cameraPath");
_topicFactory.registerClass<EventTopic>("event");
}
void Connection::handleMessage(const std::string& message) {

View File

@@ -0,0 +1,124 @@
/*****************************************************************************************
* *
* OpenSpace *
* *
* Copyright (c) 2014-2023 *
* *
* Permission is hereby granted, free of charge, to any person obtaining a copy of this *
* software and associated documentation files (the "Software"), to deal in the Software *
* without restriction, including without limitation the rights to use, copy, modify, *
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to *
* permit persons to whom the Software is furnished to do so, subject to the following *
* conditions: *
* *
* The above copyright notice and this permission notice shall be included in all copies *
* or substantial portions of the Software. *
* *
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, *
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A *
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT *
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF *
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE *
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. *
****************************************************************************************/
#include <modules/server/include/topics/eventtopic.h>
#include <modules/server/include/connection.h>
#include <modules/server/include/jsonconverters.h>
#include <openspace/engine/globals.h>
#include <openspace/events/event.h>
#include <openspace/events/eventengine.h>
#include <ghoul/fmt.h>
#include <ghoul/logging/logmanager.h>
namespace {
constexpr std::string_view _loggerCat = "EventTopic";
constexpr std::string_view StartSubscription = "start_subscription";
constexpr std::string_view StopSubscription = "stop_subscription";
} // namespace
using nlohmann::json;
namespace openspace {
bool EventTopic::isDone() const {
return !isSubscribed();
}
void EventTopic::handleJson(const nlohmann::json& json) {
std::vector<std::string> events;
auto eventJson = json.find("event");
auto statusJson = json.find("status");
if (eventJson == json.end()) {
LERROR("Payload does not contain 'event' key");
return;
}
if (statusJson == json.end() || !statusJson->is_string()) {
LERROR("Status must be a string");
return;
}
if (json.at("event").is_array()) {
events = json.at("event").get<std::vector<std::string>>();
}
else {
const std::string& event = json.at("event").get<std::string>();
if (event == "*" || event == "all") {
// Iterate over all event types and add them to list
const uint8_t lastEvent = static_cast<uint8_t>(events::Event::Type::Last);
for (uint8_t i = 0; i < lastEvent; i++) {
auto type = static_cast<events::Event::Type>(i);
events.push_back(std::string(events::toString(type)));
}
}
else {
events.push_back(event);
}
}
const std::string& status = json.at("status").get<std::string>();
for (const std::string& event : events) {
if (status == StartSubscription) {
const events::Event::Type type = events::fromString(event);
_subscribedEvents[type] = true;
auto onCallback = [this, event](ghoul::Dictionary params) {
// Include the fired event to the caller
params.setValue("Event", event);
_connection->sendJson(wrappedPayload(params));
};
global::eventEngine->registerEventTopic(_topicId, type, onCallback);
}
else if (status == StopSubscription) {
events::Event::Type type = events::fromString(event);
_subscribedEvents.erase(type);
global::eventEngine->unregisterEventTopic(_topicId, type);
}
}
}
bool EventTopic::isSubscribed() const {
if (_subscribedEvents.empty()) {
return false;
}
bool hasActiveSubscription = std::any_of(
_subscribedEvents.begin(),
_subscribedEvents.end(),
[](const std::pair<const events::Event::Type, bool>& subscription) {
return subscription.second;
});
return hasActiveSubscription;
}
} // namespace openspace

View File

@@ -1321,12 +1321,7 @@ void OpenSpaceEngine::postDraw() {
events::logAllEvents(e);
}
global::eventEngine->triggerActions();
while (e) {
// @TODO (abock, 2021-08-25) Need to send all events to a topic to be sent out to
// others
e = e->next;
}
global::eventEngine->triggerTopics();
global::eventEngine->postFrameCleanup();

View File

@@ -595,6 +595,8 @@ void logAllEvents(const Event* e) {
case Event::Type::Custom:
log(i, *static_cast<const CustomEvent*>(e));
break;
default:
break;
}
i++;

View File

@@ -29,6 +29,10 @@
#include "eventengine_lua.inl"
namespace {
constexpr std::string_view _loggerCat = "EventEngine";
}
namespace openspace {
uint32_t EventEngine::nextRegisteredEventId = 0;
@@ -71,6 +75,16 @@ void EventEngine::registerEventAction(events::Event::Type type,
nextRegisteredEventId++;
}
void EventEngine::registerEventTopic(size_t topicId, events::Event::Type type,
ScriptCallback callback)
{
TopicInfo ti;
ti.id = topicId;
ti.callback = callback;
_eventTopics[type].push_back(ti);
}
void EventEngine::unregisterEventAction(events::Event::Type type,
const std::string& identifier,
std::optional<ghoul::Dictionary> filter)
@@ -115,6 +129,37 @@ void EventEngine::unregisterEventAction(uint32_t identifier) {
));
}
void EventEngine::unregisterEventTopic(size_t topicId, events::Event::Type type) {
const auto it = _eventTopics.find(type);
if (it != _eventTopics.end()) {
const auto jt = std::find_if(
it->second.begin(), it->second.end(),
[topicId](const TopicInfo& ti) {
return ti.id == topicId;
}
);
if (jt != it->second.end()) {
it->second.erase(jt);
// This might have been the last action so we might need to remove the
// entry alltogether
if (it->second.empty()) {
_eventTopics.erase(it);
}
}
else {
LWARNING(fmt::format("Could not find registered event '{}' with topicId: {}",
events::toString(type), topicId)
);
}
}
else {
LWARNING(fmt::format("Could not find registered event '{}'",
events::toString(type))
);
}
}
std::vector<EventEngine::ActionInfo> EventEngine::registeredActions() const {
std::vector<EventEngine::ActionInfo> result;
result.reserve(_eventActions.size());
@@ -179,6 +224,27 @@ void EventEngine::triggerActions() const {
}
}
void EventEngine::triggerTopics() const {
if (_eventTopics.empty()) {
// Nothing to do here
return;
}
const events::Event* e = _firstEvent;
while (e) {
const auto it = _eventTopics.find(e->type);
if (it != _eventTopics.end()) {
ghoul::Dictionary params = toParameter(*e);
for (const TopicInfo& ti : it->second) {
ti.callback(params);
}
}
e = e->next;
}
}
scripting::LuaLibrary EventEngine::luaLibrary() {
return {
"event",