commit 7a05a87e72daf99aa6f7b52fae6d70f76645229e Author: Green Sky Date: Fri Dec 15 22:40:11 2023 +0100 working bridge diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56f48bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +.vs/ +*.o +*.swp +~* +*~ +.idea/ +cmake-build-debug/ +cmake-build-debugandtest/ +cmake-build-release/ +*.stackdump +*.coredump +compile_commands.json +/build* +/result* +.clangd +.cache + +.DS_Store +.AppleDouble +.LSOverride + +CMakeLists.txt.user* +CMakeCache.txt + +*.tox +imgui.ini diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..349b083 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,70 @@ +cmake_minimum_required(VERSION 3.9 FATAL_ERROR) + +# cmake setup begin +project(solanaceae_bridge) + +if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(SOLANACEAE_BRIDGE_STANDALONE ON) +else() + set(SOLANACEAE_BRIDGE_STANDALONE OFF) +endif() +message("II SOLANACEAE_BRIDGE_STANDALONE " ${SOLANACEAE_BRIDGE_STANDALONE}) + +option(SOLANACEAE_BRIDGE_BUILD_PLUGINS "Build the bridge plugin" ${SOLANACEAE_BRIDGE_STANDALONE}) + +if (SOLANACEAE_BRIDGE_STANDALONE) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + + # defaulting to debug mode, if not specified + if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE "Debug") + endif() + + # setup my vim ycm :D + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + + # more paths + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") +endif() + +# external libs +add_subdirectory(./external) # before increasing warn levels, sad :( + +if (SOLANACEAE_BRIDGE_STANDALONE) + set(CMAKE_CXX_EXTENSIONS OFF) + + # bump up warning levels appropriately for clang, gcc & msvc + if (${CMAKE_CXX_COMPILER_ID} STREQUAL "GNU" OR ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") + add_compile_options( + -Wall -Wextra # Reasonable and standard + -Wpedantic # Warn if non-standard C++ is used + -Wunused # Warn on anything being unused + #-Wconversion # Warn on type conversions that may lose data + #-Wsign-conversion # Warn on sign conversions + -Wshadow # Warn if a variable declaration shadows one from a parent context + ) + + if (NOT WIN32) + #link_libraries(-fsanitize=address,undefined) + #link_libraries(-fsanitize=undefined) + endif() + elseif (${CMAKE_CXX_COMPILER_ID} STREQUAL "MSVC") + if (CMAKE_CXX_FLAGS MATCHES "/W[0-4]") + string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") + else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") + endif() + endif() +endif() + +# cmake setup end + +add_subdirectory(./src) + +if (SOLANACEAE_BRIDGE_BUILD_PLUGINS) + message("II SOLANACEAE_BRIDGE_BUILD_PLUGINS " ${SOLANACEAE_BRIDGE_BUILD_PLUGINS}) + add_subdirectory(./plugins) +endif() + diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt new file mode 100644 index 0000000..7d48f78 --- /dev/null +++ b/external/CMakeLists.txt @@ -0,0 +1,50 @@ +cmake_minimum_required(VERSION 3.14...3.24 FATAL_ERROR) + +include(FetchContent) + +# TODO: move entt dep into solanaceae_contact +if (NOT TARGET EnTT::EnTT) + FetchContent_Declare(EnTT + GIT_REPOSITORY https://github.com/skypjack/entt.git + GIT_TAG v3.12.2 + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(EnTT) +endif() + +if (NOT TARGET solanaceae_util) + FetchContent_Declare(solanaceae_util + GIT_REPOSITORY https://github.com/Green-Sky/solanaceae_util.git + GIT_TAG master + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(solanaceae_util) +endif() + +if (NOT TARGET solanaceae_contact) + FetchContent_Declare(solanaceae_contact + GIT_REPOSITORY https://github.com/Green-Sky/solanaceae_contact.git + GIT_TAG master + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(solanaceae_contact) +endif() + +if (NOT TARGET solanaceae_message3) + FetchContent_Declare(solanaceae_message3 + GIT_REPOSITORY https://github.com/Green-Sky/solanaceae_message3.git + GIT_TAG master + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(solanaceae_message3) +endif() + +if (NOT TARGET solanaceae_plugin) + FetchContent_Declare(solanaceae_plugin + GIT_REPOSITORY https://github.com/Green-Sky/solanaceae_plugin.git + GIT_TAG master + EXCLUDE_FROM_ALL + ) + FetchContent_MakeAvailable(solanaceae_plugin) +endif() + diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..cd68fa6 --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.14...3.24 FATAL_ERROR) + +add_library(plugin_bridge SHARED + ./plugin_bridge.cpp +) + +target_link_libraries(plugin_bridge PUBLIC + solanaceae_plugin + solanaceae_bridge +) + diff --git a/plugins/plugin_bridge.cpp b/plugins/plugin_bridge.cpp new file mode 100644 index 0000000..8e997c6 --- /dev/null +++ b/plugins/plugin_bridge.cpp @@ -0,0 +1,78 @@ +#include + +#include "../src/bridge.hpp" + +#include +#include + +#define RESOLVE_INSTANCE(x) static_cast(solana_api->resolveInstance(#x)) +#define PROVIDE_INSTANCE(x, p, v) solana_api->provideInstance(#x, p, static_cast(v)) + +static std::unique_ptr g_bridge = nullptr; + +extern "C" { + +SOLANA_PLUGIN_EXPORT const char* solana_plugin_get_name(void) { + return "Bridge"; +} + +SOLANA_PLUGIN_EXPORT uint32_t solana_plugin_get_version(void) { + return SOLANA_PLUGIN_VERSION; +} + +SOLANA_PLUGIN_EXPORT uint32_t solana_plugin_start(struct SolanaAPI* solana_api) { + std::cout << "PLUGIN Bridge START()\n"; + + if (solana_api == nullptr) { + return 1; + } + + Contact3Registry* cr; + RegistryMessageModel* rmm = nullptr; + ConfigModelI* conf = nullptr; + + { // make sure required types are loaded + cr = RESOLVE_INSTANCE(Contact3Registry); + rmm = RESOLVE_INSTANCE(RegistryMessageModel); + conf = RESOLVE_INSTANCE(ConfigModelI); + + if (cr == nullptr) { + std::cerr << "PLUGIN Bridge missing Contact3Registry\n"; + return 2; + } + + if (rmm == nullptr) { + std::cerr << "PLUGIN Bridge missing RegistryMessageModel\n"; + return 2; + } + + if (conf == nullptr) { + std::cerr << "PLUGIN Bridge missing ConfigModelI\n"; + return 2; + } + } + + // static store, could be anywhere tho + // construct with fetched dependencies + g_bridge = std::make_unique(*cr, *rmm, *conf); + + // register types + PROVIDE_INSTANCE(Bridge, "Bridge", g_bridge.get()); + + return 0; +} + +SOLANA_PLUGIN_EXPORT void solana_plugin_stop(void) { + std::cout << "PLUGIN Bridge STOP()\n"; + + g_bridge.reset(); +} + +SOLANA_PLUGIN_EXPORT void solana_plugin_tick(float delta) { + (void)delta; + //std::cout << "PLUGIN Bridge TICK()\n"; + g_bridge->iterate(delta); +} + +} // extern C + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..2025deb --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.9 FATAL_ERROR) + +add_library(solanaceae_bridge STATIC + ./bridge.hpp + ./bridge.cpp +) + +target_compile_features(solanaceae_bridge PUBLIC cxx_std_17) +target_link_libraries(solanaceae_bridge PUBLIC + solanaceae_contact + solanaceae_message3 +) + diff --git a/src/bridge.cpp b/src/bridge.cpp new file mode 100644 index 0000000..d288c9b --- /dev/null +++ b/src/bridge.cpp @@ -0,0 +1,184 @@ +#include "./bridge.hpp" + +#include +#include +#include + +#include + +namespace detail { + constexpr uint8_t nib_from_hex(char c) { + assert((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')); + + if (c >= '0' && c <= '9') { + return static_cast(c) - '0'; + } else if (c >= 'a' && c <= 'f') { + return (static_cast(c) - 'a') + 10u; + } else { + return 0u; + } + } + + constexpr char nib_to_hex(uint8_t c) { + assert(c <= 0x0f); + + if (c <= 0x09) { + return c + '0'; + } else { + return (c - 10u) + 'a'; + } + } +} // detail + +static std::vector hex2bin(const std::string_view str) { + assert(str.size() % 2 == 0); + std::vector bin{}; + bin.resize(str.size()/2, 0); + + for (size_t i = 0; i < bin.size(); i++) { + bin[i] = detail::nib_from_hex(str[i*2]) << 4 | detail::nib_from_hex(str[i*2+1]); + } + + return bin; +} + +static std::string bin2hex(const std::vector data) { + std::string str; + for (size_t i = 0; i < data.size(); i++) { + str.push_back(detail::nib_to_hex(data[i] >> 4)); + str.push_back(detail::nib_to_hex(data[i] & 0x0f)); + } + return str; +} + +Bridge::Bridge( + Contact3Registry& cr, + RegistryMessageModel& rmm, + ConfigModelI& conf +) : _cr(cr), _rmm(rmm), _conf(conf) { + _rmm.subscribe(this, enumType::message_construct); + + // load synced contacts (bridged groups) + std::map tmp_name_to_id; + for (const auto [contact_id, vgroup_str] : _conf.entries_string("Bridge", "contact_to_vgroup")) { + const auto tmp_vgroup_str = std::string{vgroup_str.start, vgroup_str.start+vgroup_str.extend}; + if (!tmp_name_to_id.count(tmp_vgroup_str)) { + tmp_name_to_id[tmp_vgroup_str] = _vgroups.size(); + _vgroups.emplace_back();; + } + auto& v_group = _vgroups.at(tmp_name_to_id.at(tmp_vgroup_str)); + + auto& new_vgc = v_group.contacts.emplace_back(); + new_vgc.c = {_cr, entt::null}; // this is annoying af + new_vgc.id = hex2bin(contact_id); + } + + updateVGroups(); +} + +Bridge::~Bridge(void) { +} + +void Bridge::iterate(float time_delta) { + _iterate_timer += time_delta; + if (_iterate_timer >= 10.f) { + _iterate_timer = 0.f; + + updateVGroups(); + } +} + +void Bridge::updateVGroups(void) { + // fill in contacts, some contacts might be created late + for (size_t i_vg = 0; i_vg < _vgroups.size(); i_vg++) { + for (auto& vgc : _vgroups[i_vg].contacts) { + assert(!vgc.id.empty()); + + if (!vgc.c.valid()) { + // search + auto view = _cr.view(); + for (const auto c : view) { + if (view.get(c).data == vgc.id) { + vgc.c = {_cr, c}; + std::cout << "Bridge: found contact for vgroup\n"; + break; + } + } + } + + if (vgc.c.valid()) { + _c_to_vg[vgc.c] = i_vg; + } + } + } +} + +bool Bridge::onEvent(const Message::Events::MessageConstruct& e) { + if (!e.e.valid()) { + return false; // how + } + + if (!e.e.all_of()) { + return false; // non text message, skip + } + + if (!e.e.all_of()) { + return false; // how + } + + const auto contact_from = e.e.get().c; + if (_cr.any_of(contact_from)) { + return false; // skip own messages + } + + const auto contact_to = e.e.get().c; + // if e.e is in c to vg + const auto it = _c_to_vg.find(Contact3Handle{_cr, contact_to}); + if (it == _c_to_vg.cend()) { + return false; // contact is not bridged + } + const auto& vgroup = _vgroups.at(it->second); + const auto& message_text = e.e.get().text; + const bool is_action = e.e.all_of(); + + std::string from_str; + if (_cr.all_of(contact_from)) { + const auto& name = _cr.get(contact_from).name; + if (name.empty()) { + from_str += "(contact_from)) { + // copy + auto id = _cr.get(contact_from).data; + id.resize(3); + + from_str += "#" + bin2hex(id); + } + + from_str += "> "; + + // for each c in vg not c... + for (const auto& other_vc : vgroup.contacts) { + if (other_vc.c == contact_to) { + continue; // skip self + } + + // TODO: support fake/virtual contacts. true bridging + std::string relayed_message {from_str}; + + relayed_message += message_text; + + _rmm.sendText( + other_vc.c, + relayed_message, + is_action + ); + } + + return false; +} + diff --git a/src/bridge.hpp b/src/bridge.hpp new file mode 100644 index 0000000..286f17f --- /dev/null +++ b/src/bridge.hpp @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include + +#include +#include + +// fwd +struct ConfigModelI; + +class Bridge : public RegistryMessageModelEventI { + Contact3Registry& _cr; + RegistryMessageModel& _rmm; + ConfigModelI& _conf; + + struct VirtualGroups { + struct VContact { + Contact3Handle c; // might be null + std::vector id; // if contact appears, we check + }; + std::vector contacts; + + // metadata/settings? + }; + std::vector _vgroups; + std::map _c_to_vg; + + float _iterate_timer {0.f}; + + public: + Bridge( + Contact3Registry& cr, + RegistryMessageModel& rmm, + ConfigModelI& conf + ); + ~Bridge(void); + + void iterate(float time_delta); + + private: + void updateVGroups(void); + + protected: // mm + bool onEvent(const Message::Events::MessageConstruct& e) override; +}; +