Repository: magnopus-opensource/connected-spaces-platform
Pull request #679: [OF-1510] feat: Add new entity serializer
Author: login: MAG-mv
Created: 2025-05-02T09:12:11Z | Merged: 2025-05-09T10:52:45Z

This is the first PR as part of this ticket

- Created new generic signalr serializer/deserializer 
- Added tests
- A stack-based serializer/deserializer which wraps signalr and exposes all of its supported types

This PR aims to fix issues with the old SignalRMsgPackSerializer by rewriting:

- The old system mixes multiple layers of abstraction, as it handles  SignalR serialization logic and converting CSP object types to the expected structure for MCS. This created unnecessary complexity with the public interface and with the internal logic. This also made modifying the serializer if new CSP types are introduced risky and time-consuming.

- The old system only supported entity serialization, so we couldn't serialize event types. This has led to custom signalr conversion throughout our software.

- The new system gives the ability to write custom object serializer/deseriaizers

- The new system is well tested
 


Diff:
diff --git a/Library/src/Multiplayer/SignalRSerializer.cpp b/Library/src/Multiplayer/SignalRSerializer.cpp
new file mode 100644
index 000000000..6878f5cfa
--- /dev/null
+++ b/Library/src/Multiplayer/SignalRSerializer.cpp
@@ -0,0 +1,301 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::StartWriteArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::EndWriteArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto ArrayObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerialization(signalr::value { std::get<std::vector<signalr::value>>(ArrayObject) });
+}
+
+void SignalRSerializer::StartWriteStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto StringMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerialization(signalr::value { std::get<std::map<std::string, signalr::value>>(StringMapObject) });
+}
+
+void SignalRSerializer::StartWriteUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto UintMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerialization(signalr::value { std::get<std::map<uint64_t, signalr::value>>(UintMapObject) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    if (std::holds_alternative<signalr::value>(Stack.top()))
+    {
+        return std::get<signalr::value>(Stack.top());
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Stack.top()) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::FinalizeContainerSerialization(signalr::value&& SerializedContainer)
+{
+    // We dont check for maps in this function because key-values are handled separately.
+    if (Stack.size() == 0)
+    {
+        // If the last element has been popped, push back a root value.
+        Stack.push(SerializedContainer);
+        return;
+    }
+
+    // Dispatch internal variant type to the correct FinalizeContainerSerializationInternal call.
+    std::visit([this, &SerializedContainer](auto& ValType) { this->FinalizeContainerSerializationInternal(ValType, std::move(SerializedContainer)); },
+        Stack.top());
+}
+
+void SignalRSerializer::FinalizeContainerSerializationInternal(std::vector<signalr::value>& Container, signalr::value&& SerializedContainer)
+{
+    Container.push_back(SerializedContainer);
+}
+
+SignalRDeserializer::SignalRDeserializer(const signalr::value& Object)
+    : Root { Object }
+{
+    ObjectStack.push(nullptr);
+}
+
+SignalRDeserializer::SignalRDeserializer(signalr::value&& Object)
+    : Root { std::move(Object) }
+{
+    ObjectStack.push(nullptr);
+}
+
+void SignalRDeserializer::StartReadArray(size_t& Size)
+{
+    const signalr::value& ArrayObject = ReadNextValue();
+
+    if (ArrayObject.is_array() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't an array");
+    }
+
+    ObjectStack.push(ArrayObject.as_array().begin());
+
+    Size = ArrayObject.as_array().size();
+}
+
+void SignalRDeserializer::EndReadArray()
+{
+    EndReadArrayInternal();
+    IncrementIterator();
+}
+
+void SignalRDeserializer::StartReadUintMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_uint_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a uint map");
+    }
+
+    ObjectStack.push(MapObject.as_uint_map().begin());
+
+    Size = MapObject.as_uint_map().size();
+}
+void SignalRDeserializer::EndReadUintMap()
+{
+    EndReadUintMapInternal();
+    IncrementIterator();
+}
+
+void SignalRDeserializer::StartReadStringMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_string_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a string map");
+    }
+
+    ObjectStack.push(MapObject.as_string_map().begin());
+
+    Size = MapObject.as_string_map().size();
+}
+
+void SignalRDeserializer::EndReadStringMap()
+{
+    EndReadStringMapInternal();
+    IncrementIterator();
+}
+
+const signalr::value& SignalRDeserializer::ReadNextValue()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        return *std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top());
+    }
+    else if (std::holds_alternative<nullptr_t>(ObjectStack.top()))
+    {
+        return Root;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected deserializer state");
+    }
+}
+
+void SignalRDeserializer::EndReadArrayInternal()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in an array");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EndReadUintMapInternal()
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EndReadStringMapInternal()
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    ObjectStack.pop();
+}
+
+const std::pair<std::uint64_t, signalr::value> SignalRDeserializer::ReadNextUintKeyValue() const
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    return *std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+const std::pair<std::string, signalr::value> SignalRDeserializer::ReadNextStringKeyValue() const
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    return *std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, double& OutVal)
+{
+    if (Object.is_double() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a double");
+    }
+
+    OutVal = Object.as_double();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, float& OutVal)
+{
+    if (Object.is_double() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a double");
+    }
+
+    OutVal = static_cast<float>(Object.as_double());
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, bool& OutVal)
+{
+    if (Object.is_bool() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a bool");
+    }
+
+    OutVal = Object.as_bool();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, std::string& OutVal)
+{
+    if (Object.is_string() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a string");
+    }
+
+    OutVal = Object.as_string();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, nullptr_t)
+{
+    if (Object.is_null() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not null");
+    }
+}
+
+void SignalRDeserializer::IncrementIterator()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+}
+}
diff --git a/Library/src/Multiplayer/SignalRSerializer.h b/Library/src/Multiplayer/SignalRSerializer.h
new file mode 100644
index 000000000..ea4be708f
--- /dev/null
+++ b/Library/src/Multiplayer/SignalRSerializer.h
@@ -0,0 +1,571 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <limits>
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A serializer which allows for custom class serialization using ISignalRSerializable interface.
+/// @details Writing single values is supported. However, if you want to serialize multiple values,
+/// you will need to add a container, by calling WriteValue on one of the supported containers,
+/// or calling StartWrite[X].
+///
+/// Currently supported types:
+///     - Unsigned integers
+///     - Signed integers
+///     - Doubles
+///     - Floats
+///     - String
+///     - Null pointers (represents null)
+///     - Optionals
+///     - Vectors
+///     - Unsigned integer key maps
+///     - string key maps
+///     - Types with ISignalRSerializable interface
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopStringMap should be used to finalize the map.
+    void StartWriteStringMap();
+
+    /// @brief Ends the current string map in the serializer.
+    /// @pre StartWriteStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteStringMap();
+
+    /// @brief Starts writing a uint map (std::map<uint64_t, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopUintMap should be used to finalize the map.
+    void StartWriteUintMap();
+
+    /// @brief Ends the current uint map in the serializer.
+    /// @pre StartWriteUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteUintMap();
+
+    /// @brief Gets the serialized singnal r value.
+    /// @return signalr::value
+    /// @pre The serializer should be at the root (array and maps should all be ended using EndWrite[X]).
+    signalr::value Get() const;
+
+private:
+    // Adds the serialized container object to the previous object, or the root.
+    // This will internally call one of the FinalizeContainerSerializationInternal overrides.
+    void FinalizeContainerSerialization(signalr::value&& SerializedContainer);
+
+    // Case where we fallback to an exception is we are trying to serialize to an unsupported container.
+    template <typename T> void FinalizeContainerSerializationInternal(T&, signalr::value&& SerializedContainer);
+    // Case for serializing value into a vector.
+    void FinalizeContainerSerializationInternal(std::vector<signalr::value>& Container, signalr::value&& SerializedContainer);
+    // Case for serializing value into a pair for a map.
+    template <typename K> void FinalizeContainerSerializationInternal(std::pair<K, signalr::value>& Pair, signalr::value&& SerializedContainer);
+
+    // Writes all of our primitive types to a signalr value.
+    template <typename T> void WriteValueInternal(const T& Value);
+
+    // Case for writing container types to a signalr value.
+    // These cases are recursive.
+    template <typename T> void WriteValueInternal(const std::optional<T>& Value);
+    template <typename T> void WriteValueInternal(const std::vector<T>& Value);
+    template <typename K, typename T> void WriteValueInternal(const std::map<K, T>& Value);
+    template <typename T> void WriteValueInternal(const std::map<std::string, T>& Value);
+
+    // Converts primitive value to a signalr object.
+    template <typename T> signalr::value CreateSignalRObject(const T& Value);
+
+    using Container = std::variant<signalr::value, std::vector<signalr::value>, std::map<std::string, signalr::value>,
+        std::map<uint64_t, signalr::value>, std::pair<uint64_t, signalr::value>, std::pair<std::string, signalr::value>>;
+
+    std::stack<Container> Stack;
+};
+
+/// @brief A signalr deserializer which allows for custom class deserialization using ISignalRDeserializable interface.
+/// @details This supports all types outlined in the SignalRSerializer, except ISignalRDeserializable is needed for deserialization.
+/// If ISignalRDeserializable types are being deserialized from a container, then need to be default constructable.
+class SignalRDeserializer
+{
+public:
+    /// @brief Constructor used to copy object to deserialize.
+    /// @param Object const signalr::value& : The value to deserialize
+    /// This should match the structure generated by the SignalRSerializer.
+    SignalRDeserializer(const signalr::value& Object);
+
+    /// @brief Constructor used to move object to deserialize.
+    /// @param Object signalr::value&& : The value to deserialize
+    /// This should match the structure generated by the SignalRSerializer.
+    SignalRDeserializer(signalr::value&& Object);
+
+    /// @brief Reads a value from the internal signalr array.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartReadArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> ReadValue(T& OutVal);
+
+    /// @brief Reads a uint key-value pair to the current uint map.
+    /// @pre EnterUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> ReadKeyValue(std::pair<K, T>& OutVal);
+
+    /// @brief Reads a string key-value pair to the current string map.
+    /// @pre EnterStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> ReadKeyValue(std::pair<std::string, T>& OutVal);
+
+    /// @brief Enters the internal signalr value as an array
+    /// @details Once this function has been called, ReadValue should be used to read elements from the array.
+    /// @param Size size_t& Output parameter specifying the size of the array.
+    /// ExitArray should be used to exit the array once elements have been read.
+    void StartReadArray(size_t& Size);
+
+    /// @brief Exits the internal signalr array.
+    /// @pre StartReadArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndReadArray();
+
+    /// @brief Enters the internal signalr value as an uint map
+    /// @details Once this function has been called, ReadKeyValue should be used to read elements from the map.
+    /// @param Size size_t& Output parameter specifying the size of the map.
+    /// ExitUintMap should be used to exit the map once elements have been read.
+    void StartReadUintMap(size_t& Size);
+
+    /// @brief Exits the internal signalr uint map.
+    /// @pre StartReadUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndReadUintMap();
+
+    /// @brief Enters the internal signalr value as an string map
+    /// @details Once this function has been called, ReadKeyValue should be used to read elements from the map.
+    /// @param Size size_t& Output parameter specifying the size of the map.
+    /// ExitStringMap should be used to exit the map once elements have been read.
+    void StartReadStringMap(size_t& Size);
+
+    /// @brief Exits the internal signalr string map.
+    /// @pre StartReadStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndReadStringMap();
+
+private:
+    // Reads the next signalr value using the current iterator.
+    const signalr::value& ReadNextValue();
+
+    void EndReadArrayInternal();
+    void EndReadUintMapInternal();
+    void EndReadStringMapInternal();
+
+    const std::pair<std::uint64_t, signalr::value> ReadNextUintKeyValue() const;
+    const std::pair<std::string, signalr::value> ReadNextStringKeyValue() const;
+
+    // Reads the specified type from the given signalr object.
+    // This internally calls the ReadValueFromObjectInternal for the given type.
+    template <typename T> void ReadValueFromObject(const signalr::value& Object, T& OutVal);
+
+    // Case for reading signed integers.
+    template <typename T> std::enable_if_t<IsSignedIntegerV<T>> ReadValueFromObjectInternal(const signalr::value& Object, T& OutVal);
+    // Case for reading unsigned integers.
+    template <typename T> std::enable_if_t<IsUnsignedIntegerV<T>> ReadValueFromObjectInternal(const signalr::value& Object, T& OutVal);
+    // Case for other primitive types.
+    void ReadValueFromObjectInternal(const signalr::value& Object, double& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, float& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, bool& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, std::string& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, nullptr_t);
+
+    // Case for other container values.
+    template <typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::optional<T>& OutVal);
+    template <typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::vector<T>& OutVal);
+    template <typename K, typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::map<K, T>& OutVal);
+    template <typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::map<std::string, T>& OutVal);
+
+    void IncrementIterator();
+
+    using Iterator = std::variant<nullptr_t, std::vector<signalr::value>::const_iterator, std::map<uint64_t, signalr::value>::const_iterator,
+        std::map<std::string, signalr::value>::const_iterator>;
+
+    signalr::value Root;
+    std::stack<Iterator> ObjectStack;
+};
+
+/// @brief A serializer interface to allow classes to be serialized.
+class ISignalRSerializable
+{
+public:
+    /// @brief Function internally called when serializing this class using SignalRSerializer.
+    virtual void Serialize(SignalRSerializer&) const = 0;
+
+    virtual ~ISignalRSerializable() = default;
+
+protected:
+    ISignalRSerializable() = default;
+
+    ISignalRSerializable(const ISignalRSerializable&) = default;
+    ISignalRSerializable(ISignalRSerializable&&) = default;
+
+    ISignalRSerializable& operator=(const ISignalRSerializable&) = default;
+    ISignalRSerializable& operator=(ISignalRSerializable&&) = default;
+};
+
+/// @brief A deserializer interace to allow classes to be deserialized.
+class ISignalRDeserializable
+{
+public:
+    /// @brief Function internally called when serializing this class using SignalRDeserializer.
+    virtual void Deserialize(SignalRDeserializer&) = 0;
+
+    virtual ~ISignalRDeserializable() = default;
+
+protected:
+    ISignalRDeserializable() = default;
+
+    ISignalRDeserializable(const ISignalRDeserializable&) = default;
+    ISignalRDeserializable(ISignalRDeserializable&&) = default;
+
+    ISignalRDeserializable& operator=(const ISignalRDeserializable&) = default;
+    ISignalRDeserializable& operator=(ISignalRDeserializable&&) = default;
+};
+
+template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> SignalRSerializer::WriteValue(const T& Value) { WriteValueInternal(Value); }
+
+template <typename K, typename T>
+std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> SignalRSerializer::WriteKeyValue(K Key, const T& Value)
+{
+    if (Stack.size() == 0 || std ::holds_alternative<std::map<uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    Stack.push(std::pair<uint64_t, signalr::value> {});
+    std::get<std::pair<uint64_t, signalr::value>>(Stack.top()).first = static_cast<uint64_t>(Key);
+
+    WriteValue(Value);
+
+    // Get our pair from the top of the stack and pop
+    std::pair<uint64_t, signalr::value> Pair = std::get<std::pair<uint64_t, signalr::value>>(Stack.top());
+    Stack.pop();
+
+    // Get our map from the top of the stack and append the pair
+    std::map<uint64_t, signalr::value>& Map = std::get<std::map<uint64_t, signalr::value>>(Stack.top());
+    Map[Pair.first] = Pair.second;
+}
+
+template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> SignalRSerializer::WriteKeyValue(std::string Key, const T& Value)
+{
+    if (Stack.size() == 0 || std ::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    Stack.push(std::pair<std::string, signalr::value> {});
+    std::get<std::pair<std::string, signalr::value>>(Stack.top()).first = Key;
+
+    WriteValue(Value);
+
+    // Get our pair from the top of the stack and pop.
+    std::pair<std::string, signalr::value> Pair = std::get<std::pair<std::string, signalr::value>>(Stack.top());
+    Stack.pop();
+
+    // Get our map from the top of the stack and append the pair.
+    std::map<std::string, signalr::value>& Map = std::get<std::map<std::string, signalr::value>>(Stack.top());
+    Map[Pair.first] = Pair.second;
+}
+
+template <typename T> inline void SignalRSerializer::FinalizeContainerSerializationInternal(T&, signalr::value&&)
+{
+    throw std::runtime_error("Unexpected serializer state");
+}
+
+template <typename K>
+inline void SignalRSerializer::FinalizeContainerSerializationInternal(std::pair<K, signalr::value>& Pair, signalr::value&& SerializedContainer)
+{
+    Pair.second = SerializedContainer;
+}
+
+template <typename T> inline void SignalRSerializer::WriteValueInternal(const T& Value)
+{
+    signalr::value SerializedValue;
+
+    if constexpr (IsCustomSerializableV<T>)
+    {
+        Value.Serialize(*this);
+    }
+    else
+    {
+        if (Stack.size() == 0)
+        {
+            // Case where a user only want to serialize a single value
+            Stack.push(CreateSignalRObject(Value));
+        }
+        else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+        {
+            std::get<std::vector<signalr::value>>(Stack.top()).push_back(CreateSignalRObject(Value));
+        }
+        else if (std::holds_alternative<std::pair<uint64_t, signalr::value>>(Stack.top()))
+        {
+            std::get<std::pair<uint64_t, signalr::value>>(Stack.top()).second = CreateSignalRObject(Value);
+        }
+        else if (std::holds_alternative<std::pair<std::string, signalr::value>>(Stack.top()))
+        {
+            std::get<std::pair<std::string, signalr::value>>(Stack.top()).second = CreateSignalRObject(Value);
+        }
+        else
+        {
+            throw std::runtime_error("Invalid call: Serializer was not in an array or at the root");
+        }
+    }
+}
+
+template <typename T> inline void SignalRSerializer::WriteValueInternal(const std::optional<T>& Value)
+{
+    if (Value.has_value())
+    {
+        WriteValueInternal(*Value);
+    }
+    else
+    {
+        WriteValueInternal<nullptr_t>(nullptr);
+    }
+}
+
+template <typename T> inline void SignalRSerializer::WriteValueInternal(const std::vector<T>& Value)
+{
+    StartWriteArray();
+
+    for (const auto& Element : Value)
+    {
+        WriteValue(Element);
+    }
+
+    EndWriteArray();
+}
+
+template <typename K, typename T> inline void SignalRSerializer::WriteValueInternal(const std::map<K, T>& Value)
+{
+    StartWriteUintMap();
+
+    for (const auto& Pair : Value)
+    {
+        WriteKeyValue(Pair.first, Pair.second);
+    }
+
+    EndWriteUintMap();
+}
+
+template <typename T> inline void SignalRSerializer::WriteValueInternal(const std::map<std::string, T>& Value)
+{
+    StartWriteStringMap();
+
+    for (const auto& Pair : Value)
+    {
+        WriteKeyValue(Pair.first, Pair.second);
+    }
+
+    EndWriteStringMap();
+}
+
+template <typename T> inline signalr::value SignalRSerializer::CreateSignalRObject(const T& Value)
+{
+    // Allows us to support different integer types, as signalr only supports uint64 and int64.
+    if constexpr (IsSignedIntegerV<T>)
+    {
+        return signalr::value { static_cast<int64_t>(Value) };
+    }
+    else if constexpr (IsUnsignedIntegerV<T>)
+    {
+        return signalr::value { static_cast<uint64_t>(Value) };
+    }
+    else
+    {
+        return signalr::value { Value };
+    }
+}
+
+template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> SignalRDeserializer::ReadValue(T& OutVal)
+{
+    const signalr::value& Next = ReadNextValue();
+    ReadValueFromObject(Next, OutVal);
+
+    IncrementIterator();
+}
+
+template <typename K, typename T>
+std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> SignalRDeserializer::ReadKeyValue(std::pair<K, T>& OutVal)
+{
+    const std::pair<uint64_t, signalr::value>& Next = ReadNextUintKeyValue();
+
+    OutVal.first = static_cast<K>(Next.first);
+    ReadValueFromObject(Next.second, OutVal.second);
+
+    IncrementIterator();
+}
+
+template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> SignalRDeserializer::ReadKeyValue(std::pair<std::string, T>& OutVal)
+{
+    const std::pair<std::string, signalr::value>& Next = ReadNextStringKeyValue();
+
+    OutVal.first = Next.first;
+    ReadValueFromObject(Next.second, OutVal.second);
+
+    IncrementIterator();
+}
+
+template <typename T> void SignalRDeserializer::ReadValueFromObject(const signalr::value& Object, T& OutVal)
+{
+    if constexpr (IsCustomDeserializableV<T>)
+    {
+        SignalRDeserializer Deserializer { Object };
+        OutVal.Deserialize(Deserializer);
+    }
+    else if constexpr (IsSupportedSignalRType<T>::value)
+    {
+        ReadValueFromObjectInternal(Object, OutVal);
+    }
+    else
+    {
+        static_assert(IsSupportedSignalRType<T>::value, "Type passed to ReadValueFromObject is not supported by SignalR deserialization.");
+    }
+}
+
+template <typename T> std::enable_if_t<IsSignedIntegerV<T>> SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, T& OutVal)
+{
+    if (Object.is_integer() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not an integer");
+    }
+
+    if (Object.as_integer() > std::numeric_limits<T>().max() || Object.as_integer() < std::numeric_limits<T>().min())
+    {
+        throw std::runtime_error("Invalid uinteger type: Value being deserialized is larger than the maximum value of the input type");
+    }
+
+    OutVal = static_cast<T>(Object.as_integer());
+}
+
+template <typename T>
+std::enable_if_t<IsUnsignedIntegerV<T>> SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, T& OutVal)
+{
+    if (Object.is_uinteger() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a uinteger");
+    }
+
+    if (Object.as_uinteger() > std::numeric_limits<T>().max())
+    {
+        throw std::runtime_error("Invalid uinteger type: Value being deserialized is larger than the maximum value of the input type");
+    }
+
+    OutVal = static_cast<T>(Object.as_uinteger());
+}
+
+template <typename T> inline void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, std::optional<T>& OutVal)
+{
+    if (Object.is_null())
+    {
+        OutVal = std::nullopt;
+    }
+    else
+    {
+        OutVal = T {};
+        ReadValueFromObject(Object, *OutVal);
+    }
+}
+
+template <typename T> inline void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value&, std::vector<T>& OutVal)
+{
+    size_t ArraySize = 0;
+    StartReadArray(ArraySize);
+
+    OutVal.resize(ArraySize);
+
+    for (size_t i = 0; i < ArraySize; ++i)
+    {
+        ReadValue(OutVal[i]);
+    }
+
+    EndReadArrayInternal();
+}
+
+template <typename K, typename T> inline void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value&, std::map<K, T>& OutVal)
+{
+    size_t MapSize = 0;
+    StartReadUintMap(MapSize);
+
+    for (size_t i = 0; i < MapSize; ++i)
+    {
+        std::pair<K, T> Pair;
+        ReadKeyValue(Pair);
+        OutVal[Pair.first] = Pair.second;
+    }
+
+    EndReadUintMapInternal();
+}
+
+template <typename T> inline void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value&, std::map<std::string, T>& OutVal)
+{
+    size_t MapSize = 0;
+    StartReadStringMap(MapSize);
+
+    for (size_t i = 0; i < MapSize; ++i)
+    {
+        std::pair<std::string, T> Pair;
+        ReadKeyValue(Pair);
+        OutVal[Pair.first] = Pair.second;
+    }
+
+    EndReadStringMapInternal();
+}
+}
diff --git a/Library/src/Multiplayer/SignalRSerializerTypeTraits.h b/Library/src/Multiplayer/SignalRSerializerTypeTraits.h
new file mode 100644
index 000000000..992a4fdc6
--- /dev/null
+++ b/Library/src/Multiplayer/SignalRSerializerTypeTraits.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <signalrclient/signalr_value.h>
+
+#include <optional>
+#include <type_traits>
+#include <variant>
+#include <vector>
+
+namespace csp::multiplayer
+{
+class ISignalRSerializable;
+class ISignalRDeserializable;
+
+// Type Checks ----------------------------------------------------------------------------------------------------------
+
+// All suported numeric types.
+// clang-format off
+template <typename T>
+struct IsInteger : std::bool_constant<
+    std::is_same_v<T, int8_t> ||
+    std::is_same_v<T, uint8_t> ||
+    std::is_same_v<T, int16_t> ||
+    std::is_same_v<T, uint16_t> ||
+    std::is_same_v<T, int32_t> ||
+    std::is_same_v<T, uint32_t> ||
+    std::is_same_v<T, int64_t> ||
+    std::is_same_v<T, uint64_t> ||
+    std::is_same_v<T, long long> ||
+    std::is_same_v<T, unsigned long long>>
+ {};
+// clang-format on
+//
+// Is type an integer.
+template <typename T> constexpr bool IsIntegerV = IsInteger<T>::value;
+
+// Is type an unsigned integer.
+template <typename T> struct IsUnsignedInteger : std::conjunction<IsInteger<T>, std::negation<std::is_signed<T>>>
+{
+};
+
+template <typename T> constexpr bool IsUnsignedIntegerV = IsUnsignedInteger<T>::value;
+
+// Is type a signed integer.
+template <typename T> struct IsSignedInteger : std::conjunction<IsInteger<T>, std::is_signed<T>>
+{
+};
+
+template <typename T> constexpr bool IsSignedIntegerV = IsSignedInteger<T>::value;
+
+// Is type a custom serializable/deserializable type.
+template <typename T> constexpr bool IsCustomSerializableV = std::is_base_of_v<ISignalRSerializable, T>;
+template <typename T> constexpr bool IsCustomDeserializableV = std::is_base_of_v<ISignalRDeserializable, T>;
+
+// Supported types ------------------------------------------------------------------------------------------------
+
+// All supported basic types our serializer supports.
+// clang-format off
+template <typename T>
+constexpr bool IsSupportedSignalRBasicType = 
+    IsIntegerV<T> || 
+    std::is_same_v<T, double> || 
+    std::is_same_v<T, float> ||
+    std::is_same_v<T, bool> || 
+    std::is_same_v<T, std::string> || 
+    std::is_same_v<T, nullptr_t> ||
+    std::is_base_of_v<ISignalRSerializable, T> ||
+    std::is_base_of_v<ISignalRDeserializable, T>;
+// clang-format on
+
+// Add supported container types.
+template <typename T, typename = void> struct IsSupportedSignalRType : std::false_type
+{
+};
+
+template <typename T> struct IsSupportedSignalRType<T, std::enable_if_t<IsSupportedSignalRBasicType<T>>> : std::true_type
+{
+};
+
+// Optional
+template <typename T> struct IsSupportedSignalRType<std::optional<T>, std::enable_if_t<IsSupportedSignalRType<T>::value>> : std::true_type
+{
+};
+
+// Vector
+template <typename T> struct IsSupportedSignalRType<std::vector<T>, std::enable_if_t<IsSupportedSignalRType<T>::value>> : std::true_type
+{
+};
+
+// Uint map that can support any of our unsigned integers as a key
+template <typename K, typename T>
+struct IsSupportedSignalRType<std::map<K, T>, std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value>> : std::true_type
+{
+};
+
+// String map
+template <typename T> struct IsSupportedSignalRType<std::map<std::string, T>, std::enable_if_t<IsSupportedSignalRType<T>::value>> : std::true_type
+{
+};
+
+}
diff --git a/Tests/src/InternalTests/SignalRSerializerTests.cpp b/Tests/src/InternalTests/SignalRSerializerTests.cpp
new file mode 100644
index 000000000..1f956032e
--- /dev/null
+++ b/Tests/src/InternalTests/SignalRSerializerTests.cpp
@@ -0,0 +1,675 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Multiplayer/SignalRSerializer.h"
+#include "TestHelpers.h"
+
+#include <gtest/gtest.h>
+
+using namespace csp::multiplayer;
+
+// Test we can serialize/deserialize int64 values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeIntTest)
+{
+    const int64_t Value = 2;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    int64_t DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+// Test we can serialize/deserialize uint64 values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUintTest)
+{
+    const uint64_t Value = 1;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    uint64_t DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+/*
+    This is important, as signalr only supports int64 signed integer types,
+    so we need to test our internal conversion logic.
+*/
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeShortTest)
+{
+    const short Value = -2;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    short DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+// Test we throw an exception if the value being deserialized is larger than the input type.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUintInvalidSizeTest)
+{
+    const uint64_t Value = 99999999;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    uint16_t DeserializedValue;
+    EXPECT_THROW(Deserializer.ReadValue(DeserializedValue), std::runtime_error);
+}
+
+// Test we throw an exception if the value being deserialized is larger than the input type.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeIntInvalidSizeTest)
+{
+    const int64_t Value = 99999999;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    int16_t DeserializedValue;
+    EXPECT_THROW(Deserializer.ReadValue(DeserializedValue), std::runtime_error);
+}
+
+// Test we throw an exception if the value being deserialized is larger than the input type.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeIntInvalidSizeMinTest)
+{
+    const int64_t Value = -99999999;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    int16_t DeserializedValue;
+    EXPECT_THROW(Deserializer.ReadValue(DeserializedValue), std::runtime_error);
+}
+
+/*
+    This is important, as signalr only supports uint64 unsigned integer types,
+    so we need to test our internal conversion logic.
+*/
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUshortTest)
+{
+    const unsigned short Value = 2;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    unsigned short DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+// Test we can serialize/deserialize double values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeDoubleTest)
+{
+    const double Value = 3.0;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    double DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+/*
+    This is important, as signalr only supports doubles, and not floats,
+    so we need to test our internal conversion logic.
+*/
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeFloatTest)
+{
+    const float Value = 3.f;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    float DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+// Test we can serialize/deserialize bool values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeBoolTest)
+{
+    const bool Value = false;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    bool DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+// Test we can serialize/deserialize string values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeStringTest)
+{
+    const std::string Value = "Test";
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    std::string DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+// Test we can serialize/deserialize optionals when theyre unset.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUnsetOptionalTest)
+{
+    const std::optional<std::string> Value;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    std::optional<std::string> DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+// Test we can serialize/deserialize optionals when theyre set.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeSetOptionalTest)
+{
+    const std::optional<std::string> Value = "Test";
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    std::optional<std::string> DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeArrayTest)
+{
+    std::vector<float> Value { -0.1f, 1.f, 2.f, 3.f };
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+    std::vector<float> DeserializedValue;
+
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeArrayMultipleTypesTest)
+{
+    const auto Value = std::make_tuple(1ll, 2ull, 3.0, true, std::string { "Test" }, nullptr);
+
+    SignalRSerializer Serializer;
+    Serializer.StartWriteArray();
+    {
+        Serializer.WriteValue(std::get<0>(Value));
+        Serializer.WriteValue(std::get<1>(Value));
+        Serializer.WriteValue(std::get<2>(Value));
+        Serializer.WriteValue(std::get<3>(Value));
+        Serializer.WriteValue(std::get<4>(Value));
+        Serializer.WriteValue(std::get<5>(Value));
+    }
+    Serializer.EndWriteArray();
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+    std::tuple<int64_t, uint64_t, double, bool, std::string, nullptr_t> DeserializedValue;
+
+    size_t ArraySize = 0;
+    Deserializer.StartReadArray(ArraySize);
+    {
+        Deserializer.ReadValue(std::get<0>(DeserializedValue));
+        Deserializer.ReadValue(std::get<1>(DeserializedValue));
+        Deserializer.ReadValue(std::get<2>(DeserializedValue));
+        Deserializer.ReadValue(std::get<3>(DeserializedValue));
+        Deserializer.ReadValue(std::get<4>(DeserializedValue));
+        Deserializer.ReadValue(std::get<5>(DeserializedValue));
+    }
+    Deserializer.EndReadArray();
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeArrayMultipleArraysTest)
+{
+    const auto Value = std::make_tuple(1ll, 2ull, std::make_tuple(5ull, std::string { "Test2" }), 3.0, true, std::string { "Test" }, nullptr);
+
+    SignalRSerializer Serializer;
+    Serializer.StartWriteArray();
+    {
+        Serializer.WriteValue(std::get<0>(Value));
+        Serializer.WriteValue(std::get<1>(Value));
+
+        Serializer.StartWriteArray();
+        {
+            Serializer.WriteValue(std::get<0>(std::get<2>(Value)));
+            Serializer.WriteValue(std::get<1>(std::get<2>(Value)));
+        }
+        Serializer.EndWriteArray();
+
+        Serializer.WriteValue(std::get<3>(Value));
+        Serializer.WriteValue(std::get<4>(Value));
+        Serializer.WriteValue(std::get<5>(Value));
+        Serializer.WriteValue(std::get<6>(Value));
+    }
+    Serializer.EndWriteArray();
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+    std::tuple<int64_t, uint64_t, std::tuple<uint64_t, std::string>, double, bool, std::string, nullptr_t> DeserializedValue;
+
+    size_t ArraySize = 0;
+    Deserializer.StartReadArray(ArraySize);
+    {
+        Deserializer.ReadValue(std::get<0>(DeserializedValue));
+        Deserializer.ReadValue(std::get<1>(DeserializedValue));
+
+        size_t ArraySize2 = 0;
+        Deserializer.StartReadArray(ArraySize2);
+        {
+            Deserializer.ReadValue(std::get<0>(std::get<2>(DeserializedValue)));
+            Deserializer.ReadValue(std::get<1>(std::get<2>(DeserializedValue)));
+        }
+        Deserializer.EndReadArray();
+
+        Deserializer.ReadValue(std::get<3>(DeserializedValue));
+        Deserializer.ReadValue(std::get<4>(DeserializedValue));
+        Deserializer.ReadValue(std::get<5>(DeserializedValue));
+        Deserializer.ReadValue(std::get<6>(DeserializedValue));
+    }
+    Deserializer.EndReadArray();
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUintMapTest)
+{
+    std::map<uint64_t, std::string> Value { { 0, "Test1" }, { 1, "Test2" }, { 2, "Test3" } };
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+    std::map<uint64_t, std::string> DeserializedValue;
+
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUintMapMultipleTypesTest)
+{
+    std::pair<uint64_t, int64_t> Pair1 { 0, 1 };
+    std::pair<uint64_t, uint64_t> Pair2 { 1, 2ull };
+    std::pair<uint64_t, double> Pair3 { 2, 3.0 };
+    std::pair<uint64_t, bool> Pair4 { 3, true };
+    std::pair<uint64_t, std::string> Pair5 { 4, "Test1" };
+    std::pair<uint64_t, nullptr_t> Pair6 { 5, nullptr };
+
+    SignalRSerializer Serializer;
+    Serializer.StartWriteUintMap();
+    {
+        Serializer.WriteKeyValue(Pair1.first, Pair1.second);
+        Serializer.WriteKeyValue(Pair2.first, Pair2.second);
+        Serializer.WriteKeyValue(Pair3.first, Pair3.second);
+        Serializer.WriteKeyValue(Pair4.first, Pair4.second);
+        Serializer.WriteKeyValue(Pair5.first, Pair5.second);
+        Serializer.WriteKeyValue(Pair6.first, Pair6.second);
+    }
+    Serializer.EndWriteUintMap();
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    std::pair<uint64_t, int64_t> DeserializedPair1;
+    std::pair<uint64_t, uint64_t> DeserializedPair2;
+    std::pair<uint64_t, double> DeserializedPair3;
+    std::pair<uint64_t, bool> DeserializedPair4;
+    std::pair<uint64_t, std::string> DeserializedPair5;
+    std::pair<uint64_t, nullptr_t> DeserializedPair6;
+
+    size_t ArraySize = 0;
+    Deserializer.StartReadUintMap(ArraySize);
+    {
+        Deserializer.ReadKeyValue(DeserializedPair1);
+        Deserializer.ReadKeyValue(DeserializedPair2);
+        Deserializer.ReadKeyValue(DeserializedPair3);
+        Deserializer.ReadKeyValue(DeserializedPair4);
+        Deserializer.ReadKeyValue(DeserializedPair5);
+        Deserializer.ReadKeyValue(DeserializedPair6);
+    }
+    Deserializer.EndReadUintMap();
+
+    EXPECT_EQ(Pair1, DeserializedPair1);
+    EXPECT_EQ(Pair2, DeserializedPair2);
+    EXPECT_EQ(Pair3, DeserializedPair3);
+    EXPECT_EQ(Pair4, DeserializedPair4);
+    EXPECT_EQ(Pair5, DeserializedPair5);
+    EXPECT_EQ(Pair6, DeserializedPair6);
+}
+
+/*
+    This is important, as signalr only supports uint64 uint map keys,
+    so we need to test our internal conversion logic.
+*/
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUshortMapTest)
+{
+    std::map<unsigned short, std::string> Value { { 0, "Test1" }, { 1, "Test2" }, { 2, "Test3" } };
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+    std::map<unsigned short, std::string> DeserializedValue;
+
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+/*
+    This is important, as signalr only supports uint64 uint map keys,
+    so we need to test our internal conversion logic.
+*/
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeShortMapMultipleTypes)
+{
+    std::pair<unsigned short, int64_t> Pair1 { 0, 1 };
+    std::pair<unsigned short, uint64_t> Pair2 { 1, 2ull };
+    std::pair<unsigned short, double> Pair3 { 2, 3.0 };
+
+    SignalRSerializer Serializer;
+    Serializer.StartWriteUintMap();
+    {
+        Serializer.WriteKeyValue(Pair1.first, Pair1.second);
+        Serializer.WriteKeyValue(Pair2.first, Pair2.second);
+        Serializer.WriteKeyValue(Pair3.first, Pair3.second);
+    }
+    Serializer.EndWriteUintMap();
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    std::pair<unsigned short, int64_t> DeserializedPair1;
+    std::pair<unsigned short, uint64_t> DeserializedPair2;
+    std::pair<unsigned short, double> DeserializedPair3;
+
+    size_t ArraySize = 0;
+    Deserializer.StartReadUintMap(ArraySize);
+    {
+        Deserializer.ReadKeyValue(DeserializedPair1);
+        Deserializer.ReadKeyValue(DeserializedPair2);
+        Deserializer.ReadKeyValue(DeserializedPair3);
+    }
+    Deserializer.EndReadUintMap();
+
+    EXPECT_EQ(Pair1, DeserializedPair1);
+    EXPECT_EQ(Pair2, DeserializedPair2);
+    EXPECT_EQ(Pair3, DeserializedPair3);
+}
+
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeStringMapTest)
+{
+    std::map<std::string, std::string> Value { { "0", "Test1" }, { "1", "Test2" }, { "2", "Test3" } };
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+    std::map<std::string, std::string> DeserializedValue;
+
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeStringMapMultipleTypes)
+{
+    std::pair<std::string, int64_t> Pair1 { "0", 1 };
+    std::pair<std::string, uint64_t> Pair2 { "1", 2ull };
+    std::pair<std::string, double> Pair3 { "2", 3.0 };
+    std::pair<std::string, bool> Pair4 { "3", true };
+    std::pair<std::string, std::string> Pair5 { "4", "Test1" };
+    std::pair<std::string, nullptr_t> Pair6 { "5", nullptr };
+
+    SignalRSerializer Serializer;
+    Serializer.StartWriteStringMap();
+    {
+        Serializer.WriteKeyValue(Pair1.first, Pair1.second);
+        Serializer.WriteKeyValue(Pair2.first, Pair2.second);
+        Serializer.WriteKeyValue(Pair3.first, Pair3.second);
+        Serializer.WriteKeyValue(Pair4.first, Pair4.second);
+        Serializer.WriteKeyValue(Pair5.first, Pair5.second);
+        Serializer.WriteKeyValue(Pair6.first, Pair6.second);
+    }
+    Serializer.EndWriteStringMap();
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    std::pair<std::string, int64_t> DeserializedPair1;
+    std::pair<std::string, uint64_t> DeserializedPair2;
+    std::pair<std::string, double> DeserializedPair3;
+    std::pair<std::string, bool> DeserializedPair4;
+    std::pair<std::string, std::string> DeserializedPair5;
+    std::pair<std::string, nullptr_t> DeserializedPair6;
+
+    size_t ArraySize = 0;
+    Deserializer.StartReadStringMap(ArraySize);
+    {
+        Deserializer.ReadKeyValue(DeserializedPair1);
+        Deserializer.ReadKeyValue(DeserializedPair2);
+        Deserializer.ReadKeyValue(DeserializedPair3);
+        Deserializer.ReadKeyValue(DeserializedPair4);
+        Deserializer.ReadKeyValue(DeserializedPair5);
+        Deserializer.ReadKeyValue(DeserializedPair6);
+    }
+    Deserializer.EndReadStringMap();
+
+    EXPECT_EQ(Pair1, DeserializedPair1);
+    EXPECT_EQ(Pair2, DeserializedPair2);
+    EXPECT_EQ(Pair3, DeserializedPair3);
+    EXPECT_EQ(Pair4, DeserializedPair4);
+    EXPECT_EQ(Pair5, DeserializedPair5);
+    EXPECT_EQ(Pair6, DeserializedPair6);
+}
+
+struct TestObject1 : public ISignalRSerializable, public ISignalRDeserializable
+{
+    std::vector<std::string> Values;
+
+    void Serialize(SignalRSerializer& Serializer) const override { Serializer.WriteValue(Values); }
+    void Deserialize(SignalRDeserializer& Deserializer) override { Deserializer.ReadValue(Values); }
+
+    bool operator==(const TestObject1& Other) const { return Values == Other.Values; }
+};
+
+struct TestObject2 : public ISignalRSerializable, public ISignalRDeserializable
+{
+    int64_t Int64Member = 0;
+    uint64_t Uint64Member = 0;
+    double DoubleMember = 0;
+    bool BoolMember = false;
+    std::string StringMember;
+    std::optional<TestObject1> OptionalMember;
+    std::vector<TestObject1> ArrayMember;
+    std::map<uint64_t, TestObject1> UintMapMember;
+    std::map<std::string, TestObject1> StringMapMember;
+
+    void Serialize(SignalRSerializer& Serializer) const override
+    {
+        Serializer.StartWriteArray();
+        {
+            Serializer.WriteValue(Int64Member);
+            Serializer.WriteValue(Uint64Member);
+            Serializer.WriteValue(DoubleMember);
+            Serializer.WriteValue(BoolMember);
+            Serializer.WriteValue(StringMember);
+            Serializer.WriteValue(OptionalMember);
+            Serializer.WriteValue(ArrayMember);
+            Serializer.WriteValue(UintMapMember);
+            Serializer.WriteValue(StringMapMember);
+        }
+        Serializer.EndWriteArray();
+    }
+
+    void Deserialize(SignalRDeserializer& Deserializer) override
+    {
+        size_t ArraySize = 0;
+        Deserializer.StartReadArray(ArraySize);
+        {
+            Deserializer.ReadValue(Int64Member);
+            Deserializer.ReadValue(Uint64Member);
+            Deserializer.ReadValue(DoubleMember);
+            Deserializer.ReadValue(BoolMember);
+            Deserializer.ReadValue(StringMember);
+            Deserializer.ReadValue(OptionalMember);
+            Deserializer.ReadValue(ArrayMember);
+            Deserializer.ReadValue(UintMapMember);
+            Deserializer.ReadValue(StringMapMember);
+        }
+        Deserializer.EndReadArray();
+    }
+
+    bool operator==(const TestObject2& Other) const
+    {
+        return (Int64Member == Other.Int64Member && Uint64Member == Other.Uint64Member && DoubleMember == Other.DoubleMember
+            && BoolMember == Other.BoolMember && StringMember == Other.StringMember && OptionalMember == Other.OptionalMember
+            && ArrayMember == Other.ArrayMember && UintMapMember == Other.UintMapMember && StringMapMember == Other.StringMapMember);
+    }
+};
+
+// Todo: split these into more focused tests.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeObjectTest)
+{
+    TestObject1 Child1;
+    Child1.Values.push_back(std::string { "Test1" });
+    Child1.Values.push_back(std::string { "Test2" });
+
+    TestObject1 Child2;
+    Child2.Values.push_back(std::string { "Test3" });
+    Child2.Values.push_back(std::string { "Test4" });
+
+    TestObject1 Child3;
+    Child3.Values.push_back(std::string { "Test5" });
+    Child3.Values.push_back(std::string { "Test6" });
+
+    TestObject1 Child4;
+    Child4.Values.push_back(std::string { "Test7" });
+    Child4.Values.push_back(std::string { "Test8" });
+
+    TestObject1 Child23;
+
+    TestObject2 Value;
+    Value.Int64Member = 1ll;
+    Value.Uint64Member = 2ull;
+    Value.DoubleMember = 3.0;
+    Value.BoolMember = true;
+    Value.StringMember = "Test";
+    Value.OptionalMember = Child1;
+    Value.ArrayMember.push_back(Child2);
+    Value.UintMapMember[0] = Child3;
+    Value.StringMapMember["0"] = Child4;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    TestObject2 DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
\ No newline at end of file


Reviews:
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:16:10Z
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:46:31Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T09:50:52Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T10:19:03Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T10:19:16Z
user: login: MAG-AdamThorn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:24:51Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:32:49Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:33:41Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T15:34:12Z
user: login: MAG-OR
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T08:22:37Z
user: login: MAG-OR
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T07:14:29Z
user: login: MAG-AdamThorn
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T15:03:25Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:33:03Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:48:15Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:48:20Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T10:51:06Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:06:55Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:08:26Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:11:49Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:11:56Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:14:51Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:14:56Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:15:38Z
user: login: MAG-mv
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:51:56Z
user: login: MAG-AdamThorn
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T09:13:26Z
user: login: MAG-AdamThorn
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T09:15:10Z

Review comments:
diff_hunk: @@ -0,0 +1,338 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::PushArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::PopArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::vector<signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::PopStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<std::string, signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::PopUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<uint64_t, signalr::value>>(Current) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    const Container& Root = Stack.top();
+
+    if (std::holds_alternative<signalr::value>(Root))
+    {
+        return std::get<signalr::value>(Root);
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Root) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+
+    return signalr::value {};
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-OR
body: [comment] isn't this unreachable? your build will fail after your rebase on the w4 commit if that is the case
created_at: 2025-05-02T09:16:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,338 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::PushArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::PopArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::vector<signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::PopStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<std::string, signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::PopUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<uint64_t, signalr::value>>(Current) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    const Container& Root = Stack.top();
+
+    if (std::holds_alternative<signalr::value>(Root))
+    {
+        return std::get<signalr::value>(Root);
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Root) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+
+    return signalr::value {};
+}
+
+void SignalRSerializer::Pop(signalr::value&& Last)
+{
+    // We dont check for maps in this function because key-values are handled seperatly
+    if (Stack.size() == 0)
+    {
+        // If the last element has been popped, push back a root value
+        Stack.push(Last);
+        return;
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        std::get<std::vector<signalr::value>>(Stack.top()).push_back(Last);
+    }
+    else if (std::holds_alternative<std::pair<uint64_t, signalr::value>>(Stack.top()))
+    {
+        std::get<std::pair<uint64_t, signalr::value>>(Stack.top()).second = Last;
+    }
+    else if (std::holds_alternative<std::pair<std::string, signalr::value>>(Stack.top()))
+    {
+        std::get<std::pair<std::string, signalr::value>>(Stack.top()).second = Last;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::AppendValueInternal(const SignalRSerializableValue& Value)
+{
+    std::visit([this](auto&& ValType) { AppendValueInternal(ValType); }, Value);
+}
+
+SignalRDeserializer::SignalRDeserializer(const signalr::value& Object)
+    : Root { Object }
+{
+    ObjectStack.push(nullptr);
+}
+
+SignalRDeserializer::SignalRDeserializer(signalr::value&& Object)
+    : Root { std::move(Object) }
+{
+    ObjectStack.push(nullptr);
+}
+
+void SignalRDeserializer::EnterArray(size_t& Size)
+{
+    const signalr::value& ArrayObject = ReadNextValue();
+
+    if (ArrayObject.is_array() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't an array");
+    }
+
+    ObjectStack.push(ArrayObject.as_array().begin());
+
+    Size = ArrayObject.as_array().size();
+}
+
+void SignalRDeserializer::ExitArray()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in an array");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EnterUintMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_uint_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a uint map");
+    }
+
+    ObjectStack.push(MapObject.as_uint_map().begin());
+
+    Size = MapObject.as_uint_map().size();
+}
+void SignalRDeserializer::ExitUintMap()
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EnterStringMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_string_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a string map");
+    }
+
+    ObjectStack.push(MapObject.as_string_map().begin());
+
+    Size = MapObject.as_string_map().size();
+}
+
+void SignalRDeserializer::ExitStringMap()
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    ObjectStack.pop();
+}
+
+const signalr::value& SignalRDeserializer::ReadNextValue()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        return *std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top());
+    }
+    else if (std::holds_alternative<nullptr_t>(ObjectStack.top()))
+    {
+        return Root;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected deserializer state");
+    }
+}
+const std::pair<std::uint64_t, signalr::value> SignalRDeserializer::ReadNextUintKeyValue() const
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    return *std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+const std::pair<std::string, signalr::value> SignalRDeserializer::ReadNextStringKeyValue() const
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    return *std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, int64_t& OutVal) const
+{
+    if (Object.is_integer() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not an integer");
+    }
+
+    OutVal = Object.as_integer();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, uint64_t& OutVal) const
+{
+    if (Object.is_uinteger() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a uinteger");
+    }
+
+    OutVal = Object.as_uinteger();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, double& OutVal) const
+{
+    if (Object.is_double() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a double");
+    }
+
+    OutVal = Object.as_double();
+}
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, bool& OutVal) const
+{
+    if (Object.is_bool() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a bool");
+    }
+
+    OutVal = Object.as_bool();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, std::string& OutVal) const
+{
+    if (Object.is_string() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a string");
+    }
+
+    OutVal = Object.as_string();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, nullptr_t&) const
+{
+    if (Object.is_null() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not null");
+    }
+}
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, SignalRSerializableValue& OutVal) const
+{
+    if (Object.is_integer())
+    {
+        OutVal = Object.as_integer();
+    }
+    else if (Object.is_uinteger())
+    {
+        OutVal = Object.as_uinteger();
+    }
+    else if (Object.is_double())
+    {
+        OutVal = Object.as_double();
+    }
+    else if (Object.is_bool())
+    {
+        OutVal = Object.as_bool();
+    }
+    else if (Object.is_string())
+    {
+        OutVal = Object.as_string();
+    }
+    else if (Object.is_null())
+    {
+        OutVal = nullptr;
+    }
+    else
+    {
+        throw std::runtime_error("IUnexpected value: Value wasn't a supported variant type");
+    }
+}
+
+void SignalRDeserializer::IncrementIterator()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top())++;
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-OR
body: [comment] I can't help but notice how many functions have if else checks that differ only by the type. would there be a way to make a list of supported types and check the type of the variable against them?
```
type = ...
if type is supported && holds_alternative<type>::...top() then
    get<type>...++
else
  throw
```
It would definitely achieve the "make it easy to support new types" goal of this refactoring.
I'm aware that getting the type of a variable isn't the most straight-forward thing in the world in C++(https://stackoverflow.com/questions/81870/is-it-possible-to-print-a-variables-type-in-standard-c/56766138#56766138 may help). just a suggestion. may be a huge can of worms you'd rather not touch with a 10-foot pole. may not make the code clearer. your call.
created_at: 2025-05-02T09:46:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,338 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::PushArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::PopArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::vector<signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::PopStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<std::string, signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::PopUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<uint64_t, signalr::value>>(Current) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    const Container& Root = Stack.top();
+
+    if (std::holds_alternative<signalr::value>(Root))
+    {
+        return std::get<signalr::value>(Root);
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Root) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+
+    return signalr::value {};
+}
+
+void SignalRSerializer::Pop(signalr::value&& Last)
+{
+    // We dont check for maps in this function because key-values are handled seperatly
+    if (Stack.size() == 0)
+    {
+        // If the last element has been popped, push back a root value
+        Stack.push(Last);
+        return;
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        std::get<std::vector<signalr::value>>(Stack.top()).push_back(Last);
+    }
+    else if (std::holds_alternative<std::pair<uint64_t, signalr::value>>(Stack.top()))
+    {
+        std::get<std::pair<uint64_t, signalr::value>>(Stack.top()).second = Last;
+    }
+    else if (std::holds_alternative<std::pair<std::string, signalr::value>>(Stack.top()))
+    {
+        std::get<std::pair<std::string, signalr::value>>(Stack.top()).second = Last;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::AppendValueInternal(const SignalRSerializableValue& Value)
+{
+    std::visit([this](auto&& ValType) { AppendValueInternal(ValType); }, Value);
+}
+
+SignalRDeserializer::SignalRDeserializer(const signalr::value& Object)
+    : Root { Object }
+{
+    ObjectStack.push(nullptr);
+}
+
+SignalRDeserializer::SignalRDeserializer(signalr::value&& Object)
+    : Root { std::move(Object) }
+{
+    ObjectStack.push(nullptr);
+}
+
+void SignalRDeserializer::EnterArray(size_t& Size)
+{
+    const signalr::value& ArrayObject = ReadNextValue();
+
+    if (ArrayObject.is_array() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't an array");
+    }
+
+    ObjectStack.push(ArrayObject.as_array().begin());
+
+    Size = ArrayObject.as_array().size();
+}
+
+void SignalRDeserializer::ExitArray()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in an array");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EnterUintMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_uint_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a uint map");
+    }
+
+    ObjectStack.push(MapObject.as_uint_map().begin());
+
+    Size = MapObject.as_uint_map().size();
+}
+void SignalRDeserializer::ExitUintMap()
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EnterStringMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_string_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a string map");
+    }
+
+    ObjectStack.push(MapObject.as_string_map().begin());
+
+    Size = MapObject.as_string_map().size();
+}
+
+void SignalRDeserializer::ExitStringMap()
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    ObjectStack.pop();
+}
+
+const signalr::value& SignalRDeserializer::ReadNextValue()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        return *std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top());
+    }
+    else if (std::holds_alternative<nullptr_t>(ObjectStack.top()))
+    {
+        return Root;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected deserializer state");
+    }
+}
+const std::pair<std::uint64_t, signalr::value> SignalRDeserializer::ReadNextUintKeyValue() const
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    return *std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+const std::pair<std::string, signalr::value> SignalRDeserializer::ReadNextStringKeyValue() const
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    return *std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, int64_t& OutVal) const
+{
+    if (Object.is_integer() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not an integer");
+    }
+
+    OutVal = Object.as_integer();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, uint64_t& OutVal) const
+{
+    if (Object.is_uinteger() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a uinteger");
+    }
+
+    OutVal = Object.as_uinteger();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, double& OutVal) const
+{
+    if (Object.is_double() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a double");
+    }
+
+    OutVal = Object.as_double();
+}
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, bool& OutVal) const
+{
+    if (Object.is_bool() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a bool");
+    }
+
+    OutVal = Object.as_bool();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, std::string& OutVal) const
+{
+    if (Object.is_string() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a string");
+    }
+
+    OutVal = Object.as_string();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, nullptr_t&) const
+{
+    if (Object.is_null() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not null");
+    }
+}
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, SignalRSerializableValue& OutVal) const
+{
+    if (Object.is_integer())
+    {
+        OutVal = Object.as_integer();
+    }
+    else if (Object.is_uinteger())
+    {
+        OutVal = Object.as_uinteger();
+    }
+    else if (Object.is_double())
+    {
+        OutVal = Object.as_double();
+    }
+    else if (Object.is_bool())
+    {
+        OutVal = Object.as_bool();
+    }
+    else if (Object.is_string())
+    {
+        OutVal = Object.as_string();
+    }
+    else if (Object.is_null())
+    {
+        OutVal = nullptr;
+    }
+    else
+    {
+        throw std::runtime_error("IUnexpected value: Value wasn't a supported variant type");
+    }
+}
+
+void SignalRDeserializer::IncrementIterator()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top())++;
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-mv
body: I will update some of these conditionals to use std::visit as I am using variants which will dispatch to the correct functions instead of using conditionals
created_at: 2025-05-02T09:50:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,338 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::PushArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::PopArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::vector<signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::PopStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<std::string, signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::PopUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<uint64_t, signalr::value>>(Current) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    const Container& Root = Stack.top();
+
+    if (std::holds_alternative<signalr::value>(Root))
+    {
+        return std::get<signalr::value>(Root);
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Root) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+
+    return signalr::value {};
+}
+
+void SignalRSerializer::Pop(signalr::value&& Last)
+{
+    // We dont check for maps in this function because key-values are handled seperatly
+    if (Stack.size() == 0)
+    {
+        // If the last element has been popped, push back a root value
+        Stack.push(Last);
+        return;
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        std::get<std::vector<signalr::value>>(Stack.top()).push_back(Last);
+    }
+    else if (std::holds_alternative<std::pair<uint64_t, signalr::value>>(Stack.top()))
+    {
+        std::get<std::pair<uint64_t, signalr::value>>(Stack.top()).second = Last;
+    }
+    else if (std::holds_alternative<std::pair<std::string, signalr::value>>(Stack.top()))
+    {
+        std::get<std::pair<std::string, signalr::value>>(Stack.top()).second = Last;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::AppendValueInternal(const SignalRSerializableValue& Value)
+{
+    std::visit([this](auto&& ValType) { AppendValueInternal(ValType); }, Value);
+}
+
+SignalRDeserializer::SignalRDeserializer(const signalr::value& Object)
+    : Root { Object }
+{
+    ObjectStack.push(nullptr);
+}
+
+SignalRDeserializer::SignalRDeserializer(signalr::value&& Object)
+    : Root { std::move(Object) }
+{
+    ObjectStack.push(nullptr);
+}
+
+void SignalRDeserializer::EnterArray(size_t& Size)
+{
+    const signalr::value& ArrayObject = ReadNextValue();
+
+    if (ArrayObject.is_array() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't an array");
+    }
+
+    ObjectStack.push(ArrayObject.as_array().begin());
+
+    Size = ArrayObject.as_array().size();
+}
+
+void SignalRDeserializer::ExitArray()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in an array");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EnterUintMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_uint_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a uint map");
+    }
+
+    ObjectStack.push(MapObject.as_uint_map().begin());
+
+    Size = MapObject.as_uint_map().size();
+}
+void SignalRDeserializer::ExitUintMap()
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EnterStringMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_string_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a string map");
+    }
+
+    ObjectStack.push(MapObject.as_string_map().begin());
+
+    Size = MapObject.as_string_map().size();
+}
+
+void SignalRDeserializer::ExitStringMap()
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    ObjectStack.pop();
+}
+
+const signalr::value& SignalRDeserializer::ReadNextValue()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        return *std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top());
+    }
+    else if (std::holds_alternative<nullptr_t>(ObjectStack.top()))
+    {
+        return Root;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected deserializer state");
+    }
+}
+const std::pair<std::uint64_t, signalr::value> SignalRDeserializer::ReadNextUintKeyValue() const
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    return *std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+const std::pair<std::string, signalr::value> SignalRDeserializer::ReadNextStringKeyValue() const
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    return *std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, int64_t& OutVal) const
+{
+    if (Object.is_integer() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not an integer");
+    }
+
+    OutVal = Object.as_integer();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, uint64_t& OutVal) const
+{
+    if (Object.is_uinteger() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a uinteger");
+    }
+
+    OutVal = Object.as_uinteger();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, double& OutVal) const
+{
+    if (Object.is_double() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a double");
+    }
+
+    OutVal = Object.as_double();
+}
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, bool& OutVal) const
+{
+    if (Object.is_bool() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a bool");
+    }
+
+    OutVal = Object.as_bool();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, std::string& OutVal) const
+{
+    if (Object.is_string() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a string");
+    }
+
+    OutVal = Object.as_string();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, nullptr_t&) const
+{
+    if (Object.is_null() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not null");
+    }
+}
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, SignalRSerializableValue& OutVal) const
+{
+    if (Object.is_integer())
+    {
+        OutVal = Object.as_integer();
+    }
+    else if (Object.is_uinteger())
+    {
+        OutVal = Object.as_uinteger();
+    }
+    else if (Object.is_double())
+    {
+        OutVal = Object.as_double();
+    }
+    else if (Object.is_bool())
+    {
+        OutVal = Object.as_bool();
+    }
+    else if (Object.is_string())
+    {
+        OutVal = Object.as_string();
+    }
+    else if (Object.is_null())
+    {
+        OutVal = nullptr;
+    }
+    else
+    {
+        throw std::runtime_error("IUnexpected value: Value wasn't a supported variant type");
+    }
+}
+
+void SignalRDeserializer::IncrementIterator()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top())++;
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-mv
body: Also, when it comes to supporting more types, I was referring to CSP types. This is a wrapper around signalr and doesnt include any CSP types, so this will only change if signalr does
created_at: 2025-05-02T10:19:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,338 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::PushArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::PopArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::vector<signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::PopStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<std::string, signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::PopUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<uint64_t, signalr::value>>(Current) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    const Container& Root = Stack.top();
+
+    if (std::holds_alternative<signalr::value>(Root))
+    {
+        return std::get<signalr::value>(Root);
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Root) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+
+    return signalr::value {};
+}
+
+void SignalRSerializer::Pop(signalr::value&& Last)
+{
+    // We dont check for maps in this function because key-values are handled seperatly
+    if (Stack.size() == 0)
+    {
+        // If the last element has been popped, push back a root value
+        Stack.push(Last);
+        return;
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        std::get<std::vector<signalr::value>>(Stack.top()).push_back(Last);
+    }
+    else if (std::holds_alternative<std::pair<uint64_t, signalr::value>>(Stack.top()))
+    {
+        std::get<std::pair<uint64_t, signalr::value>>(Stack.top()).second = Last;
+    }
+    else if (std::holds_alternative<std::pair<std::string, signalr::value>>(Stack.top()))
+    {
+        std::get<std::pair<std::string, signalr::value>>(Stack.top()).second = Last;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::AppendValueInternal(const SignalRSerializableValue& Value)
+{
+    std::visit([this](auto&& ValType) { AppendValueInternal(ValType); }, Value);
+}
+
+SignalRDeserializer::SignalRDeserializer(const signalr::value& Object)
+    : Root { Object }
+{
+    ObjectStack.push(nullptr);
+}
+
+SignalRDeserializer::SignalRDeserializer(signalr::value&& Object)
+    : Root { std::move(Object) }
+{
+    ObjectStack.push(nullptr);
+}
+
+void SignalRDeserializer::EnterArray(size_t& Size)
+{
+    const signalr::value& ArrayObject = ReadNextValue();
+
+    if (ArrayObject.is_array() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't an array");
+    }
+
+    ObjectStack.push(ArrayObject.as_array().begin());
+
+    Size = ArrayObject.as_array().size();
+}
+
+void SignalRDeserializer::ExitArray()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in an array");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EnterUintMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_uint_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a uint map");
+    }
+
+    ObjectStack.push(MapObject.as_uint_map().begin());
+
+    Size = MapObject.as_uint_map().size();
+}
+void SignalRDeserializer::ExitUintMap()
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EnterStringMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_string_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a string map");
+    }
+
+    ObjectStack.push(MapObject.as_string_map().begin());
+
+    Size = MapObject.as_string_map().size();
+}
+
+void SignalRDeserializer::ExitStringMap()
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    ObjectStack.pop();
+}
+
+const signalr::value& SignalRDeserializer::ReadNextValue()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        return *std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top());
+    }
+    else if (std::holds_alternative<nullptr_t>(ObjectStack.top()))
+    {
+        return Root;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected deserializer state");
+    }
+}
+const std::pair<std::uint64_t, signalr::value> SignalRDeserializer::ReadNextUintKeyValue() const
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    return *std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+const std::pair<std::string, signalr::value> SignalRDeserializer::ReadNextStringKeyValue() const
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    return *std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, int64_t& OutVal) const
+{
+    if (Object.is_integer() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not an integer");
+    }
+
+    OutVal = Object.as_integer();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, uint64_t& OutVal) const
+{
+    if (Object.is_uinteger() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a uinteger");
+    }
+
+    OutVal = Object.as_uinteger();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, double& OutVal) const
+{
+    if (Object.is_double() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a double");
+    }
+
+    OutVal = Object.as_double();
+}
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, bool& OutVal) const
+{
+    if (Object.is_bool() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a bool");
+    }
+
+    OutVal = Object.as_bool();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, std::string& OutVal) const
+{
+    if (Object.is_string() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a string");
+    }
+
+    OutVal = Object.as_string();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, nullptr_t&) const
+{
+    if (Object.is_null() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not null");
+    }
+}
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, SignalRSerializableValue& OutVal) const
+{
+    if (Object.is_integer())
+    {
+        OutVal = Object.as_integer();
+    }
+    else if (Object.is_uinteger())
+    {
+        OutVal = Object.as_uinteger();
+    }
+    else if (Object.is_double())
+    {
+        OutVal = Object.as_double();
+    }
+    else if (Object.is_bool())
+    {
+        OutVal = Object.as_bool();
+    }
+    else if (Object.is_string())
+    {
+        OutVal = Object.as_string();
+    }
+    else if (Object.is_null())
+    {
+        OutVal = nullptr;
+    }
+    else
+    {
+        throw std::runtime_error("IUnexpected value: Value wasn't a supported variant type");
+    }
+}
+
+void SignalRDeserializer::IncrementIterator()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top())++;
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-mv
body: I will still try and address these conditionals, though
created_at: 2025-05-02T10:19:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,362 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <signalrclient/signalr_value.h>
+
+#include <map>
+#include <memory>
+#include <optional>
+#include <stack>
+#include <stdexcept>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace csp::multiplayer
+{
+/// @brief A variant reresenting all possible basic types serializable to a signalr value.
+struct SignalRSerializableValue : std::variant<int64_t, uint64_t, double, bool, std::string, nullptr_t>
+{
+    using variant::variant;
+};
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Pushes an array onto the stack.
+    /// @details Once this function has been called, AppendValue should be used to add elements to the array.
+    /// PopArray should be used to finalize the array.
+    void PushArray();
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-AdamThorn
body: [Consider] As I was reading this API I was trying to create a mental model of what is going on. At first, reading these push / pop method descriptions, it sounded like the serializer had a stack we were pushing an array onto, appending values to the array on the stack and then popping the completed array. This didn't sound right to me so I read the functional tests and I think that PushArray and PopArray should be renamed to something like 'StartArraySerialisation' and 'StopArraySerialisation'.
created_at: 2025-05-02T11:27:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,338 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::PushArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::PopArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::vector<signalr::value>>(Current) });
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-AdamThorn
body: [Question] I am finding this hard to reason about. We are calling Stack.pop and then our own Pop method which actually seems to push a value onto the top stack element. I maybe wrong, but it is hard to read. I personally would benefit from a walkthrough, is that possible?
created_at: 2025-05-02T13:24:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,362 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <signalrclient/signalr_value.h>
+
+#include <map>
+#include <memory>
+#include <optional>
+#include <stack>
+#include <stdexcept>
+#include <string>
+#include <variant>
+#include <vector>
+
+namespace csp::multiplayer
+{
+/// @brief A variant reresenting all possible basic types serializable to a signalr value.
+struct SignalRSerializableValue : std::variant<int64_t, uint64_t, double, bool, std::string, nullptr_t>
+{
+    using variant::variant;
+};
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Pushes an array onto the stack.
+    /// @details Once this function has been called, AppendValue should be used to add elements to the array.
+    /// PopArray should be used to finalize the array.
+    void PushArray();
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/f1360d0c1167bd0020e98a193b5c246f90ab461d
created_at: 2025-05-02T15:32:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,338 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::PushArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::PopArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::vector<signalr::value>>(Current) });
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/544220ca5b0491eda4d1888102a95a3a6443af3b
created_at: 2025-05-02T15:33:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,338 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::PushArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::PopArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::vector<signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::PopStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<std::string, signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::PopUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<uint64_t, signalr::value>>(Current) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    const Container& Root = Stack.top();
+
+    if (std::holds_alternative<signalr::value>(Root))
+    {
+        return std::get<signalr::value>(Root);
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Root) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+
+    return signalr::value {};
+}
+
+void SignalRSerializer::Pop(signalr::value&& Last)
+{
+    // We dont check for maps in this function because key-values are handled seperatly
+    if (Stack.size() == 0)
+    {
+        // If the last element has been popped, push back a root value
+        Stack.push(Last);
+        return;
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        std::get<std::vector<signalr::value>>(Stack.top()).push_back(Last);
+    }
+    else if (std::holds_alternative<std::pair<uint64_t, signalr::value>>(Stack.top()))
+    {
+        std::get<std::pair<uint64_t, signalr::value>>(Stack.top()).second = Last;
+    }
+    else if (std::holds_alternative<std::pair<std::string, signalr::value>>(Stack.top()))
+    {
+        std::get<std::pair<std::string, signalr::value>>(Stack.top()).second = Last;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::AppendValueInternal(const SignalRSerializableValue& Value)
+{
+    std::visit([this](auto&& ValType) { AppendValueInternal(ValType); }, Value);
+}
+
+SignalRDeserializer::SignalRDeserializer(const signalr::value& Object)
+    : Root { Object }
+{
+    ObjectStack.push(nullptr);
+}
+
+SignalRDeserializer::SignalRDeserializer(signalr::value&& Object)
+    : Root { std::move(Object) }
+{
+    ObjectStack.push(nullptr);
+}
+
+void SignalRDeserializer::EnterArray(size_t& Size)
+{
+    const signalr::value& ArrayObject = ReadNextValue();
+
+    if (ArrayObject.is_array() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't an array");
+    }
+
+    ObjectStack.push(ArrayObject.as_array().begin());
+
+    Size = ArrayObject.as_array().size();
+}
+
+void SignalRDeserializer::ExitArray()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in an array");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EnterUintMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_uint_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a uint map");
+    }
+
+    ObjectStack.push(MapObject.as_uint_map().begin());
+
+    Size = MapObject.as_uint_map().size();
+}
+void SignalRDeserializer::ExitUintMap()
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    ObjectStack.pop();
+}
+
+void SignalRDeserializer::EnterStringMap(size_t& Size)
+{
+    const signalr::value& MapObject = ReadNextValue();
+
+    if (MapObject.is_string_map() == false)
+    {
+        throw std::runtime_error("Unexpected value: Value isn't a string map");
+    }
+
+    ObjectStack.push(MapObject.as_string_map().begin());
+
+    Size = MapObject.as_string_map().size();
+}
+
+void SignalRDeserializer::ExitStringMap()
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    ObjectStack.pop();
+}
+
+const signalr::value& SignalRDeserializer::ReadNextValue()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        return *std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top());
+    }
+    else if (std::holds_alternative<nullptr_t>(ObjectStack.top()))
+    {
+        return Root;
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected deserializer state");
+    }
+}
+const std::pair<std::uint64_t, signalr::value> SignalRDeserializer::ReadNextUintKeyValue() const
+{
+    if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a uint map");
+    }
+
+    return *std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+const std::pair<std::string, signalr::value> SignalRDeserializer::ReadNextStringKeyValue() const
+{
+    if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Deserializer was not in a string map");
+    }
+
+    return *std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top());
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, int64_t& OutVal) const
+{
+    if (Object.is_integer() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not an integer");
+    }
+
+    OutVal = Object.as_integer();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, uint64_t& OutVal) const
+{
+    if (Object.is_uinteger() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a uinteger");
+    }
+
+    OutVal = Object.as_uinteger();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, double& OutVal) const
+{
+    if (Object.is_double() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a double");
+    }
+
+    OutVal = Object.as_double();
+}
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, bool& OutVal) const
+{
+    if (Object.is_bool() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a bool");
+    }
+
+    OutVal = Object.as_bool();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, std::string& OutVal) const
+{
+    if (Object.is_string() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not a string");
+    }
+
+    OutVal = Object.as_string();
+}
+
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, nullptr_t&) const
+{
+    if (Object.is_null() == false)
+    {
+        throw std::runtime_error("Invalid call: Value was not null");
+    }
+}
+void SignalRDeserializer::ReadValueFromObjectInternal(const signalr::value& Object, SignalRSerializableValue& OutVal) const
+{
+    if (Object.is_integer())
+    {
+        OutVal = Object.as_integer();
+    }
+    else if (Object.is_uinteger())
+    {
+        OutVal = Object.as_uinteger();
+    }
+    else if (Object.is_double())
+    {
+        OutVal = Object.as_double();
+    }
+    else if (Object.is_bool())
+    {
+        OutVal = Object.as_bool();
+    }
+    else if (Object.is_string())
+    {
+        OutVal = Object.as_string();
+    }
+    else if (Object.is_null())
+    {
+        OutVal = nullptr;
+    }
+    else
+    {
+        throw std::runtime_error("IUnexpected value: Value wasn't a supported variant type");
+    }
+}
+
+void SignalRDeserializer::IncrementIterator()
+{
+    if (std::holds_alternative<std::vector<signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::vector<signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<uint64_t, signalr::value>::const_iterator>(ObjectStack.top())++;
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top()))
+    {
+        std::get<std::map<std::string, signalr::value>::const_iterator>(ObjectStack.top())++;
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/b997c7685af4ddb467b07bc8364a164ae5aa635a 
created_at: 2025-05-02T15:34:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,338 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::PushArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::PopArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::vector<signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::PopStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<std::string, signalr::value>>(Current) });
+}
+
+void SignalRSerializer::PushUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::PopUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto Current = std::move(Stack.top());
+    Stack.pop();
+    Pop(signalr::value { std::get<std::map<uint64_t, signalr::value>>(Current) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    const Container& Root = Stack.top();
+
+    if (std::holds_alternative<signalr::value>(Root))
+    {
+        return std::get<signalr::value>(Root);
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Root) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Root))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Root) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+
+    return signalr::value {};
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-OR
body: yes it's failing
created_at: 2025-05-06T08:22:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-AdamThorn
body: [Question] Which write functions are you referring to, WriteValue? If so that function has a comment which seems to be directing the user to use this function - seems circular.

/// @pre This function should be used if this serializer represents a single value,
/// or if StartArray is called first to write to the array.
created_at: 2025-05-07T08:36:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-AdamThorn
body: [Question] Same comment as above. I have not read the cpp at this point, so as someone consuming this api for the first time, the comment below could also do with further info.
created_at: 2025-05-07T08:38:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopStringMap should be used to finalize the map.
+    void StartWriteStringMap();
+
+    /// @brief Ends the current string map in the serializer.
+    /// @pre StartWriteStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteStringMap();
+
+    /// @brief Starts writing a uint map (std::map<uint64_t, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-AdamThorn
body: Same as above
created_at: 2025-05-07T08:39:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-AdamThorn
body: [Consider] I think you should expand the comment here to make it clearer to people how this serializer actually works - what does stack based mean in this context?
created_at: 2025-05-07T08:41:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopStringMap should be used to finalize the map.
+    void StartWriteStringMap();
+
+    /// @brief Ends the current string map in the serializer.
+    /// @pre StartWriteStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteStringMap();
+
+    /// @brief Starts writing a uint map (std::map<uint64_t, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopUintMap should be used to finalize the map.
+    void StartWriteUintMap();
+
+    /// @brief Ends the current uint map in the serializer.
+    /// @pre StartWriteUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteUintMap();
+
+    /// @brief Gets the serialized singnal r value.
+    /// @return signalr::value
+    /// @pre The serializer should be at the root (array and maps should all be popped).
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-AdamThorn
body: See comment at top abut expanding on definition of what a stack based serializer is. I think it would render this precondition more meaningful.
created_at: 2025-05-07T08:42:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopStringMap should be used to finalize the map.
+    void StartWriteStringMap();
+
+    /// @brief Ends the current string map in the serializer.
+    /// @pre StartWriteStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteStringMap();
+
+    /// @brief Starts writing a uint map (std::map<uint64_t, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopUintMap should be used to finalize the map.
+    void StartWriteUintMap();
+
+    /// @brief Ends the current uint map in the serializer.
+    /// @pre StartWriteUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteUintMap();
+
+    /// @brief Gets the serialized singnal r value.
+    /// @return signalr::value
+    /// @pre The serializer should be at the root (array and maps should all be popped).
+    signalr::value Get() const;
+
+private:
+    // Adds the serialized container object to the previous object, or the root.
+    // This will internally call one of the FinalizeContainerSerializaitonInternal overrides.
+    void FinalizeContainerSerializaiton(signalr::value&& SerializedContainer);
+
+    // Case where we fallback to an exception is we are trying to serialize to an unsupported container.
+    template <typename T> void FinalizeContainerSerializaitonInternal(T&, signalr::value&& SerializedContainer);
+    // Case for serializing value into a vector.
+    void FinalizeContainerSerializaitonInternal(std::vector<signalr::value>& Container, signalr::value&& SerializedContainer);
+    // Case for serializing value into a pair for a map.
+    template <typename K> void FinalizeContainerSerializaitonInternal(std::pair<K, signalr::value>& Pair, signalr::value&& SerializedContainer);
+
+    // Writes all of our primitive types to a signalr value.
+    template <typename T> void WriteValueInternal(const T& Value);
+
+    // Case for writing container types to a signalr value.
+    // These cases are recursive.
+    template <typename T> void WriteValueInternal(const std::optional<T>& Value);
+    template <typename T> void WriteValueInternal(const std::vector<T>& Value);
+    template <typename K, typename T> void WriteValueInternal(const std::map<K, T>& Value);
+    template <typename T> void WriteValueInternal(const std::map<std::string, T>& Value);
+
+    // Converts primitive value to a signalr object.
+    template <typename T> signalr::value CreateSignalRObject(const T& Value);
+
+    using Container = std::variant<signalr::value, std::vector<signalr::value>, std::map<std::string, signalr::value>,
+        std::map<uint64_t, signalr::value>, std::pair<uint64_t, signalr::value>, std::pair<std::string, signalr::value>>;
+
+    std::stack<Container> Stack;
+};
+
+/// @brief A stack-based signalr deserializer which allows for custom class deserialization using ISignalRDeserializable
+class SignalRDeserializer
+{
+public:
+    /// @brief Constructor used to copy object to deserialize.
+    /// @param Object const signalr::value& : The value to deserialize
+    /// This should match the structure generated by
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-AdamThorn
body: [fix] Missing the end of this comment... "generated by..."

Same with one below.
created_at: 2025-05-07T08:44:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopStringMap should be used to finalize the map.
+    void StartWriteStringMap();
+
+    /// @brief Ends the current string map in the serializer.
+    /// @pre StartWriteStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteStringMap();
+
+    /// @brief Starts writing a uint map (std::map<uint64_t, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopUintMap should be used to finalize the map.
+    void StartWriteUintMap();
+
+    /// @brief Ends the current uint map in the serializer.
+    /// @pre StartWriteUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteUintMap();
+
+    /// @brief Gets the serialized singnal r value.
+    /// @return signalr::value
+    /// @pre The serializer should be at the root (array and maps should all be popped).
+    signalr::value Get() const;
+
+private:
+    // Adds the serialized container object to the previous object, or the root.
+    // This will internally call one of the FinalizeContainerSerializaitonInternal overrides.
+    void FinalizeContainerSerializaiton(signalr::value&& SerializedContainer);
+
+    // Case where we fallback to an exception is we are trying to serialize to an unsupported container.
+    template <typename T> void FinalizeContainerSerializaitonInternal(T&, signalr::value&& SerializedContainer);
+    // Case for serializing value into a vector.
+    void FinalizeContainerSerializaitonInternal(std::vector<signalr::value>& Container, signalr::value&& SerializedContainer);
+    // Case for serializing value into a pair for a map.
+    template <typename K> void FinalizeContainerSerializaitonInternal(std::pair<K, signalr::value>& Pair, signalr::value&& SerializedContainer);
+
+    // Writes all of our primitive types to a signalr value.
+    template <typename T> void WriteValueInternal(const T& Value);
+
+    // Case for writing container types to a signalr value.
+    // These cases are recursive.
+    template <typename T> void WriteValueInternal(const std::optional<T>& Value);
+    template <typename T> void WriteValueInternal(const std::vector<T>& Value);
+    template <typename K, typename T> void WriteValueInternal(const std::map<K, T>& Value);
+    template <typename T> void WriteValueInternal(const std::map<std::string, T>& Value);
+
+    // Converts primitive value to a signalr object.
+    template <typename T> signalr::value CreateSignalRObject(const T& Value);
+
+    using Container = std::variant<signalr::value, std::vector<signalr::value>, std::map<std::string, signalr::value>,
+        std::map<uint64_t, signalr::value>, std::pair<uint64_t, signalr::value>, std::pair<std::string, signalr::value>>;
+
+    std::stack<Container> Stack;
+};
+
+/// @brief A stack-based signalr deserializer which allows for custom class deserialization using ISignalRDeserializable
+class SignalRDeserializer
+{
+public:
+    /// @brief Constructor used to copy object to deserialize.
+    /// @param Object const signalr::value& : The value to deserialize
+    /// This should match the structure generated by
+    SignalRDeserializer(const signalr::value& Object);
+
+    /// @brief Constructor used to move object to deserialize.
+    /// @param Object signalr::value&& : The value to deserialize
+    /// This should match the structure generated by
+    SignalRDeserializer(signalr::value&& Object);
+
+    /// @brief Reads a value from the internal signalr array.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartReadArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> ReadValue(T& OutVal);
+
+    /// @brief Reads a uint key-value pair to the current uint map.
+    /// @pre EnterUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> ReadKeyValue(std::pair<K, T>& OutVal);
+
+    /// @brief Reads a string key-value pair to the current string map.
+    /// @pre EnterStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> ReadKeyValue(std::pair<std::string, T>& OutVal);
+
+    /// @brief Enters the internal signalr value as an array
+    /// @details Once this function has been called, ReadValue should be used to read elements from the array.
+    /// @param Size size_t& Output parameter specifying the size of the array.
+    /// ExitArray should be used to exit the array once elements have been read.
+    void StartReadArray(size_t& Size);
+
+    /// @brief Exits the internal signalr array.
+    /// @pre StartReadArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndReadArray();
+
+    /// @brief Enters the internal signalr value as an uint map
+    /// @details Once this function has been called, ReadKeyValue should be used to read elements from the map.
+    /// @param Size size_t& Output parameter specifying the size of the map.
+    /// ExitUintMap should be used to exit the map once elements have been read.
+    void StartReadUintMap(size_t& Size);
+
+    /// @brief Exits the internal signalr uint map.
+    /// @pre StartReadUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndReadUintMap();
+
+    /// @brief Enters the internal signalr value as an string map
+    /// @details Once this function has been called, ReadKeyValue should be used to read elements from the map.
+    /// @param Size size_t& Output parameter specifying the size of the map.
+    /// ExitStringMap should be used to exit the map once elements have been read.
+    void StartReadStringMap(size_t& Size);
+
+    /// @brief Exits the internal signalr string map.
+    /// @pre StartReadStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndReadStringMap();
+
+private:
+    // Reads the next signalr value using the current iterator.
+    const signalr::value& ReadNextValue();
+
+    const std::pair<std::uint64_t, signalr::value> ReadNextUintKeyValue() const;
+    const std::pair<std::string, signalr::value> ReadNextStringKeyValue() const;
+
+    // Reads the specified type from the given signalr object.
+    // This internally calls the ReadValueFromObjectInternal for the given type.
+    template <typename T> void ReadValueFromObject(const signalr::value& Object, T& OutVal);
+
+    // Case for reading signed integers.
+    template <typename T> std::enable_if_t<IsSignedIntegerV<T>> ReadValueFromObjectInternal(const signalr::value& Object, T& OutVal);
+    // Case for reading unsigned integers.
+    template <typename T> std::enable_if_t<IsUnsignedIntegerV<T>> ReadValueFromObjectInternal(const signalr::value& Object, T& OutVal);
+    // Case for other primitive types.
+    void ReadValueFromObjectInternal(const signalr::value& Object, double& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, float& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, bool& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, std::string& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, nullptr_t);
+
+    // Case for other container values.
+    template <typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::optional<T>& OutVal);
+    template <typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::vector<T>& OutVal);
+    template <typename K, typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::map<K, T>& OutVal);
+    template <typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::map<std::string, T>& OutVal);
+
+    void IncrementIterator();
+
+    using Iterator = std::variant<nullptr_t, std::vector<signalr::value>::const_iterator, std::map<uint64_t, signalr::value>::const_iterator,
+        std::map<std::string, signalr::value>::const_iterator>;
+
+    signalr::value Root;
+    std::stack<Iterator> ObjectStack;
+};
+
+/// @brief A serializer interface to allow classes to be serialized.
+class ISignalRSerializable
+{
+public:
+    /// @brief Function internally called when serializing this class using SignalRSerializer.
+    virtual void Serialize(SignalRSerializer&) const = 0;
+
+    virtual ~ISignalRSerializable() = default;
+
+protected:
+    ISignalRSerializable() = default;
+
+    ISignalRSerializable(const ISignalRSerializable&) = default;
+    ISignalRSerializable(ISignalRSerializable&&) = default;
+
+    ISignalRSerializable& operator=(const ISignalRSerializable&) = default;
+    ISignalRSerializable& operator=(ISignalRSerializable&&) = default;
+};
+
+/// @brief A deserializer interace to allow classes to be deserialized.
+class ISignalRDeserializable
+{
+public:
+    /// @brief Function internally called when serializing this class using SignalRDeserializer.
+    virtual void Deserialize(SignalRDeserializer&) = 0;
+
+    virtual ~ISignalRDeserializable() = default;
+
+protected:
+    ISignalRDeserializable() = default;
+
+    ISignalRDeserializable(const ISignalRDeserializable&) = default;
+    ISignalRDeserializable(ISignalRDeserializable&&) = default;
+
+    ISignalRDeserializable& operator=(const ISignalRDeserializable&) = default;
+    ISignalRDeserializable& operator=(ISignalRDeserializable&&) = default;
+};
+
+template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> SignalRSerializer::WriteValue(const T& Value) { WriteValueInternal(Value); }
+
+template <typename K, typename T>
+std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> SignalRSerializer::WriteKeyValue(K Key, const T& Value)
+{
+    if (Stack.size() == 0 || std ::holds_alternative<std::map<uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    Stack.push(std::pair<uint64_t, signalr::value> {});
+    std::get<std::pair<uint64_t, signalr::value>>(Stack.top()).first = static_cast<uint64_t>(Key);
+
+    WriteValue(Value);
+
+    // Get our pair form the top of the stack and pop
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-AdamThorn
body: [fix] type: form > from
created_at: 2025-05-07T08:49:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,283 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::StartWriteArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::EndWriteArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto ArrayObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::vector<signalr::value>>(ArrayObject) });
+}
+
+void SignalRSerializer::StartWriteStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto StringMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<std::string, signalr::value>>(StringMapObject) });
+}
+
+void SignalRSerializer::StartWriteUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto UintMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<uint64_t, signalr::value>>(UintMapObject) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    if (std::holds_alternative<signalr::value>(Stack.top()))
+    {
+        return std::get<signalr::value>(Stack.top());
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Stack.top()) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::FinalizeContainerSerializaiton(signalr::value&& SerializedContainer)
+{
+    // We dont check for maps in this function because key-values are handled seperatly
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-AdamThorn
body: [fix} spelling seperatly > separately
created_at: 2025-05-07T14:41:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,283 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::StartWriteArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::EndWriteArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto ArrayObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::vector<signalr::value>>(ArrayObject) });
+}
+
+void SignalRSerializer::StartWriteStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto StringMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<std::string, signalr::value>>(StringMapObject) });
+}
+
+void SignalRSerializer::StartWriteUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto UintMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<uint64_t, signalr::value>>(UintMapObject) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    if (std::holds_alternative<signalr::value>(Stack.top()))
+    {
+        return std::get<signalr::value>(Stack.top());
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Stack.top()) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::FinalizeContainerSerializaiton(signalr::value&& SerializedContainer)
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-AdamThorn
body: [fix] function spelling FinalizeContainerSerializaiton > FinalizeContainerSerialization
created_at: 2025-05-07T14:42:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,283 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::StartWriteArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::EndWriteArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto ArrayObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::vector<signalr::value>>(ArrayObject) });
+}
+
+void SignalRSerializer::StartWriteStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto StringMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<std::string, signalr::value>>(StringMapObject) });
+}
+
+void SignalRSerializer::StartWriteUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto UintMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<uint64_t, signalr::value>>(UintMapObject) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    if (std::holds_alternative<signalr::value>(Stack.top()))
+    {
+        return std::get<signalr::value>(Stack.top());
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Stack.top()) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::FinalizeContainerSerializaiton(signalr::value&& SerializedContainer)
+{
+    // We dont check for maps in this function because key-values are handled seperatly
+    if (Stack.size() == 0)
+    {
+        // If the last element has been popped, push back a root value
+        Stack.push(SerializedContainer);
+        return;
+    }
+
+    // Dispatch internal variant type to the correct FinalizeContainerSerializaitonInternal call
+    std::visit([this, &SerializedContainer](auto& ValType) { this->FinalizeContainerSerializaitonInternal(ValType, std::move(SerializedContainer)); },
+        Stack.top());
+}
+
+void SignalRSerializer::FinalizeContainerSerializaitonInternal(std::vector<signalr::value>& Container, signalr::value&& SerializedContainer)
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-AdamThorn
body: [fix] spelling FinalizeContainerSerializaitonInternal > FinalizeContainerSerializationInternal
created_at: 2025-05-07T14:43:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,574 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Multiplayer/SignalRSerializer.h"
+#include "TestHelpers.h"
+
+#include <gtest/gtest.h>
+
+using namespace csp::multiplayer;
+
+// Test we can serialize/deserialize int64 values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeIntTest)
path: Tests/src/InternalTests/SignalRSerializerTests.cpp
user: login: MAG-AdamThorn
body: [Consider] Adding a test that actually adds multiple arrays, maps or both to the stack. It looks like all of the current tests just push and pop one thing
created_at: 2025-05-07T14:52:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,574 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Multiplayer/SignalRSerializer.h"
+#include "TestHelpers.h"
+
+#include <gtest/gtest.h>
+
+using namespace csp::multiplayer;
+
+// Test we can serialize/deserialize int64 values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeIntTest)
+{
+    const int64_t Value = 2;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    int64_t DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+// Test we can serialize/deserialize uint64 values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUintTest)
+{
+    const uint64_t Value = 1;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    uint64_t DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+/*
+    This is important, as signalr only supports int64 signed integer types,
+    so we need to test our internal conversion logic.
+*/
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeShortTest)
+{
+    const short Value = -2;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    short DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+/*
+    This is important, as signalr only supports uint64 unsigned integer types,
+    so we need to test our internal conversion logic.
+*/
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUshortTest)
+{
+    const unsigned short Value = 2;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    unsigned short DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
path: Tests/src/InternalTests/SignalRSerializerTests.cpp
user: login: MAG-AdamThorn
body: [Question] What happens if someone tries to deserailize a number that is outside the bounds of a short into a short? Ie they serialize a int64 but deserialize it as a short?
created_at: 2025-05-07T14:55:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-mv
body: Ive removed these comments as they were not really accurate: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/a244675422b322e6a951945b93ca80cff5c602a6
created_at: 2025-05-08T10:33:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/commit/a244675422b322e6a951945b93ca80cff5c602a6
created_at: 2025-05-08T10:48:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopStringMap should be used to finalize the map.
+    void StartWriteStringMap();
+
+    /// @brief Ends the current string map in the serializer.
+    /// @pre StartWriteStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteStringMap();
+
+    /// @brief Starts writing a uint map (std::map<uint64_t, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/commit/a244675422b322e6a951945b93ca80cff5c602a6
created_at: 2025-05-08T10:48:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/8a62b660f4c66fbf9421b27c7750be4f0d7a1f8d
created_at: 2025-05-08T10:51:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopStringMap should be used to finalize the map.
+    void StartWriteStringMap();
+
+    /// @brief Ends the current string map in the serializer.
+    /// @pre StartWriteStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteStringMap();
+
+    /// @brief Starts writing a uint map (std::map<uint64_t, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopUintMap should be used to finalize the map.
+    void StartWriteUintMap();
+
+    /// @brief Ends the current uint map in the serializer.
+    /// @pre StartWriteUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteUintMap();
+
+    /// @brief Gets the serialized singnal r value.
+    /// @return signalr::value
+    /// @pre The serializer should be at the root (array and maps should all be popped).
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/9d0fcdc282805d847c3028402823311e0a203f73
created_at: 2025-05-08T11:06:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopStringMap should be used to finalize the map.
+    void StartWriteStringMap();
+
+    /// @brief Ends the current string map in the serializer.
+    /// @pre StartWriteStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteStringMap();
+
+    /// @brief Starts writing a uint map (std::map<uint64_t, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopUintMap should be used to finalize the map.
+    void StartWriteUintMap();
+
+    /// @brief Ends the current uint map in the serializer.
+    /// @pre StartWriteUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteUintMap();
+
+    /// @brief Gets the serialized singnal r value.
+    /// @return signalr::value
+    /// @pre The serializer should be at the root (array and maps should all be popped).
+    signalr::value Get() const;
+
+private:
+    // Adds the serialized container object to the previous object, or the root.
+    // This will internally call one of the FinalizeContainerSerializaitonInternal overrides.
+    void FinalizeContainerSerializaiton(signalr::value&& SerializedContainer);
+
+    // Case where we fallback to an exception is we are trying to serialize to an unsupported container.
+    template <typename T> void FinalizeContainerSerializaitonInternal(T&, signalr::value&& SerializedContainer);
+    // Case for serializing value into a vector.
+    void FinalizeContainerSerializaitonInternal(std::vector<signalr::value>& Container, signalr::value&& SerializedContainer);
+    // Case for serializing value into a pair for a map.
+    template <typename K> void FinalizeContainerSerializaitonInternal(std::pair<K, signalr::value>& Pair, signalr::value&& SerializedContainer);
+
+    // Writes all of our primitive types to a signalr value.
+    template <typename T> void WriteValueInternal(const T& Value);
+
+    // Case for writing container types to a signalr value.
+    // These cases are recursive.
+    template <typename T> void WriteValueInternal(const std::optional<T>& Value);
+    template <typename T> void WriteValueInternal(const std::vector<T>& Value);
+    template <typename K, typename T> void WriteValueInternal(const std::map<K, T>& Value);
+    template <typename T> void WriteValueInternal(const std::map<std::string, T>& Value);
+
+    // Converts primitive value to a signalr object.
+    template <typename T> signalr::value CreateSignalRObject(const T& Value);
+
+    using Container = std::variant<signalr::value, std::vector<signalr::value>, std::map<std::string, signalr::value>,
+        std::map<uint64_t, signalr::value>, std::pair<uint64_t, signalr::value>, std::pair<std::string, signalr::value>>;
+
+    std::stack<Container> Stack;
+};
+
+/// @brief A stack-based signalr deserializer which allows for custom class deserialization using ISignalRDeserializable
+class SignalRDeserializer
+{
+public:
+    /// @brief Constructor used to copy object to deserialize.
+    /// @param Object const signalr::value& : The value to deserialize
+    /// This should match the structure generated by
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/01dd37753716733c296959a524a721373a51a27d
created_at: 2025-05-08T11:08:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,541 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "SignalRSerializerTypeTraits.h"
+
+#include <map>
+#include <stack>
+#include <stdexcept>
+
+namespace csp::multiplayer
+{
+
+class ISignalRSerializable;
+
+/// @brief A stack-based signalr serializer which allows for custom class serialization using ISignalRSerializable
+class SignalRSerializer
+{
+public:
+    /// @brief Writes a value to the current container of the serializer.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteValue(const T& Value);
+
+    /// @brief Writes a uint key-value pair to the current uint map.
+    /// @pre StartUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> WriteKeyValue(K Key, const T& Value);
+
+    /// @brief Writes a string key-value pair to the current map.
+    /// @pre StartStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> WriteKeyValue(std::string Key, const T& Value);
+
+    /// @brief Starts writing an array into the serializer.
+    /// @details Once this function has been called, WriteValue should be used to add elements to the array.
+    /// Favour writing the array directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopArray should be used to finalize the array.
+    void StartWriteArray();
+
+    /// @brief Ends the current array in the serializer.
+    /// @pre StartWriteArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteArray();
+
+    /// @brief Starts writing a string map (std::map<std::string, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopStringMap should be used to finalize the map.
+    void StartWriteStringMap();
+
+    /// @brief Ends the current string map in the serializer.
+    /// @pre StartWriteStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteStringMap();
+
+    /// @brief Starts writing a uint map (std::map<uint64_t, T> into the serializer.
+    /// @details Once this function has been called, WriteKeyValue should be used to add elements to the map.
+    /// Favour writing the map directly to the serializer using the write functions over this.
+    /// Start/End functions should be used if you need custom serialization logic.
+    /// PopUintMap should be used to finalize the map.
+    void StartWriteUintMap();
+
+    /// @brief Ends the current uint map in the serializer.
+    /// @pre StartWriteUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndWriteUintMap();
+
+    /// @brief Gets the serialized singnal r value.
+    /// @return signalr::value
+    /// @pre The serializer should be at the root (array and maps should all be popped).
+    signalr::value Get() const;
+
+private:
+    // Adds the serialized container object to the previous object, or the root.
+    // This will internally call one of the FinalizeContainerSerializaitonInternal overrides.
+    void FinalizeContainerSerializaiton(signalr::value&& SerializedContainer);
+
+    // Case where we fallback to an exception is we are trying to serialize to an unsupported container.
+    template <typename T> void FinalizeContainerSerializaitonInternal(T&, signalr::value&& SerializedContainer);
+    // Case for serializing value into a vector.
+    void FinalizeContainerSerializaitonInternal(std::vector<signalr::value>& Container, signalr::value&& SerializedContainer);
+    // Case for serializing value into a pair for a map.
+    template <typename K> void FinalizeContainerSerializaitonInternal(std::pair<K, signalr::value>& Pair, signalr::value&& SerializedContainer);
+
+    // Writes all of our primitive types to a signalr value.
+    template <typename T> void WriteValueInternal(const T& Value);
+
+    // Case for writing container types to a signalr value.
+    // These cases are recursive.
+    template <typename T> void WriteValueInternal(const std::optional<T>& Value);
+    template <typename T> void WriteValueInternal(const std::vector<T>& Value);
+    template <typename K, typename T> void WriteValueInternal(const std::map<K, T>& Value);
+    template <typename T> void WriteValueInternal(const std::map<std::string, T>& Value);
+
+    // Converts primitive value to a signalr object.
+    template <typename T> signalr::value CreateSignalRObject(const T& Value);
+
+    using Container = std::variant<signalr::value, std::vector<signalr::value>, std::map<std::string, signalr::value>,
+        std::map<uint64_t, signalr::value>, std::pair<uint64_t, signalr::value>, std::pair<std::string, signalr::value>>;
+
+    std::stack<Container> Stack;
+};
+
+/// @brief A stack-based signalr deserializer which allows for custom class deserialization using ISignalRDeserializable
+class SignalRDeserializer
+{
+public:
+    /// @brief Constructor used to copy object to deserialize.
+    /// @param Object const signalr::value& : The value to deserialize
+    /// This should match the structure generated by
+    SignalRDeserializer(const signalr::value& Object);
+
+    /// @brief Constructor used to move object to deserialize.
+    /// @param Object signalr::value&& : The value to deserialize
+    /// This should match the structure generated by
+    SignalRDeserializer(signalr::value&& Object);
+
+    /// @brief Reads a value from the internal signalr array.
+    /// @pre This function should be used if this serializer represents a single value,
+    /// or if StartReadArray is called first to write to the array.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> ReadValue(T& OutVal);
+
+    /// @brief Reads a uint key-value pair to the current uint map.
+    /// @pre EnterUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename K, typename T>
+    std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> ReadKeyValue(std::pair<K, T>& OutVal);
+
+    /// @brief Reads a string key-value pair to the current string map.
+    /// @pre EnterStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> ReadKeyValue(std::pair<std::string, T>& OutVal);
+
+    /// @brief Enters the internal signalr value as an array
+    /// @details Once this function has been called, ReadValue should be used to read elements from the array.
+    /// @param Size size_t& Output parameter specifying the size of the array.
+    /// ExitArray should be used to exit the array once elements have been read.
+    void StartReadArray(size_t& Size);
+
+    /// @brief Exits the internal signalr array.
+    /// @pre StartReadArray should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndReadArray();
+
+    /// @brief Enters the internal signalr value as an uint map
+    /// @details Once this function has been called, ReadKeyValue should be used to read elements from the map.
+    /// @param Size size_t& Output parameter specifying the size of the map.
+    /// ExitUintMap should be used to exit the map once elements have been read.
+    void StartReadUintMap(size_t& Size);
+
+    /// @brief Exits the internal signalr uint map.
+    /// @pre StartReadUintMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndReadUintMap();
+
+    /// @brief Enters the internal signalr value as an string map
+    /// @details Once this function has been called, ReadKeyValue should be used to read elements from the map.
+    /// @param Size size_t& Output parameter specifying the size of the map.
+    /// ExitStringMap should be used to exit the map once elements have been read.
+    void StartReadStringMap(size_t& Size);
+
+    /// @brief Exits the internal signalr string map.
+    /// @pre StartReadStringMap should be called before this function.
+    /// A std::runtime_error will be thrown if this condition is not met.
+    void EndReadStringMap();
+
+private:
+    // Reads the next signalr value using the current iterator.
+    const signalr::value& ReadNextValue();
+
+    const std::pair<std::uint64_t, signalr::value> ReadNextUintKeyValue() const;
+    const std::pair<std::string, signalr::value> ReadNextStringKeyValue() const;
+
+    // Reads the specified type from the given signalr object.
+    // This internally calls the ReadValueFromObjectInternal for the given type.
+    template <typename T> void ReadValueFromObject(const signalr::value& Object, T& OutVal);
+
+    // Case for reading signed integers.
+    template <typename T> std::enable_if_t<IsSignedIntegerV<T>> ReadValueFromObjectInternal(const signalr::value& Object, T& OutVal);
+    // Case for reading unsigned integers.
+    template <typename T> std::enable_if_t<IsUnsignedIntegerV<T>> ReadValueFromObjectInternal(const signalr::value& Object, T& OutVal);
+    // Case for other primitive types.
+    void ReadValueFromObjectInternal(const signalr::value& Object, double& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, float& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, bool& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, std::string& OutVal);
+    void ReadValueFromObjectInternal(const signalr::value& Object, nullptr_t);
+
+    // Case for other container values.
+    template <typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::optional<T>& OutVal);
+    template <typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::vector<T>& OutVal);
+    template <typename K, typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::map<K, T>& OutVal);
+    template <typename T> void ReadValueFromObjectInternal(const signalr::value& Object, std::map<std::string, T>& OutVal);
+
+    void IncrementIterator();
+
+    using Iterator = std::variant<nullptr_t, std::vector<signalr::value>::const_iterator, std::map<uint64_t, signalr::value>::const_iterator,
+        std::map<std::string, signalr::value>::const_iterator>;
+
+    signalr::value Root;
+    std::stack<Iterator> ObjectStack;
+};
+
+/// @brief A serializer interface to allow classes to be serialized.
+class ISignalRSerializable
+{
+public:
+    /// @brief Function internally called when serializing this class using SignalRSerializer.
+    virtual void Serialize(SignalRSerializer&) const = 0;
+
+    virtual ~ISignalRSerializable() = default;
+
+protected:
+    ISignalRSerializable() = default;
+
+    ISignalRSerializable(const ISignalRSerializable&) = default;
+    ISignalRSerializable(ISignalRSerializable&&) = default;
+
+    ISignalRSerializable& operator=(const ISignalRSerializable&) = default;
+    ISignalRSerializable& operator=(ISignalRSerializable&&) = default;
+};
+
+/// @brief A deserializer interace to allow classes to be deserialized.
+class ISignalRDeserializable
+{
+public:
+    /// @brief Function internally called when serializing this class using SignalRDeserializer.
+    virtual void Deserialize(SignalRDeserializer&) = 0;
+
+    virtual ~ISignalRDeserializable() = default;
+
+protected:
+    ISignalRDeserializable() = default;
+
+    ISignalRDeserializable(const ISignalRDeserializable&) = default;
+    ISignalRDeserializable(ISignalRDeserializable&&) = default;
+
+    ISignalRDeserializable& operator=(const ISignalRDeserializable&) = default;
+    ISignalRDeserializable& operator=(ISignalRDeserializable&&) = default;
+};
+
+template <typename T> std::enable_if_t<IsSupportedSignalRType<T>::value> SignalRSerializer::WriteValue(const T& Value) { WriteValueInternal(Value); }
+
+template <typename K, typename T>
+std::enable_if_t<IsUnsignedIntegerV<K> && IsSupportedSignalRType<T>::value> SignalRSerializer::WriteKeyValue(K Key, const T& Value)
+{
+    if (Stack.size() == 0 || std ::holds_alternative<std::map<uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    Stack.push(std::pair<uint64_t, signalr::value> {});
+    std::get<std::pair<uint64_t, signalr::value>>(Stack.top()).first = static_cast<uint64_t>(Key);
+
+    WriteValue(Value);
+
+    // Get our pair form the top of the stack and pop
path: Library/src/Multiplayer/SignalRSerializer.h
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/162647a7118d4312d16adbf49cf49a1cfe667054
created_at: 2025-05-08T11:11:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,283 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::StartWriteArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::EndWriteArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto ArrayObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::vector<signalr::value>>(ArrayObject) });
+}
+
+void SignalRSerializer::StartWriteStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto StringMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<std::string, signalr::value>>(StringMapObject) });
+}
+
+void SignalRSerializer::StartWriteUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto UintMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<uint64_t, signalr::value>>(UintMapObject) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    if (std::holds_alternative<signalr::value>(Stack.top()))
+    {
+        return std::get<signalr::value>(Stack.top());
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Stack.top()) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::FinalizeContainerSerializaiton(signalr::value&& SerializedContainer)
+{
+    // We dont check for maps in this function because key-values are handled seperatly
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/commit/162647a7118d4312d16adbf49cf49a1cfe667054
created_at: 2025-05-08T11:11:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,283 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::StartWriteArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::EndWriteArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto ArrayObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::vector<signalr::value>>(ArrayObject) });
+}
+
+void SignalRSerializer::StartWriteStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto StringMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<std::string, signalr::value>>(StringMapObject) });
+}
+
+void SignalRSerializer::StartWriteUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto UintMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<uint64_t, signalr::value>>(UintMapObject) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    if (std::holds_alternative<signalr::value>(Stack.top()))
+    {
+        return std::get<signalr::value>(Stack.top());
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Stack.top()) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::FinalizeContainerSerializaiton(signalr::value&& SerializedContainer)
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/dd419f6800dfb3ae0fb269cfa6fe8e8a78d22d8d
created_at: 2025-05-08T11:14:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,283 @@
+#include "SignalRSerializer.h"
+
+namespace csp::multiplayer
+{
+void SignalRSerializer::StartWriteArray() { Stack.push(std::vector<signalr::value> {}); }
+
+void SignalRSerializer::EndWriteArray()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::vector<signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in an array");
+    }
+
+    auto ArrayObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::vector<signalr::value>>(ArrayObject) });
+}
+
+void SignalRSerializer::StartWriteStringMap() { Stack.push(std::map<std::string, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteStringMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a string map");
+    }
+
+    auto StringMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<std::string, signalr::value>>(StringMapObject) });
+}
+
+void SignalRSerializer::StartWriteUintMap() { Stack.push(std::map<uint64_t, signalr::value> {}); }
+
+void SignalRSerializer::EndWriteUintMap()
+{
+    if (Stack.size() == 0 || std::holds_alternative<std::map<std::uint64_t, signalr::value>>(Stack.top()) == false)
+    {
+        throw std::runtime_error("Invalid call: Serializer was not in a uint map");
+    }
+
+    auto UintMapObject = std::move(Stack.top());
+    Stack.pop();
+    FinalizeContainerSerializaiton(signalr::value { std::get<std::map<uint64_t, signalr::value>>(UintMapObject) });
+}
+
+signalr::value SignalRSerializer::Get() const
+{
+    if (Stack.size() == 0)
+    {
+        return signalr::value();
+    }
+
+    if (Stack.size() > 1)
+    {
+        throw std::runtime_error("Invalid call: Serializer is not at the root");
+    }
+
+    if (std::holds_alternative<signalr::value>(Stack.top()))
+    {
+        return std::get<signalr::value>(Stack.top());
+    }
+    else if (std::holds_alternative<std::vector<signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::vector<signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<uint64_t, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<uint64_t, signalr::value>>(Stack.top()) };
+    }
+    else if (std::holds_alternative<std::map<std::string, signalr::value>>(Stack.top()))
+    {
+        return signalr::value { std::get<std::map<std::string, signalr::value>>(Stack.top()) };
+    }
+    else
+    {
+        throw std::runtime_error("Unexpected serializer state");
+    }
+}
+
+void SignalRSerializer::FinalizeContainerSerializaiton(signalr::value&& SerializedContainer)
+{
+    // We dont check for maps in this function because key-values are handled seperatly
+    if (Stack.size() == 0)
+    {
+        // If the last element has been popped, push back a root value
+        Stack.push(SerializedContainer);
+        return;
+    }
+
+    // Dispatch internal variant type to the correct FinalizeContainerSerializaitonInternal call
+    std::visit([this, &SerializedContainer](auto& ValType) { this->FinalizeContainerSerializaitonInternal(ValType, std::move(SerializedContainer)); },
+        Stack.top());
+}
+
+void SignalRSerializer::FinalizeContainerSerializaitonInternal(std::vector<signalr::value>& Container, signalr::value&& SerializedContainer)
path: Library/src/Multiplayer/SignalRSerializer.cpp
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/dd419f6800dfb3ae0fb269cfa6fe8e8a78d22d8d
created_at: 2025-05-08T11:14:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,574 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Multiplayer/SignalRSerializer.h"
+#include "TestHelpers.h"
+
+#include <gtest/gtest.h>
+
+using namespace csp::multiplayer;
+
+// Test we can serialize/deserialize int64 values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeIntTest)
path: Tests/src/InternalTests/SignalRSerializerTests.cpp
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/cbddd97547951e2551b6b186edacfa6a3e711e5f

This was actually a bug!
created_at: 2025-05-08T11:15:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,574 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Multiplayer/SignalRSerializer.h"
+#include "TestHelpers.h"
+
+#include <gtest/gtest.h>
+
+using namespace csp::multiplayer;
+
+// Test we can serialize/deserialize int64 values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeIntTest)
+{
+    const int64_t Value = 2;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    int64_t DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+// Test we can serialize/deserialize uint64 values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUintTest)
+{
+    const uint64_t Value = 1;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    uint64_t DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+/*
+    This is important, as signalr only supports int64 signed integer types,
+    so we need to test our internal conversion logic.
+*/
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeShortTest)
+{
+    const short Value = -2;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    short DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
+
+    EXPECT_EQ(DeserializedValue, Value);
+}
+
+/*
+    This is important, as signalr only supports uint64 unsigned integer types,
+    so we need to test our internal conversion logic.
+*/
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeUshortTest)
+{
+    const unsigned short Value = 2;
+
+    SignalRSerializer Serializer;
+    Serializer.WriteValue(Value);
+
+    signalr::value SerializedValue = Serializer.Get();
+
+    SignalRDeserializer Deserializer { SerializedValue };
+
+    unsigned short DeserializedValue;
+    Deserializer.ReadValue(DeserializedValue);
path: Tests/src/InternalTests/SignalRSerializerTests.cpp
user: login: MAG-mv
body: Fixed in: https://github.com/magnopus-opensource/connected-spaces-platform/pull/679/commits/e3028b6126d28e5d0812c2ed6fceab0de7ea056d
created_at: 2025-05-08T11:51:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,574 @@
+/*
+ * Copyright 2025 Magnopus LLC
+
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "Multiplayer/SignalRSerializer.h"
+#include "TestHelpers.h"
+
+#include <gtest/gtest.h>
+
+using namespace csp::multiplayer;
+
+// Test we can serialize/deserialize int64 values.
+CSP_INTERNAL_TEST(CSPEngine, SignalRSerializerTests, SerializeIntTest)
path: Tests/src/InternalTests/SignalRSerializerTests.cpp
user: login: MAG-AdamThorn
body: Reviews for the win!
created_at: 2025-05-09T09:13:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: MAG-ElliotMorris
created_at: 2025-05-02T09:17:41Z
body: [Consider] Updating your PR description to explain why this is here, one would naively expect this to be replacing something. I expect you're doing a staged removal, but worth calling it out, explaining what sort of timeframe, whether this is going to fully replace all serialization all at once in the next step or get strangler-patterned over, etc.

P.S: Not reviewing, but this looks great from a skim.
author_association: CONTRIBUTOR
user: login: MAG-mv
created_at: 2025-05-02T09:31:13Z
body: > [Consider] Updating your PR description to explain why this is here, one would naively expect this to be replacing something. I expect you're doing a staged removal, but worth calling it out, explaining what sort of timeframe, whether this is going to fully replace all serialization all at once in the next step or get strangler-patterned over, etc.
> 
> P.S: Not reviewing, but this looks great from a skim.

Thanks. I'll update the description
author_association: CONTRIBUTOR