diff --git a/Tests/Tests.cpp b/Tests/Tests.cpp index 6861234..fc7e1c7 100644 --- a/Tests/Tests.cpp +++ b/Tests/Tests.cpp @@ -395,6 +395,27 @@ void handle_participants_changed(void* context, interactive_session session, int Logger::WriteMessage(s.str().c_str()); } +void handle_control_changed(void* context, interactive_session session, interactive_control_change_type changeType, const interactive_control* control) +{ + std::stringstream s; + switch (changeType) + { + case interactive_control_created: + s << "Created "; + break; + case interactive_control_updated: + s << "Updated "; + break; + case interactive_control_deleted: + s << "Deleted "; + break; + default: + break; + } + s << "'" << control->id << "' (" << control->kind << ")\r\n"; + Logger::WriteMessage(s.str().c_str()); +} + void handle_error_assert(void* context, interactive_session session, int errorCode, const char* errorMessage, size_t errorMessageLength) { Logger::WriteMessage(("[ERROR] (" + std::to_string(errorCode) + ")" + errorMessage).c_str()); diff --git a/source/interactivity.h b/source/interactivity.h index 61df101..6517ee1 100644 --- a/source/interactivity.h +++ b/source/interactivity.h @@ -183,6 +183,12 @@ extern "C" { size_t kindLength; }; + enum interactive_control_change_type + { + interactive_control_created, + interactive_control_updated, + interactive_control_deleted + }; // Known control properties #define CONTROL_PROP_DISABLED "disabled" @@ -435,6 +441,11 @@ extern "C" { /// typedef void(*on_transaction_complete)(void* context, interactive_session session, const char* transactionId, size_t transactionIdLength, unsigned int error, const char* errorMessage, size_t errorMessageLength); + /// + /// Callback when a control is updated. + /// + typedef void(*on_control_changed)(void* context, interactive_session session, interactive_control_change_type changeType, const interactive_control* control); + /// /// Callback when any method is called that is not handled by the existing callbacks. This may be useful for more advanced scenarios or future protocol changes that may not have existed in this version of the library. /// @@ -465,6 +476,11 @@ extern "C" { /// int interactive_set_transaction_complete_handler(interactive_session session, on_transaction_complete onTransactionComplete); + /// + /// Set the handler function for control updates. This function is called by your own thread during interactive_run + /// + int interactive_set_control_changed_handler(interactive_session session, on_control_changed onControlUpdated); + /// /// Set the handler function for unhandled methods. This may be useful for more advanced scenarios or future protocol changes that may not have existed in this version of the library. This function is called by your own thread during interactive_run /// @@ -607,7 +623,8 @@ extern "C" { MIXER_ERROR_WS_DISCONNECT_FAILED, MIXER_ERROR_WS_READ_FAILED, MIXER_ERROR_WS_SEND_FAILED, - MIXER_ERROR_NOT_CONNECTED + MIXER_ERROR_NOT_CONNECTED, + MIXER_ERROR_OBJECT_EXISTS } mixer_result_code; /** @} */ diff --git a/source/internal/interactive_control.cpp b/source/internal/interactive_control.cpp index d953434..05b59b0 100644 --- a/source/internal/interactive_control.cpp +++ b/source/internal/interactive_control.cpp @@ -154,6 +154,118 @@ int verify_get_property_args_and_get_control_value(interactive_session session, return MIXER_OK; } +void parse_control(rapidjson::Value& controlJson, interactive_control& control) +{ + control.id = controlJson[RPC_CONTROL_ID].GetString(); + control.idLength = controlJson[RPC_CONTROL_ID].GetStringLength(); + if (controlJson.HasMember(RPC_CONTROL_KIND)) + { + control.kind = controlJson[RPC_CONTROL_KIND].GetString(); + control.kindLength = controlJson[RPC_CONTROL_KIND].GetStringLength(); + } +} + +int cache_new_control(interactive_session_internal& session, const char* sceneId, interactive_control& control, rapidjson::Value& controlJson) +{ + std::shared_lock readLock(session.scenesMutex); + if (session.controls.find(control.id) != session.controls.end()) + { + return MIXER_ERROR_OBJECT_EXISTS; + } + + auto sceneItr = session.scenes.find(sceneId); + if (sceneItr == session.scenes.end()) + { + return MIXER_ERROR_OBJECT_NOT_FOUND; + } + + std::string scenePtr = sceneItr->second; + rapidjson::Value* scene = rapidjson::Pointer(rapidjson::StringRef(scenePtr.c_str(), scenePtr.length())).Get(session.scenesRoot); + + rapidjson::Document::AllocatorType& allocator = session.scenesRoot.GetAllocator(); + auto controlsItr = scene->FindMember(RPC_PARAM_CONTROLS); + rapidjson::Value* controls; + if (controlsItr == scene->MemberEnd() || !controlsItr->value.IsArray()) + { + readLock.unlock(); + std::unique_lock writeLock(session.scenesMutex); + controls = &scene->AddMember(RPC_PARAM_CONTROLS, rapidjson::Value(rapidjson::kArrayType), allocator); + writeLock.unlock(); + readLock.lock(); + } + else + { + controls = &controlsItr->value; + } + + rapidjson::Value myControlJson(rapidjson::kObjectType); + myControlJson.CopyFrom(controlJson, session.scenesRoot.GetAllocator()); + controls->PushBack(myControlJson, allocator); + RETURN_IF_FAILED(update_control_pointers(session, sceneId)); + + return MIXER_OK; +} + + +int update_cached_control(interactive_session_internal& session, interactive_control& control, rapidjson::Value& controlJson) +{ + std::unique_lock writeLock(session.scenesMutex); + auto itr = session.controls.find(control.id); + if (itr == session.controls.end()) + { + return MIXER_ERROR_OBJECT_NOT_FOUND; + } + + std::string controlPtr = itr->second; + rapidjson::Value myControlJson(rapidjson::kObjectType); + myControlJson.CopyFrom(controlJson, session.scenesRoot.GetAllocator()); + rapidjson::Pointer(rapidjson::StringRef(controlPtr.c_str(), controlPtr.length())) + .Swap(session.scenesRoot, myControlJson); + + return MIXER_OK; +} + +int delete_cached_control(interactive_session_internal& session, const char* sceneId, interactive_control& control) +{ + std::shared_lock readLock(session.scenesMutex); + if (session.controls.find(control.id) == session.controls.end()) + { + // This control doesn't exist, ignore this deletion. + return MIXER_OK; + } + + auto sceneItr = session.scenes.find(sceneId); + if (sceneItr == session.scenes.end()) + { + return MIXER_ERROR_OBJECT_NOT_FOUND; + } + + // Find the controls array on the scene. + std::string scenePtr = sceneItr->second; + rapidjson::Value* scene = rapidjson::Pointer(rapidjson::StringRef(scenePtr.c_str(), scenePtr.length())).Get(session.scenesRoot); + auto controlsItr = scene->FindMember(RPC_PARAM_CONTROLS); + if (controlsItr == scene->MemberEnd() || !controlsItr->value.IsArray()) + { + // If the scene has no controls on it, ignore this deletion. + return MIXER_OK; + } + + // Erase the value from the array. + rapidjson::Value* controls = &controlsItr->value; + for (auto controlItr = controls->Begin(); controlItr != controls->End(); ++controlItr) + { + if (0 == strcmp(controlItr->GetObject()[RPC_CONTROL_ID].GetString(), control.id)) + { + controls->Erase(controlItr); + break; + } + } + + RETURN_IF_FAILED(update_control_pointers(session, sceneId)); + + return MIXER_OK; +} + } using namespace mixer_internal; @@ -298,8 +410,6 @@ int interactive_control_get_meta_property_data(interactive_session session, cons return err; } - - int interactive_control_get_property_int(interactive_session session, const char* controlId, const char* key, int* property) { rapidjson::Value* controlValue; diff --git a/source/internal/interactive_scene.cpp b/source/internal/interactive_scene.cpp index 281b148..ff8b9ee 100644 --- a/source/internal/interactive_scene.cpp +++ b/source/internal/interactive_scene.cpp @@ -24,11 +24,27 @@ int cache_scenes(interactive_session_internal& session) scenesArray.CopyFrom(replyScenesArray, session.scenesRoot.GetAllocator()); session.scenesRoot.AddMember(RPC_PARAM_SCENES, scenesArray, session.scenesRoot.GetAllocator()); + RETURN_IF_FAILED(update_control_pointers(session)); + + return MIXER_OK; +} + +int update_control_pointers(interactive_session_internal& session, const char* sceneId) +{ // Iterate through each scene and set up a pointer to each control. int sceneIndex = 0; for (auto& scene : session.scenesRoot[RPC_PARAM_SCENES].GetArray()) { std::string scenePointer = "/" + std::string(RPC_PARAM_SCENES) + "/" + std::to_string(sceneIndex++); + auto thisSceneId = scene[RPC_SCENE_ID].GetString(); + if (nullptr != sceneId) + { + if (0 != strcmp(sceneId, thisSceneId)) + { + continue; + } + } + auto controlsArray = scene.FindMember(RPC_PARAM_CONTROLS); if (controlsArray != scene.MemberEnd() && controlsArray->value.IsArray()) { @@ -39,7 +55,7 @@ int cache_scenes(interactive_session_internal& session) } } - session.scenes.emplace(scene[RPC_SCENE_ID].GetString(), scenePointer); + session.scenes.emplace(thisSceneId, scenePointer); } return MIXER_OK; @@ -136,6 +152,12 @@ int interactive_scene_get_controls(interactive_session session, const char* scen for (auto& controlObj : (*sceneVal)[RPC_PARAM_CONTROLS].GetArray()) { interactive_control control; + if (!controlObj.IsObject()) { + return MIXER_ERROR_UNRECOGNIZED_DATA_FORMAT; + } + if (!controlObj.HasMember(RPC_CONTROL_ID) || !controlObj[RPC_CONTROL_ID].IsString()) { + return MIXER_ERROR_UNRECOGNIZED_DATA_FORMAT; + } control.id = controlObj[RPC_CONTROL_ID].GetString(); control.idLength = controlObj[RPC_CONTROL_ID].GetStringLength(); control.kind = controlObj[RPC_CONTROL_KIND].GetString(); diff --git a/source/internal/interactive_session.cpp b/source/internal/interactive_session.cpp index 30f9cb7..79f3218 100644 --- a/source/internal/interactive_session.cpp +++ b/source/internal/interactive_session.cpp @@ -387,6 +387,52 @@ int handle_scene_changed(interactive_session_internal& session, rapidjson::Docum return cache_scenes(session); } +int handle_control_changed(interactive_session_internal& session, rapidjson::Document& doc) +{ + if (!doc.HasMember(RPC_PARAMS) + || !doc[RPC_PARAMS].HasMember(RPC_PARAM_CONTROLS) || !doc[RPC_PARAMS][RPC_PARAM_CONTROLS].IsArray() + || !doc[RPC_PARAMS].HasMember(RPC_SCENE_ID) || !doc[RPC_PARAMS][RPC_SCENE_ID].IsString()) + { + return MIXER_ERROR_UNRECOGNIZED_DATA_FORMAT; + } + + const char * sceneId = doc[RPC_PARAMS][RPC_SCENE_ID].GetString(); + rapidjson::Value& controls = doc[RPC_PARAMS][RPC_PARAM_CONTROLS]; + for (auto itr = controls.Begin(); itr != controls.End(); ++itr) + { + interactive_control control; + memset(&control, 0, sizeof(interactive_control)); + parse_control(*itr, control); + + interactive_control_change_type changeType = interactive_control_updated; + if (0 == strcmp(doc[RPC_METHOD].GetString(), RPC_METHOD_ON_CONTROL_UPDATE)) + { + RETURN_IF_FAILED(update_cached_control(session, control, *itr)); + } + else if (0 == strcmp(doc[RPC_METHOD].GetString(), RPC_METHOD_ON_CONTROL_CREATE)) + { + changeType = interactive_control_created; + RETURN_IF_FAILED(cache_new_control(session, sceneId, control, *itr)); + } + else if (0 == strcmp(doc[RPC_METHOD].GetString(), RPC_METHOD_ON_CONTROL_DELETE)) + { + changeType = interactive_control_deleted; + RETURN_IF_FAILED(delete_cached_control(session, sceneId, control)); + } + else + { + return MIXER_ERROR_UNKNOWN_METHOD; + } + + if (session.onControlChanged) + { + session.onControlChanged(session.callerContext, &session, changeType, &control); + } + } + + return MIXER_OK; +} + int route_method(interactive_session_internal& session, rapidjson::Document& doc) { std::string method = doc[RPC_METHOD].GetString(); @@ -418,6 +464,7 @@ void register_method_handlers(interactive_session_internal& session) session.methodHandlers.emplace(RPC_METHOD_ON_PARTICIPANT_UPDATE, handle_participants_update); session.methodHandlers.emplace(RPC_METHOD_ON_GROUP_UPDATE, handle_group_changed); session.methodHandlers.emplace(RPC_METHOD_ON_GROUP_CREATE, handle_group_changed); + session.methodHandlers.emplace(RPC_METHOD_ON_CONTROL_UPDATE, handle_control_changed); session.methodHandlers.emplace(RPC_METHOD_UPDATE_SCENES, handle_scene_changed); } @@ -1004,6 +1051,20 @@ int interactive_set_transaction_complete_handler(interactive_session session, on return MIXER_OK; } + +int interactive_set_control_changed_handler(interactive_session session, on_control_changed onControlChanged) +{ + if (nullptr == session) + { + return MIXER_ERROR_INVALID_POINTER; + } + + interactive_session_internal* sessionInternal = reinterpret_cast(session); + sessionInternal->onControlChanged = onControlChanged; + + return MIXER_OK; +} + int interactive_set_unhandled_method_handler(interactive_session session, on_unhandled_method onUnhandledMethod) { if (nullptr == session) diff --git a/source/internal/interactive_session.h b/source/internal/interactive_session.h index 648179d..b5f8d65 100644 --- a/source/internal/interactive_session.h +++ b/source/internal/interactive_session.h @@ -70,6 +70,7 @@ struct interactive_session_internal on_error onError; on_state_changed onStateChanged; on_participants_changed onParticipantsChanged; + on_control_changed onControlChanged; on_transaction_complete onTransactionComplete; on_unhandled_method onUnhandledMethod; @@ -130,7 +131,10 @@ int receive_reply(interactive_session_internal& session, unsigned int id, std::s int cache_groups(interactive_session_internal& session); int cache_scenes(interactive_session_internal& session); +int update_cached_control(interactive_session_internal& session, interactive_control& control, rapidjson::Value& controlJson); +int update_control_pointers(interactive_session_internal& session, const char* sceneId = nullptr); void parse_participant(rapidjson::Value& participantJson, interactive_participant& participant); +void parse_control(rapidjson::Value& controlJson, interactive_control& control); // Common reply handler that checks a reply for errors and calls the session's error handler if it exists. int check_reply_errors(interactive_session_internal& session, rapidjson::Document& reply); @@ -160,7 +164,9 @@ int check_reply_errors(interactive_session_internal& session, rapidjson::Documen #define RPC_METHOD_ON_READY_CHANGED "onReady" // called by server to both game and participant clients #define RPC_METHOD_ON_GROUP_CREATE "onGroupCreate" #define RPC_METHOD_ON_GROUP_UPDATE "onGroupUpdate" +#define RPC_METHOD_ON_CONTROL_CREATE "onControlCreate" #define RPC_METHOD_ON_CONTROL_UPDATE "onControlUpdate" +#define RPC_METHOD_ON_CONTROL_DELETE "onControlDelete" #define RPC_METHOD_GET_TIME "getTime" #define RPC_TIME "time" #define RPC_PARAM_IS_READY "isReady" diff --git a/source/internal/interactive_session_internal.cpp b/source/internal/interactive_session_internal.cpp index 1782910..284b914 100644 --- a/source/internal/interactive_session_internal.cpp +++ b/source/internal/interactive_session_internal.cpp @@ -6,7 +6,7 @@ namespace mixer_internal interactive_session_internal::interactive_session_internal() : callerContext(nullptr), isReady(false), state(interactive_disconnected), shutdownRequested(false), packetId(0), sequenceId(0), wsOpen(false), - onInput(nullptr), onError(nullptr), onStateChanged(nullptr), onParticipantsChanged(nullptr), onUnhandledMethod(nullptr) + onInput(nullptr), onError(nullptr), onStateChanged(nullptr), onParticipantsChanged(nullptr), onUnhandledMethod(nullptr), onControlChanged(nullptr) { scenesRoot.SetObject(); }