commit fe3aa4a77eca40cbcce73d741985cb839030bee4 Author: BordedDev <> Date: Thu Jan 30 17:41:19 2025 +0100 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1312090 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.sh text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff8cde7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,303 @@ +*.d +*.slo +*.lo +*.o +*.obj +*.gch +*.pch +*.so +*.dylib +*.dll +*.mod +*.smod +*.lai +*.la +*.a +*.lib +*.exe +*.out +*.app +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/**/aws.xml +.idea/**/contentModel.xml +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml +.idea/**/gradle.xml +.idea/**/libraries +cmake-build-*/ +.idea/**/mongoSettings.xml +*.iws +out/ +.idea_modules/ +atlassian-ide-plugin.xml +.idea/replstate.xml +.idea/sonarlint/ +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +.idea/httpRequests +.idea/caches/build_file_checksums.ser +*~ +.fuse_hidden* +.directory +.Trash-* +.nfs* +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.userprefs +mono_crash.* +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ +.vs/ +Generated\ Files/ +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.VisualState.xml +TestResult.xml +nunit-*.xml +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c +BenchmarkDotNet.Artifacts/ +project.lock.json +project.fragment.lock.json +artifacts/ +ScaffoldingReadMe.txt +StyleCopReport.xml +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.iobj +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc +_Chutzpah* +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb +*.psess +*.vsp +*.vspx +*.sap +*.e2e +$tf/ +*.gpState +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user +_TeamCity* +*.dotCover +.axoCover/* +!.axoCover/settings.json +coverage*.json +coverage*.xml +coverage*.info +*.coverage +*.coveragexml +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* +*.mm.* +AutoTest.Net/ +.sass-cache/ +[Ee]xpress/ +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html +publish/ +*.[Pp]ublish.xml +*.azurePubxml +*.pubxml +*.publishproj +PublishScripts/ +*.nupkg +*.snupkg +**/[Pp]ackages/* +!**/[Pp]ackages/build/ +*.nuget.props +*.nuget.targets +csx/ +*.build.csdef +ecf/ +rcf/ +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload +*.[Cc]ache +!?*.[Cc]ache/ +ClientBin/ +~$* +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs +Generated_Code/ +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak +*.mdf +*.ldf +*.ndf +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl +FakesAssemblies/ +*.GhostDoc.xml +.ntvs_analysis.dat +node_modules/ +*.plg +*.opt +*.vbw +*.vbp +*.dsw +*.dsp +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions +.paket/paket.exe +paket-files/ +.fake/ +.cr/personal +__pycache__/ +*.pyc +*.tss +*.jmconfig +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs +OpenCover/ +ASALocalRun/ +*.binlog +*.nvuser +.mfractor/ +.localhistory/ +.vshistory/ +healthchecksdb +MigrationBackup/ +.ionide/ +FodyWeavers.xsd +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.history/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.sln.iml +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db +*.stackdump +[Dd]esktop.ini +$RECYCLE.BIN/ +*.lnk +.DS_Store +.AppleDouble +.LSOverride +Icon +._* +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +.runenv/ +vcpkg_installed/ +git2 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..aa471e2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.30) +project(mmo) + +set(CMAKE_CXX_STANDARD 26) +if (MSVC) + add_compile_options(/W4) + add_compile_options(/WX) + add_compile_options(/external:anglebrackets) + add_compile_options(/external:W0) + add_compile_options(/wd4100) + add_compile_options(/wd5050) + add_definitions(-DWIN32_LEAN_AND_MEAN -DVC_EXTRALEAN) + add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX) +else () + add_compile_options(-Wall) + add_compile_options(-Wextra) + add_compile_options(-Wpedantic) + add_compile_options(-Werror) +endif () + +# function add santizers +function(add_sanitizers target) + if (MSVC) + target_compile_options(${target} PRIVATE /fsanitize=address /fsanitize=fuzzer /Zi) + target_link_options(${target} PRIVATE /fsanitize=address /fsanitize=fuzzer /Zi) + else () + target_compile_options(${target} PRIVATE -fsanitize=address -fsanitize=fuzzer) + target_link_options(${target} PRIVATE -fsanitize=address -fsanitize=fuzzer) + endif () +endfunction() + +if (DEFINED VCPKG_INSTALLED_DIR) +elseif (DEFINED ENV{VCPKG_ROOT}) + include($ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake) +else () + message(FATAL_ERROR "VCPKG is not loaded, set VCPKG_ROOT to automatically load it or specify the cmake toolchain") +endif () + +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + +add_subdirectory(common) +#add_subdirectory(client) +#add_subdirectory(server) diff --git a/README.md b/README.md new file mode 100644 index 0000000..1679f7c --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +Add `-DCMAKE_TOOLCHAIN_FILE=K:\Dev\Tools\vcpkg\scripts\buildsystems\vcpkg.cmake` to cmake command line to use vcpkg +toolchain file. + +This is only necessary in debug mode: + +Modify the VCPKG port of crypto++ and cli11 add the following to the +`\ports\\portfile.cmake` file before the configure +command: + +```cmake +if (MSVC) + set(VCPKG_CXX_FLAGS_DEBUG "/fsanitize=address /fsanitize=fuzzer") + set(VCPKG_C_FLAGS_DEBUG "/fsanitize=address /fsanitize=fuzzer") + set(VCPKG_LINKER_FLAGS_DEBUG "/fsanitize=address /fsanitize=fuzzer") +else () + set(VCPKG_CXX_FLAGS_DEBUG -fsanitize=address) + set(VCPKG_C_FLAGS_DEBUG -fsanitize=address) + set(VCPKG_LINKER_FLAGS_DEBUG -fsanitize=address) +endif () +``` + +An overlay can be created but I don't want to maintain adding the overlay to the vcpkg toolchain file and keeping it up +to date with the vcpkg toolchain file. + +Generate a certificate using the following command (for testing purposes only): + +```bash +openssl req -new -newkey rsa:2048 -days 1 -nodes -x509 -keyout key.pem -out cert.pem -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" +``` + +## Notes + +Someone decided to migrate MSQuic's vcpkg to schannel instead of using OpenSSL (which is exclusive to windows). +This means that certificates are not supported, which is a problem. Which is why the 0-RTT feature is enabled by in +vcpkg.json, not because we need or want it. + +Initially, Glaze was intended for parsing session info, but it caused issues with the MSVC compiler. +Instead, we're using simdjson. \ No newline at end of file diff --git a/common/CMakeLists.txt b/common/CMakeLists.txt new file mode 100644 index 0000000..92c42bd --- /dev/null +++ b/common/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(logging) +add_subdirectory(connection) +add_subdirectory(packet) \ No newline at end of file diff --git a/common/connection/CMakeLists.txt b/common/connection/CMakeLists.txt new file mode 100644 index 0000000..798bc30 --- /dev/null +++ b/common/connection/CMakeLists.txt @@ -0,0 +1,62 @@ +project(connection) + + +set(${PROJECT_NAME}_src + src/DataConnection.cppm + src/MSQuicConnection.cppm + src/MSQuicServer.cppm + src/MSQuicGlobal.cppm + src/MSQuicError.cppm) +# this is the "object library" target: compiles the sources only once +add_library(${PROJECT_NAME}_lib STATIC ${${PROJECT_NAME}_src}) +target_include_directories(${PROJECT_NAME}_lib PUBLIC ./src) + +find_package(msquic CONFIG REQUIRED) +target_link_libraries(${PROJECT_NAME}_lib PRIVATE msquic) +target_link_libraries(${PROJECT_NAME}_lib PUBLIC logging_lib) +if(WIN32) + target_link_libraries(${PROJECT_NAME}_lib PUBLIC ntdll) +endif() + +# Setup tests +enable_testing() + +find_package(GTest CONFIG REQUIRED) +include(GoogleTest) + +file(GLOB_RECURSE tests_${PROJECT_NAME}_src CONFIGURE_DEPENDS tests/*.cppm tests/*/*.cppm) + +add_executable(tests_${PROJECT_NAME} ${tests_${PROJECT_NAME}_src} + tests/sanitizers.cpp) +target_link_libraries(tests_${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main GTest::gmock GTest::gmock_main ${PROJECT_NAME}_lib) + +add_test(AllTestsIn${PROJECT_NAME} tests_${PROJECT_NAME}) + +gtest_discover_tests(tests_${PROJECT_NAME}) + +find_program(OPENSSL_EXECUTABLE openssl) + +set(OPENSSL_EXECUTABLE_CFG "" CACHE FILEPATH "Path to openssl.cnf") +if (NOT OPENSSL_EXECUTABLE) + find_package(OpenSSL REQUIRED) + + message(STATUS "OpenSSL found: ${OPENSSL_FOUND} ${OPENSSL_VERSION} ${OPENSSL_INCLUDE_DIR} ${OPENSSL_LIBRARIES}") + + set(OPENSSL_EXECUTABLE ${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/tools/openssl/openssl) +endif () +if (NOT OPENSSL_EXECUTABLE) + message(FATAL_ERROR "OpenSSL not found") +endif () + +if (OPENSSL_EXECUTABLE MATCHES ^${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/tools/openssl/openssl) + set(OPENSSL_EXECUTABLE_CFG ${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/tools/openssl/openssl.cnf) +endif () + +add_custom_command(TARGET tests_${PROJECT_NAME} POST_BUILD + COMMAND "${OPENSSL_EXECUTABLE}" req -new -newkey rsa:2048 -days 1 -nodes -x509 -keyout key.pem -out cert.pem -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" $<$:-config> $<$:${OPENSSL_EXECUTABLE_CFG}> + WORKING_DIRECTORY ${WORKING_DIRECTORY} + BYPRODUCTS ${WORKING_DIRECTORY}/key.pem ${WORKING_DIRECTORY}/cert.pem + COMMENT "Generating self-signed certificate using ${OPENSSL_EXECUTABLE} ${OPENSSL_EXECUTABLE_CFG}" + VERBATIM + COMMAND_EXPAND_LISTS +) \ No newline at end of file diff --git a/common/connection/src/DataConnection.cppm b/common/connection/src/DataConnection.cppm new file mode 100644 index 0000000..6f01e68 --- /dev/null +++ b/common/connection/src/DataConnection.cppm @@ -0,0 +1,26 @@ +export module MMO.DataConnection; +import std; +import :MSQuicConnection; +import :MSQuicServer; + +namespace MMO::Networking { + export using NetworkingDataLane = MSQUIC::MSQuicStream; + export using NetworkingConnection = MSQUIC::MSQuicConnection; + export using NetworkingServer = MSQUIC::MSQuicServer; + + export using NetworkingError = MSQUIC::MSQuicError; + + export using NetworkSettings = QUIC_SETTINGS; + export using NetworkCertificate = QUIC_CERTIFICATE_FILE; + export using NetworkCertificateProtected = QUIC_CERTIFICATE_FILE_PROTECTED; + export using NetworkCredentials = QUIC_CREDENTIAL_CONFIG; + + export namespace Additional { + using CertificateHash = QUIC_CERTIFICATE_HASH; + using CertificateHashStore = QUIC_CERTIFICATE_HASH_STORE; + using CertificateHashStoreFlags = QUIC_CERTIFICATE_HASH_STORE_FLAGS; + + using CredentialFlags = QUIC_CREDENTIAL_FLAGS; + using CredentialType = QUIC_CREDENTIAL_TYPE; + } +} diff --git a/common/connection/src/MSQuicConnection.cppm b/common/connection/src/MSQuicConnection.cppm new file mode 100644 index 0000000..148256e --- /dev/null +++ b/common/connection/src/MSQuicConnection.cppm @@ -0,0 +1,850 @@ +export module MMO.DataConnection:MSQuicConnection; + +import ; +import std; +import :MSQuicGlobal; +import :MSQuicError; +import MMO.Logging; + +namespace MMO::Networking::MSQUIC { + export template + concept DataStorage = requires(T a) { + sizeof(typename std::remove_reference_t::value_type) == 1; + { reinterpret_cast(std::data(a)) } -> std::same_as; + { static_cast(std::size(a)) } -> std::same_as; + }; + + + // Forward declaration because we need to be able to ref MSQuicServer + QUIC_STATUS msquicClientConnectionCallbackProxy(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event); + QUIC_STATUS msquicClientStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event); + + // Forward declaration because we need to be able to ref MSQuicConnection + QUIC_STATUS msquicServerListeningCallbackProxy(HQUIC connection, void* context, QUIC_LISTENER_EVENT* event); + QUIC_STATUS msquicServerConnectionCallbackProxy(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event); + QUIC_STATUS msquicServerStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event); + + export class MSQuicServer; + export class MSQuicConnection; + + export class MSQuicStream : public std::enable_shared_from_this { + friend MSQuicConnection; + std::shared_ptr backReference = nullptr; + std::shared_ptr connectionStream = nullptr; + std::shared_ptr streamAutoCloser = nullptr; + QUIC_STATUS status = QUIC_STATUS_NOT_FOUND; + + std::vector incomingData = { }; + + std::weak_ptr origin; + + MSQuicStream( + std::shared_ptr connectionStream, + std::shared_ptr streamAutoCloser, + std::weak_ptr origin, + const QUIC_STATUS status + ) : + connectionStream(std::move(connectionStream)), streamAutoCloser(std::move(streamAutoCloser)), + status(status), + origin(std::move(origin)) { } + + public: + [[nodiscard]] + std::vector retrieveAndResetData() { + std::vector outgoingData; + outgoingData.swap(incomingData); + return outgoingData; + } + + [[nodiscard]] + bool hasData() const { + return !incomingData.empty(); + } + + std::weak_ptr getOrigin() { + return origin; + } + + [[nodiscard]] + bool isConnected() const { + return status == QUIC_STATUS_SUCCESS; + } + }; + + struct Payload : public QUIC_BUFFER, std::enable_shared_from_this { + // We use this persistent buffer to ensure that the data is not deallocated + std::any data; + + template + explicit Payload(T& data) : data(data) { + this->Length = static_cast(std::size(std::any_cast(this->data))); + this->Buffer = const_cast(reinterpret_cast( + std::data(std::any_cast(this->data)))); + } + }; + + export class MSQuicConnection : public std::enable_shared_from_this { + public: + std::shared_ptr logger = MMO::Logging::DEFAULT_LOGGER + ? MMO::Logging::DEFAULT_LOGGER + : std::make_shared(); + + protected: + std::shared_ptr configuration = nullptr; + std::shared_ptr connectionHandle = nullptr; + std::vector> connectionStreams = { }; + std::unordered_map> connectionStreamMap = { }; + + std::unordered_map> queuedData = { }; + + + std::vector, + std::shared_ptr + )>>> onRemoteStreamCallbacks; + + QUIC_STATUS connectionStatus = QUIC_STATUS_NOT_FOUND; + + public: + const std::string remoteAddress; + const uint16_t remotePort; + + + void addRemoteStreamCallbacks( + const std::weak_ptr, + std::shared_ptr + )>>& callback + ) { + onRemoteStreamCallbacks.emplace_back(callback); + } + + [[nodiscard]] + bool isConnected() const { + return connectionStatus == QUIC_STATUS_SUCCESS; + } + + [[nodiscard]] + bool isTerminated() const { + return connectionStatus != QUIC_STATUS_PENDING && connectionStatus != QUIC_STATUS_SUCCESS; + } + + [[nodiscard]] + bool isConnecting() const { + return connectionStatus == QUIC_STATUS_PENDING; + } + + private: + [[nodiscard]] explicit + MSQuicConnection( + std::shared_ptr configuration, + std::string remoteAddress, + const uint16_t remotePort + ) : + configuration(std::move(configuration)), remoteAddress(std::move(remoteAddress)), remotePort(remotePort) { } + + friend MSQuicServer; + + public: + static void defaultSettings(QUIC_SETTINGS& settings, QUIC_CREDENTIAL_CONFIG& credConfig) { + settings.IdleTimeoutMs = 5000; + settings.IsSet.IdleTimeoutMs = TRUE; + } + + [[nodiscard]] + static std::expected, MSQuicError> connectTo( + const QUIC_SETTINGS& settings, + const QUIC_CREDENTIAL_CONFIG& credConfig, + const std::string& remoteAddress, + const uint16_t remotePort + ) { + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + return std::unexpected(MSQGR.error()); + } + + const auto& MSQG = MSQGR.value().get(); + const QUIC_API_TABLE* MsQuic = *MSQG.msQuicApiTable; + + std::shared_ptr configuration( + new HQUIC(nullptr), [](const HQUIC* configuration) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value() && *configuration != nullptr) { + (*msquic.value().get().msQuicApiTable)->ConfigurationClose(*configuration); + } + + delete configuration; + } + ); + if (QUIC_STATUS status = MsQuic->ConfigurationOpen( + *MSQG.registration, + &MSQG.applicationLayerProtocolNegotiationName, 1, + &settings, sizeof(settings), nullptr, + configuration.get() + ); + QUIC_FAILED(status)) { + return std::unexpected(MSQuicError(status)); + } + + if (QUIC_STATUS status = MsQuic->ConfigurationLoadCredential(*configuration.get(), &credConfig); + QUIC_FAILED(status)) { + return std::unexpected(MSQuicError(status)); + } + + return std::shared_ptr( + new MSQuicConnection(configuration, remoteAddress, remotePort), + [](const MSQuicConnection* client) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value()) { + auto MSQG2 = msquic.value().get(); + for (auto& stream : client->connectionStreams) { + (*MSQG2.msQuicApiTable)->StreamShutdown( + *stream, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT | QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE, + 0 + ); + } + } + delete client; + } + ); + } + + [[nodiscard]] + static std::expected, MSQuicError> + connectTo(const std::string& remoteAddress, const uint16_t remotePort) { + QUIC_SETTINGS settings{ 0 }; + QUIC_CREDENTIAL_CONFIG credentialConfig{ + .Type = QUIC_CREDENTIAL_TYPE_NONE, + .Flags = QUIC_CREDENTIAL_FLAG_CLIENT | QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION + }; + defaultSettings(settings, credentialConfig); + return connectTo(settings, credentialConfig, remoteAddress, remotePort); + } + + + [[nodiscard]] + std::optional establishConnection(const std::span& resumptionTicket = { }) { + if (!isConnecting() && !isConnected()) { + connectionStatus = QUIC_STATUS_PENDING; + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + connectionStatus = MSQGR.error().status; + return MSQGR.error(); + } + + const auto& MSQG = MSQGR.value().get(); + + connectionHandle.reset( + new HQUIC(nullptr), [](const HQUIC* connectionHandle) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value() && *connectionHandle != nullptr) { + (*msquic.value().get().msQuicApiTable)->ConnectionClose(*connectionHandle); + } + + delete connectionHandle; + } + ); + + if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->ConnectionOpen( + *MSQG.registration, msquicClientConnectionCallbackProxy, this, connectionHandle.get() + ); + QUIC_FAILED(status)) { + connectionStatus = status; + connectionHandle.reset(); + return MSQuicError(status); + } + + if (!resumptionTicket.empty()) { + if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->SetParam( + *connectionHandle, QUIC_PARAM_CONN_RESUMPTION_TICKET, + static_cast(resumptionTicket.size()), + resumptionTicket.data() + ); + QUIC_FAILED(status)) { + connectionStatus = status; + connectionHandle.reset(); + return MSQuicError(status); + } + } + + if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->ConnectionStart( + *connectionHandle, *configuration, QUIC_ADDRESS_FAMILY_UNSPEC, + remoteAddress.c_str(), remotePort + ); + QUIC_FAILED(status)) { + connectionStatus = status; + connectionHandle.reset(); + return MSQuicError(status); + } + } + return std::nullopt; + } + + [[nodiscard]] + std::expected, MSQuicError> createStream( + QUIC_STREAM_OPEN_FLAGS openFlags = QUIC_STREAM_OPEN_FLAG_NONE, + QUIC_STREAM_START_FLAGS startFlags = QUIC_STREAM_START_FLAG_NONE + ) { + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + return std::unexpected(MSQGR.error()); + } + + const auto& MSQG = MSQGR.value().get(); + + std::shared_ptr stream; + stream.reset( + new HQUIC(nullptr), [&](const HQUIC* connectionStream) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value() && *connectionStream != nullptr) { + (*msquic.value().get().msQuicApiTable)->StreamClose(*connectionStream); + } + + delete connectionStream; + } + ); + if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->StreamOpen( + *connectionHandle, openFlags, + msquicClientStreamCallbackProxy, this, + stream.get() + ); + QUIC_FAILED(status)) { + return std::unexpected(MSQuicError(status)); + } + + if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->StreamStart(*stream, startFlags); + QUIC_FAILED(status)) { + return std::unexpected(MSQuicError(status)); + } + + connectionStreams.emplace_back(stream); + + // We store these separately, since connectionStreams should be closed based on the relevant events but + // a close can also be requested by losing the shared_ptr preventing a memory/stream leak + std::shared_ptr streamPtr( + new MSQuicStream( + stream, + std::shared_ptr( + nullptr, [&, stream](void*) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value() && *stream != nullptr) { + (*msquic.value().get().msQuicApiTable)->StreamShutdown( + *stream, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0 + ); + } else { std::erase(connectionStreams, stream); } + } + ), + shared_from_this(), + QUIC_STATUS_SUCCESS + ), [](const MSQuicStream* ptr) { + delete ptr; + } + ); + + connectionStreamMap.emplace(*stream, streamPtr); + + return streamPtr; + } + + template + [[nodiscard]] + std::optional send( + T& data, + std::shared_ptr& stream, + QUIC_SEND_FLAGS flags = QUIC_SEND_FLAG_NONE + ) { + if (!isConnected() || (stream && !stream->isConnected())) { + return MSQuicError(QUIC_STATUS_INVALID_STATE); + } + + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + return MSQGR.error(); + } + + const auto& MSQG = MSQGR.value().get(); + + if ((!stream || !stream->connectionStream) && !(flags & QUIC_SEND_FLAG_START)) { + return MSQuicError(QUIC_STATUS_INVALID_STATE); + } + + std::shared_ptr dataStream = nullptr; + if (stream && stream->connectionStream) { + dataStream = stream->connectionStream; + } else if (flags & QUIC_SEND_FLAG_FIN) { + dataStream.reset( + new HQUIC(nullptr), [](const HQUIC* connectionStream) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value() && *connectionStream != nullptr) { + (*msquic.value().get().msQuicApiTable)->StreamClose(*connectionStream); + } + delete connectionStream; + } + ); + + if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->StreamOpen( + *connectionHandle, QUIC_STREAM_OPEN_FLAG_NONE, //QUIC_STREAM_OPEN_FLAG_UNIDIRECTIONAL, + msquicClientStreamCallbackProxy, this, + dataStream.get() + ); + QUIC_FAILED(status)) { + return MSQuicError(status); + } + + connectionStreams.emplace_back(dataStream); + } else if (auto strim = createStream(); strim.has_value()) { + stream = strim.value(); + dataStream = stream->connectionStream; + } else { + return strim.error(); + } + + if (!dataStream) { + return MSQuicError(QUIC_STATUS_INVALID_STATE); + } + + + std::shared_ptr sendData = std::make_shared(data); + + queuedData.emplace(sendData.get(), sendData); + + // QUIC_BUFFER* buffer = new QUIC_BUFFER{ + // static_cast(std::size(data)), reinterpret_cast(std::data(data)) + // }; + if (QUIC_STATUS status = (*MSQG.msQuicApiTable)->StreamSend( + *dataStream.get(), sendData.get(), 1, flags, sendData.get() + ); + QUIC_FAILED(status)) { + (*MSQG.msQuicApiTable)->StreamShutdown(*dataStream.get(), QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 1); + return MSQuicError(status); + } + + return std::nullopt; + } + + template + [[nodiscard]] + constexpr std::optional send( + T& data, + QUIC_SEND_FLAGS flags = QUIC_SEND_FLAG_START | QUIC_SEND_FLAG_FIN + ) { + std::shared_ptr stream; + return send(data, stream, flags | QUIC_SEND_FLAG_START | QUIC_SEND_FLAG_FIN); + } + + [[nodiscard]] + std::optional shutdown() const { + if (!isConnected()) { + return MSQuicError(QUIC_STATUS_INVALID_STATE); + } + + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + return MSQGR.error(); + } + + const auto& MSQG = MSQGR.value().get(); + + (*MSQG.msQuicApiTable)->ConnectionShutdown( + *connectionHandle, QUIC_CONNECTION_SHUTDOWN_FLAG_NONE, 0 + ); + + return std::nullopt; + } + + private: + friend QUIC_STATUS msquicClientConnectionCallbackProxy( + HQUIC connection, + void* context, + QUIC_CONNECTION_EVENT* event + ); + friend QUIC_STATUS msquicClientStreamCallbackProxy( + HQUIC stream, + void* context, + QUIC_STREAM_EVENT* event + ); + friend QUIC_STATUS msquicServerConnectionCallbackProxy( + HQUIC connection, + void* context, + QUIC_CONNECTION_EVENT* event + ); + friend QUIC_STATUS msquicServerStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event); + + QUIC_STATUS connectionCallback(HQUIC connection, const QUIC_CONNECTION_EVENT* event) { + auto clientId = reinterpret_cast(connection); + switch (event->Type) { + case QUIC_CONNECTION_EVENT_CONNECTED: + logger->log( + Logging::LEVEL_DEBUG, + "[client][{:X}] Connected", clientId + ); + + connectionStatus = QUIC_STATUS_SUCCESS; + break; + + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT: + + /* + * The transport has shut down the connection. + * Generally, this is the expected way for the connection to shut down with this + * protocol, since we let idle timeout kill the connection. + */ + if (event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status == QUIC_STATUS_CONNECTION_IDLE) { + logger->log( + Logging::LEVEL_DEBUG, "[client][{:X}] Successfully shutdown on idle", clientId + ); + } else { + logger->log( + Logging::LEVEL_DEBUG, + "[client][{:X}] Shut down by transport, 0x{:X}", clientId, + static_cast(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status) + ); + } + + break; + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER: + logger->log( + Logging::LEVEL_DEBUG, + "[client][{:X}] Shut down by peer 0x{:X}", clientId, + event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode + ); + + break; + case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: + logger->log( + Logging::LEVEL_DEBUG, "[client][{:X}] Connection shutdown complete", clientId + ); + + + connectionStatus = QUIC_STATUS_NOT_FOUND; + if (!event->SHUTDOWN_COMPLETE.AppCloseInProgress) { + logger->log( + Logging::LEVEL_DEBUG, "[client][{:X}] Connection closed", clientId + ); + connectionHandle.reset(); + } + break; + case QUIC_CONNECTION_EVENT_RESUMPTION_TICKET_RECEIVED: + logger->log( + Logging::LEVEL_DEBUG, + "[client][{:X}] Resumption ticket received ({} bytes):\n{}", clientId, + event->RESUMPTION_TICKET_RECEIVED.ResumptionTicketLength, std::ranges::to( + std::views::join_with( + std::ranges::transform_view( + std::ranges::subrange( + event->RESUMPTION_TICKET_RECEIVED.ResumptionTicket, + event->RESUMPTION_TICKET_RECEIVED.ResumptionTicket + event-> + RESUMPTION_TICKET_RECEIVED.ResumptionTicketLength + ), + [](std::uint8_t byte) { + return std::format("{:0>2X}", byte); + } + ), + std::string("") + ) + ) + ); + break; + default: + logger->log( + Logging::LEVEL_DEBUG, "[client][{:X}] Connection event [{}]", clientId, std::to_underlying(event->Type)); + + break; + } + return QUIC_STATUS_SUCCESS; + } + + void cleanupStreams() const { + auto MSQGR = MSQuicGlobal::get(); + if (MSQGR.has_value()) { + auto MSQG = MSQGR.value().get(); + for (auto& stream : connectionStreams) { + if (*stream != nullptr) { + (*MSQG.msQuicApiTable)->StreamShutdown(*stream, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0); + } + } + } + } + + QUIC_STATUS commonStreamCallback(HQUIC stream, const QUIC_STREAM_EVENT* event, const bool isClient) { + auto streamId = reinterpret_cast(stream); + auto side = isClient ? "client" : "server"; + switch (event->Type) { + case QUIC_STREAM_EVENT_SEND_COMPLETE: + { + // + // A previous StreamSend call has completed, and the context is being + // returned to the app. + // + auto dataId = reinterpret_cast(event->SEND_COMPLETE.ClientContext); + logger->log( + Logging::LEVEL_DEBUG, + "[{}][stream][{:X}] Data sent {} bytes [{:X} {}]", side, streamId, + static_cast(event->SEND_COMPLETE.ClientContext)->Length, + dataId, + queuedData.contains(event->SEND_COMPLETE.ClientContext) + ? "CACHE HIT" + : "CACHE MISS" + ); + std::flush(std::cout); + + queuedData.erase(event->SEND_COMPLETE.ClientContext); + } + break; + case QUIC_STREAM_EVENT_RECEIVE: + // + // Data was received from the peer on the stream. + // + logger->log( + Logging::LEVEL_DEBUG, + "[{}][stream][{:X}] Data received ({} bytes)", side, streamId, + event->RECEIVE.TotalBufferLength + ); + std::flush(std::cout); + + if (connectionStreamMap.contains(stream)) { + if (const auto ds = connectionStreamMap.at(stream).lock(); ds) { + for (uint32_t i = 0; i < event->RECEIVE.BufferCount; i++) { + const auto& [Length, Buffer] = event->RECEIVE.Buffers[i]; + ds->incomingData.insert( + std::end(ds->incomingData), + Buffer, Buffer + Length + ); + } + } else { + connectionStreamMap.erase(stream); + } + } + break; + case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: + { + logger->log( + Logging::LEVEL_DEBUG, + "[{}][stream][{:X}] Peer aborted reason: {}", side, streamId, + event->PEER_SEND_ABORTED.ErrorCode + ); + + + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + connectionStatus = MSQGR.error().status; + return MSQGR.error().status; + } + + const auto& MSQG = MSQGR.value().get(); + (*MSQG.msQuicApiTable)->StreamShutdown(stream, QUIC_STREAM_SHUTDOWN_FLAG_ABORT, 0); + } + break; + case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN: + { + logger->log( + Logging::LEVEL_DEBUG, "[{}][stream][{:X}] Peer shut down", side, streamId); + + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + connectionStatus = MSQGR.error().status; + return MSQGR.error().status; + } + + const auto& MSQG = MSQGR.value().get(); + (*MSQG.msQuicApiTable)->StreamShutdown(stream, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0); + } + break; + case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE: + logger->log( + Logging::LEVEL_DEBUG, "[{}][stream][{:X}] Shutdown done", side, streamId); + + if (connectionStreamMap.contains(stream)) { + if (auto ds = connectionStreamMap.at(stream).lock(); ds) { + ds->status = QUIC_STATUS_ABORTED; + } + connectionStreamMap.erase(stream); + } + + std::erase_if( + connectionStreams, + [&](const std::shared_ptr& strm) { + return *strm.get() == stream; + } + ); + break; + + default: + break; + } + return QUIC_STATUS_SUCCESS; + } + + QUIC_STATUS streamCallback(HQUIC stream, const QUIC_STREAM_EVENT* event) { + return commonStreamCallback(stream, event, true); + } + + QUIC_STATUS serverConnectionCallback(HQUIC connection, const QUIC_CONNECTION_EVENT* event) { + auto connectionId = reinterpret_cast(connection); + switch (event->Type) { + case QUIC_CONNECTION_EVENT_CONNECTED: + { + logger->log( + Logging::LEVEL_DEBUG, "[server][{:X}] Connected", connectionId); + + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + connectionStatus = MSQGR.error().status; + return MSQGR.error().status; + } + + const auto& MSQG = MSQGR.value().get(); + + (*MSQG.msQuicApiTable)->ConnectionSendResumptionTicket( + connection, QUIC_SEND_RESUMPTION_FLAG_NONE, 0, + nullptr + ); + + connectionStatus = QUIC_STATUS_SUCCESS; + } + + break; + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT: + if (event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status == QUIC_STATUS_CONNECTION_IDLE) { + logger->log( + Logging::LEVEL_DEBUG, "[server][{:X}] Successfully shutdown on idle", connectionId + ); + } else { + logger->log( + Logging::LEVEL_DEBUG, + "[server][{:X}] Shut down by transport, 0x{:X}", connectionId, + static_cast(event->SHUTDOWN_INITIATED_BY_TRANSPORT.Status) + ); + } + + break; + case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_PEER: + logger->log( + Logging::LEVEL_DEBUG, + "[server][{:X}] Shut down by peer 0x{:X}", connectionId, + event->SHUTDOWN_INITIATED_BY_PEER.ErrorCode + ); + + break; + case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE: + logger->log( + Logging::LEVEL_DEBUG,"[server][{:X}] Shutdown complete", connectionId); + + + connectionStatus = QUIC_STATUS_NOT_FOUND; + connectionHandle.reset(); + break; + case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED: + { + logger->log( + Logging::LEVEL_DEBUG, + "[server][{:X}] Peer stream started ({:X})", connectionId, + reinterpret_cast(event->PEER_STREAM_STARTED.Stream) + ); + + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + connectionStatus = MSQGR.error().status; + return MSQGR.error().status; + } + + const auto& MSQG = MSQGR.value().get(); + + std::shared_ptr newStream( + new HQUIC(event->PEER_STREAM_STARTED.Stream), + [&](const HQUIC* connectionStream) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value() && *connectionStream != nullptr) { + auto table = *MSQG.msQuicApiTable; + table->SetCallbackHandler( + *connectionStream, nullptr, this + ); + table->StreamClose(*connectionStream); + } + + delete connectionStream; + } + ); + + connectionStreams.emplace_back(newStream); + + if (!onRemoteStreamCallbacks.empty()) { + std::shared_ptr client( + new MSQuicStream( + newStream, std::shared_ptr( + nullptr, [&, newStream](void*) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value() && *newStream != nullptr) { + (*msquic.value().get().msQuicApiTable)->StreamShutdown( + *newStream, QUIC_STREAM_SHUTDOWN_FLAG_GRACEFUL, 0 + ); + } else { std::erase(connectionStreams, newStream); } + } + ), shared_from_this(), QUIC_STATUS_SUCCESS + ), [](const MSQuicStream* ptr) { + delete ptr; + } + ); + + connectionStreamMap.emplace(*newStream, client); + for (auto& callback : onRemoteStreamCallbacks) { + if (const auto cb = callback.lock(); cb) { + cb->operator()(client, shared_from_this()); + } + } + } + + (*MSQG.msQuicApiTable)->SetCallbackHandler( + event->PEER_STREAM_STARTED.Stream, + msquicServerStreamCallbackProxy, this + ); + } + break; + case QUIC_CONNECTION_EVENT_RESUMED: + logger->log( + Logging::LEVEL_DEBUG, "[server][{:X}] Connection resumed!", connectionId + ); + + + break; + default: + logger->log( + Logging::LEVEL_DEBUG, "[server][{:X}] Connection event [{}]", connectionId, std::to_underlying(event->Type)); + + break; + } + return QUIC_STATUS_SUCCESS; + } + + QUIC_STATUS serverStreamCallback(HQUIC stream, const QUIC_STREAM_EVENT* event) { + return commonStreamCallback(stream, event, false); + } + }; + + QUIC_STATUS msquicClientConnectionCallbackProxy(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event) { + return static_cast(context)->connectionCallback(connection, event); + } + + QUIC_STATUS msquicClientStreamCallbackProxy(HQUIC stream, void* context, QUIC_STREAM_EVENT* event) { + return static_cast(context)->streamCallback(stream, event); + } + + QUIC_STATUS msquicServerConnectionCallbackProxy(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event) { + return static_cast(context)->serverConnectionCallback(connection, event); + } + + QUIC_STATUS msquicServerStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event) { + return static_cast(context)->serverStreamCallback(connection, event); + } + +} diff --git a/common/connection/src/MSQuicError.cppm b/common/connection/src/MSQuicError.cppm new file mode 100644 index 0000000..05d08c4 --- /dev/null +++ b/common/connection/src/MSQuicError.cppm @@ -0,0 +1,17 @@ +export module MMO.DataConnection:MSQuicError; + +import ; +import std; +namespace MMO::Networking::MSQUIC { + + export class MSQuicError : public std::runtime_error { + public: + QUIC_STATUS status = QUIC_STATUS_SUCCESS; + + [[nodiscard]] explicit + MSQuicError(QUIC_STATUS status) : status(status), + std::runtime_error( + std::format("MSQuic error: 0x{:X}", static_cast(status)) + ) { } + }; +} \ No newline at end of file diff --git a/common/connection/src/MSQuicGlobal.cppm b/common/connection/src/MSQuicGlobal.cppm new file mode 100644 index 0000000..8f851c5 --- /dev/null +++ b/common/connection/src/MSQuicGlobal.cppm @@ -0,0 +1,62 @@ +export module MMO.DataConnection:MSQuicGlobal; + +import ; +import std; +import :MSQuicError; + +namespace MMO::Networking::MSQUIC { + export class MSQuicGlobal { + public: + std::shared_ptr msQuicApiTable = nullptr; + std::shared_ptr registration; + + const QUIC_BUFFER applicationLayerProtocolNegotiationName = { sizeof("sample") - 1, (uint8_t*) "sample" }; + + void reset() { + std::println("Cleaning up MSQuic..."); + + + registration.reset(); + msQuicApiTable.reset(); + } + + [[nodiscard]] + static std::expected, MSQuicError> get() { + static MSQuicGlobal instance; + if (!instance.msQuicApiTable) { + instance.msQuicApiTable = std::shared_ptr( + new const QUIC_API_TABLE*(nullptr), [](const QUIC_API_TABLE** table) { + std::println("Cleaning up MSQuic..."); + if (*table != nullptr) { + MsQuicClose(*table); + } + delete table; + } + ); + if (QUIC_STATUS status = MsQuicOpen2(instance.msQuicApiTable.get()); QUIC_FAILED(status)) { + return std::unexpected(MSQuicError(status)); + } + + + instance.registration.reset( + new HQUIC(nullptr), + [apiTable = instance.msQuicApiTable](const HQUIC* registration) { + if (*registration != nullptr) { + (*apiTable.get())->RegistrationClose(*registration); + } + + delete registration; + } + ); + if (QUIC_STATUS status = (*instance.msQuicApiTable.get())->RegistrationOpen( + nullptr, instance.registration.get() + ); + QUIC_FAILED(status)) { + return std::unexpected(MSQuicError(status)); + } + } + + return instance; + } + }; +} diff --git a/common/connection/src/MSQuicServer.cppm b/common/connection/src/MSQuicServer.cppm new file mode 100644 index 0000000..3828b48 --- /dev/null +++ b/common/connection/src/MSQuicServer.cppm @@ -0,0 +1,316 @@ +export module MMO.DataConnection:MSQuicServer; + +import ; +import std; +import :MSQuicConnection; +import :MSQuicGlobal; +import :MSQuicError; +import MMO.Logging; + +namespace MMO::Networking::MSQUIC { + export class MSQuicServer : public std::enable_shared_from_this { + public: + std::shared_ptr logger = MMO::Logging::DEFAULT_LOGGER + ? MMO::Logging::DEFAULT_LOGGER + : std::make_shared(); + std::shared_ptr configuration = nullptr; + std::shared_ptr listenerHandle = nullptr; + const std::string hostAddress; + const uint16_t hostPort; + + std::vector)>>> onConnectionCallbacks; + + bool starting = false; + bool listening = false; + bool stopping = false; + + public: + [[nodiscard]] + bool isStarting() const { + return starting; + } + + [[nodiscard]] + bool isListening() const { + return listening; + } + + [[nodiscard]] + bool isStopping() const { + return stopping; + } + + private : + MSQuicServer(std::shared_ptr configuration, std::string hostAddress, const uint16_t hostPort) : + configuration(std::move(configuration)), hostAddress(std::move(hostAddress)), hostPort(hostPort) { } + + public: + static void defaultSettings(QUIC_SETTINGS& settings, QUIC_CREDENTIAL_CONFIG& credConfig) { + settings.IdleTimeoutMs = 5000; + settings.IsSet.IdleTimeoutMs = TRUE; + + settings.ServerResumptionLevel = QUIC_SERVER_RESUME_AND_ZERORTT; + settings.IsSet.ServerResumptionLevel = TRUE; + + settings.PeerBidiStreamCount = 1; + settings.IsSet.PeerBidiStreamCount = TRUE; + } + + + [[nodiscard]] + static std::expected, MSQuicError> hostFrom( + QUIC_SETTINGS& settings, + QUIC_CREDENTIAL_CONFIG& credConfig, + const uint16_t hostPort, + const std::string& hostAddress = "0.0.0.0" + ) { + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + return std::unexpected(MSQGR.error()); + } + + const auto& MSQG = MSQGR.value().get(); + const QUIC_API_TABLE* MsQuic = *MSQG.msQuicApiTable; + + std::shared_ptr configuration( + new HQUIC(nullptr), [](const HQUIC* configuration) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value() && *configuration != nullptr) { + (*msquic.value().get().msQuicApiTable)->ConfigurationClose(*configuration); + } + + delete configuration; + } + ); + if (QUIC_STATUS status = MsQuic->ConfigurationOpen( + *MSQG.registration, + &MSQG.applicationLayerProtocolNegotiationName, 1, + &settings, sizeof(settings), nullptr, + configuration.get() + ); + QUIC_FAILED(status)) { + return std::unexpected(MSQuicError(status)); + } + + if (QUIC_STATUS status = MsQuic->ConfigurationLoadCredential(*configuration.get(), &credConfig); + QUIC_FAILED(status)) { + return std::unexpected(MSQuicError(status)); + } + + return std::shared_ptr( + new MSQuicServer(configuration, hostAddress, hostPort), [](const auto* v) { + delete v; + } + ); + } + + [[nodiscard]] + static std::expected, MSQuicError> hostFrom( + QUIC_CREDENTIAL_CONFIG& credentialConfig, + const uint16_t hostPort, + const std::string& hostAddress = "0.0.0.0" + ) { + QUIC_SETTINGS settings{ 0 }; + defaultSettings(settings, credentialConfig); + return hostFrom(settings, credentialConfig, hostPort, hostAddress); + } + + [[nodiscard]] + std::optional startListening() { + if (!listening && !starting) { + starting = true; + + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + starting = false; + return MSQGR.error(); + } + + const auto& MSQG = MSQGR.value().get(); + + + listenerHandle.reset( + new HQUIC(nullptr), [](const HQUIC* listenerHandle) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value() && *listenerHandle != nullptr) { + (*msquic.value().get().msQuicApiTable)->ListenerClose(*listenerHandle); + } + + delete listenerHandle; + } + ); + + if (const QUIC_STATUS status = (*MSQG.msQuicApiTable)->ListenerOpen( + *MSQG.registration, + msquicServerListeningCallbackProxy, this, + listenerHandle.get() + ); + QUIC_FAILED(status)) { + starting = false; + return MSQuicError(status); + } + + QUIC_ADDR address = { 0 }; + QuicAddrSetFamily(&address, QUIC_ADDRESS_FAMILY_UNSPEC); + QuicAddrFromString(hostAddress.c_str(), hostPort, &address); + + if (const QUIC_STATUS status = (*MSQG.msQuicApiTable)->ListenerStart( + *listenerHandle.get(), &MSQG.applicationLayerProtocolNegotiationName, 1, &address + ); + QUIC_FAILED(status)) { + starting = false; + return MSQuicError(status); + } + + listening = true; + } + + return std::nullopt; + } + + [[nodiscard]] + std::optional stopListening() { + if (listening && !stopping) { + stopping = true; + + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + return MSQGR.error(); + } + + const auto& MSQG = MSQGR.value().get(); + + (*MSQG.msQuicApiTable)->ListenerStop(*listenerHandle.get()); + + listenerHandle.reset(); + } + + return std::nullopt; + } + + void addReceiveCallback(const std::weak_ptr)>>& callback) { + onConnectionCallbacks.emplace_back(callback); + } + + private: + friend QUIC_STATUS msquicServerListeningCallbackProxy( + HQUIC connection, + void* context, + QUIC_LISTENER_EVENT* event + ); + friend QUIC_STATUS msquicServerConnectionCallbackProxy( + HQUIC connection, + void* context, + QUIC_CONNECTION_EVENT* event + ); + friend QUIC_STATUS msquicServerStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event); + + QUIC_STATUS serverListenerCallback(HQUIC connection, QUIC_LISTENER_EVENT* event) { + auto connectionId = reinterpret_cast(connection); + + QUIC_STATUS Status = QUIC_STATUS_NOT_SUPPORTED; + switch (event->Type) { + case QUIC_LISTENER_EVENT_NEW_CONNECTION: + { + QUIC_ADDR_STR address = { 0 }; + QuicAddrToString(event->NEW_CONNECTION.Info->RemoteAddress, &address); + + const std::string_view remoteAddress = address.Address; + const auto indexSplit = remoteAddress.find_last_of(':'); + auto remoteAddressPort = static_cast(std::strtol( + remoteAddress.substr(indexSplit + 1).data(), nullptr, 10 + )); + std::string remoteAddressIp(remoteAddress.substr(0, indexSplit)); + + logger->log( + Logging::LEVEL_DEBUG, + "[server][{:X}][{}] New connection from {}:{}", connectionId, + reinterpret_cast(event->NEW_CONNECTION.Connection), + remoteAddressIp, remoteAddressPort + ); + + + auto MSQGR = MSQuicGlobal::get(); + + if (!MSQGR.has_value()) { + listening = false; + return MSQGR.error().status; + } + + const auto& MSQG = MSQGR.value().get(); + + const std::shared_ptr client( + new MSQuicConnection{ + configuration, + remoteAddressIp, + remoteAddressPort + }, [](MSQuicConnection* c) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value()) { + auto MSQG2 = msquic->get(); + for (auto& stream : c->connectionStreams) { + (*MSQG2.msQuicApiTable)->StreamShutdown( + *stream, + QUIC_STREAM_SHUTDOWN_FLAG_ABORT | QUIC_STREAM_SHUTDOWN_FLAG_IMMEDIATE, 0 + ); + } + if (c->connectionHandle) { + const auto handle = *c->connectionHandle.get(); + c->connectionHandle.reset(); + (*MSQG2.msQuicApiTable)->SetCallbackHandler(handle, nullptr, nullptr); + } + } + delete c; + } + ); + + client->connectionHandle.reset( + new HQUIC(event->NEW_CONNECTION.Connection), + [](const HQUIC* connectionHandle) { + auto msquic = MSQuicGlobal::get(); + if (msquic.has_value() && *connectionHandle != nullptr) { + (*msquic.value().get().msQuicApiTable)->ConnectionClose( + *connectionHandle + ); + } + + delete connectionHandle; + } + ); + + (*MSQG.msQuicApiTable)->SetCallbackHandler( + event->NEW_CONNECTION.Connection, + msquicServerConnectionCallbackProxy, client.get() + ); + Status = (*MSQG.msQuicApiTable)->ConnectionSetConfiguration( + event->NEW_CONNECTION.Connection, *configuration.get() + ); + + for (auto& callback : onConnectionCallbacks) { + if (const auto cb = callback.lock(); cb) { + cb->operator()(client); + } + } + } + break; + case QUIC_LISTENER_EVENT_STOP_COMPLETE: + { + listenerHandle.reset(); + listening = false; + stopping = false; + } + default: + break; + } + return Status; + } + }; + + QUIC_STATUS msquicServerListeningCallbackProxy(HQUIC connection, void* context, QUIC_LISTENER_EVENT* event) { + return static_cast(context)->serverListenerCallback(connection, event); + } + +} diff --git a/common/connection/tests/DataConnection_test.cppm b/common/connection/tests/DataConnection_test.cppm new file mode 100644 index 0000000..617452b --- /dev/null +++ b/common/connection/tests/DataConnection_test.cppm @@ -0,0 +1,266 @@ +module; + +#include +#include + +#include + +export module DataConnection_test; +import MMO.DataConnection; +import std; + +// class DataConnection : public ::testing::Test { +// void TearDown() override { +// MMO::Networking::MSQUIC::MSQuicGlobal::get()->get().reset(); +// } +// }; + +TEST(DataConnection, ClientSettings) { + MMO::Networking::NetworkSettings settings{0}; + MMO::Networking::NetworkCredentials credConfig{ + .Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_NONE, + .Flags = MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_CLIENT | MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION + }; + + MMO::Networking::NetworkingConnection::defaultSettings(settings, credConfig); + auto res = MMO::Networking::NetworkingConnection::connectTo(settings, credConfig, "127.0.0.1", 2524); + EXPECT_TRUE(res.has_value()); +} + +TEST(DataConnection, ServerSettings) { + MMO::Networking::NetworkSettings settings{0}; + MMO::Networking::NetworkCertificate certFile{ + .PrivateKeyFile = "../key.pem", + .CertificateFile = "../cert.pem", + }; + MMO::Networking::NetworkCredentials credConfig{ + .Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE, + .CertificateFile = &certFile + }; + + MMO::Networking::NetworkingServer::defaultSettings(settings, credConfig); + auto res = MMO::Networking::NetworkingServer::hostFrom(settings, credConfig, 2524, "127.0.0.1"); + EXPECT_TRUE(res.has_value()); +} + +TEST(DataConnection, ConnectToRemote) { + MMO::Networking::NetworkSettings settings{0}; + MMO::Networking::NetworkCredentials credConfig{ + .Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_NONE, + .Flags = MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_CLIENT | MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION + }; + MMO::Networking::NetworkingConnection::defaultSettings(settings, credConfig); + auto res = MMO::Networking::NetworkingConnection::connectTo(settings, credConfig, "127.0.0.1", 2524); + EXPECT_TRUE(res.has_value()); + + auto connection = res.value(); + + auto result = connection->establishConnection(); + + EXPECT_FALSE(result.has_value()); +} + +TEST(DataConnection, ListenForConnections) { + MMO::Networking::NetworkSettings settings{0}; + MMO::Networking::NetworkCertificate certFile{ + .PrivateKeyFile = "../key.pem", + .CertificateFile = "../cert.pem", + }; + MMO::Networking::NetworkCredentials credConfig{ + .Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE, + .CertificateFile = &certFile + }; + MMO::Networking::NetworkingServer::defaultSettings(settings, credConfig); + auto res = MMO::Networking::NetworkingServer::hostFrom(settings, credConfig, 2524, "127.0.0.1"); + EXPECT_TRUE(res.has_value()); + + auto server = res.value(); + + EXPECT_THAT(server->startListening(), testing::Eq(std::nullopt)); +} + + +TEST(DataConnection, EstablishConnection) { + auto timeout = std::chrono::seconds(5); + auto start = std::chrono::high_resolution_clock::now(); + + + MMO::Networking::NetworkSettings serverSettings{0}; + MMO::Networking::NetworkCertificate serverCertFile{ + .PrivateKeyFile = "../key.pem", + .CertificateFile = "../cert.pem", + }; + MMO::Networking::NetworkCredentials serverCredConfig{ + .Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE, + .CertificateFile = &serverCertFile + }; + MMO::Networking::NetworkingServer::defaultSettings(serverSettings, serverCredConfig); + auto serverRes = MMO::Networking::NetworkingServer::hostFrom(serverSettings, serverCredConfig, 2524, + "0.0.0.0"); + EXPECT_TRUE(serverRes.has_value()); + + auto server = serverRes.value(); + + EXPECT_THAT(server->startListening(), testing::Eq(std::nullopt)); + + std::println("Binding listener..."); + std::fflush(stdout); + + std::shared_ptr serverConnection; + + auto serverCB = std::make_shared)>>( + [&serverConnection](auto data) { + serverConnection = std::move(data); + }); + + server->addReceiveCallback(serverCB); + + std::println("Creating client->.."); + std::fflush(stdout); + + MMO::Networking::NetworkSettings clientSettings{0}; + MMO::Networking::NetworkCredentials clientCredConfig{ + .Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_NONE, + .Flags = MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_CLIENT | MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION + }; + MMO::Networking::NetworkingConnection::defaultSettings(clientSettings, clientCredConfig); + auto clientRes = MMO::Networking::NetworkingConnection::connectTo(clientSettings, clientCredConfig, "127.0.0.1", + 2524); + EXPECT_TRUE(clientRes.has_value()); + + auto client = clientRes.value(); + + std::println("Connecting to server..."); + std::fflush(stdout); + EXPECT_FALSE(client->establishConnection().has_value()); + + while (!client->isConnected()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (std::chrono::high_resolution_clock::now() - start > timeout) { + FAIL() << "Connection timed out"; + } + } + + EXPECT_TRUE(client->isConnected()); + EXPECT_TRUE(serverConnection != nullptr); + + + while (!serverConnection->isConnected()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (std::chrono::high_resolution_clock::now() - start > timeout) { + FAIL() << "Connection timed out"; + } + } + EXPECT_TRUE(serverConnection->isConnected()); + +} + + +TEST(DataConnection, SendData) { + auto timeout = std::chrono::seconds(5); + auto start = std::chrono::high_resolution_clock::now(); + + MMO::Networking::NetworkSettings serverSettings{0}; + MMO::Networking::NetworkCertificate serverCertFile{ + .PrivateKeyFile = "../key.pem", + .CertificateFile = "../cert.pem", + }; + MMO::Networking::NetworkCredentials serverCredConfig{ + .Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_CERTIFICATE_FILE, + .CertificateFile = &serverCertFile + }; + MMO::Networking::NetworkingServer::defaultSettings(serverSettings, serverCredConfig); + auto serverRes = MMO::Networking::NetworkingServer::hostFrom(serverSettings, serverCredConfig, 2524, + "0.0.0.0"); + EXPECT_TRUE(serverRes.has_value()); + + auto server = serverRes.value(); + + EXPECT_THAT(server->startListening(), testing::Eq(std::nullopt)); + + std::println("Binding listener..."); + std::fflush(stdout); + + std::shared_ptr serverConnection; + + auto serverCB = std::make_shared)>>( + [&serverConnection](auto data) { + serverConnection = std::move(data); + }); + + server->addReceiveCallback(serverCB); + + + std::println("Creating client->.."); + std::fflush(stdout); + + MMO::Networking::NetworkSettings clientSettings{0}; + MMO::Networking::NetworkCredentials clientCredConfig{ + .Type = MMO::Networking::Additional::CredentialType::QUIC_CREDENTIAL_TYPE_NONE, + .Flags = MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_CLIENT | MMO::Networking::Additional::CredentialFlags::QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION + }; + MMO::Networking::NetworkingConnection::defaultSettings(clientSettings, clientCredConfig); + auto clientRes = MMO::Networking::NetworkingConnection::connectTo(clientSettings, clientCredConfig, "127.0.0.1", + 2524); + EXPECT_TRUE(clientRes.has_value()); + + auto client = clientRes.value(); + + std::println("Connecting to server..."); + std::fflush(stdout); + EXPECT_FALSE(client->establishConnection().has_value()); + + while (!client->isConnected()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (std::chrono::high_resolution_clock::now() - start > timeout) { + FAIL() << "Connection timed out"; + } + } + + EXPECT_TRUE(client->isConnected()); + EXPECT_TRUE(serverConnection != nullptr); + + + while (!serverConnection->isConnected()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (std::chrono::high_resolution_clock::now() - start > timeout) { + FAIL() << "Connection timed out"; + } + } + EXPECT_TRUE(serverConnection->isConnected()); + + std::println("Sending data..."); + std::fflush(stdout); + + std::shared_ptr serverStream; + + auto serverStreamCB = std::make_shared, std::shared_ptr)>>( + [&serverStream](const auto& dataLane, const auto&) { + serverStream = dataLane; + }); + + serverConnection->addRemoteStreamCallbacks(serverStreamCB); + + // auto streamRes = client->createStream(); + // EXPECT_TRUE(streamRes.has_value()); + // auto stream = streamRes.value(); + std::vector testData = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + auto sendResult = client->send(testData); + EXPECT_FALSE(sendResult.has_value()); + + std::println("Waiting for data to be received..."); + std::fflush(stdout); + while (!serverStream || !serverStream->hasData()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + if (std::chrono::high_resolution_clock::now() - start > timeout) { + FAIL() << "No data received"; + } + } + + EXPECT_TRUE(serverStream->hasData()); + + auto receivedData = serverStream->retrieveAndResetData(); + + EXPECT_THAT(receivedData, testing::ContainerEq(testData)); + +} diff --git a/common/connection/tests/sanitizers.cpp b/common/connection/tests/sanitizers.cpp new file mode 100644 index 0000000..b9c673b --- /dev/null +++ b/common/connection/tests/sanitizers.cpp @@ -0,0 +1,14 @@ + +#include + +extern "C" { + void __ubsan_on_report() { + FAIL() << "Encountered an undefined behavior sanitizer error"; + } + void __asan_on_error() { + FAIL() << "Encountered an address sanitizer error"; + } + void __tsan_on_report() { + FAIL() << "Encountered a thread sanitizer error"; + } +} \ No newline at end of file diff --git a/common/logging/CMakeLists.txt b/common/logging/CMakeLists.txt new file mode 100644 index 0000000..50eda4e --- /dev/null +++ b/common/logging/CMakeLists.txt @@ -0,0 +1,26 @@ +project(logging) + + +set(${PROJECT_NAME}_src + src/logging.cppm) +# this is the "object library" target: compiles the sources only once +add_library(${PROJECT_NAME}_lib STATIC ${${PROJECT_NAME}_src}) +target_include_directories(${PROJECT_NAME}_lib PUBLIC ./src) + +find_package(spdlog CONFIG REQUIRED) +target_link_libraries(${PROJECT_NAME}_lib PUBLIC spdlog::spdlog) +# Setup tests +enable_testing() + +find_package(GTest CONFIG REQUIRED) +include(GoogleTest) + +file(GLOB_RECURSE tests_${PROJECT_NAME}_src CONFIGURE_DEPENDS tests/*.cppm tests/*/*.cppm) + +add_executable(tests_${PROJECT_NAME} ${tests_${PROJECT_NAME}_src} + tests/sanitizers.cpp) +target_link_libraries(tests_${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main GTest::gmock GTest::gmock_main ${PROJECT_NAME}_lib) +add_test(AllTestsIn${PROJECT_NAME} tests_${PROJECT_NAME}) + +gtest_discover_tests(tests_${PROJECT_NAME}) + diff --git a/common/logging/src/logging.cppm b/common/logging/src/logging.cppm new file mode 100644 index 0000000..4f95ee0 --- /dev/null +++ b/common/logging/src/logging.cppm @@ -0,0 +1,105 @@ +export module MMO.Logging; + +import ; +import std; + +namespace MMO::Logging { + export class Logger { + public: + virtual ~Logger() = default; + + + virtual void log(uint8_t level, const std::string& message) = 0; + + template + void log(uint8_t level, const std::format_string<_Types...> _Fmt, _Types&&... _Args) { + log(level, std::format(_Fmt, std::forward<_Types>(_Args)...)); + } + }; + + export constexpr uint8_t LEVEL_OFF = 0; + export constexpr uint8_t LEVEL_TRACE = 1; + export constexpr uint8_t LEVEL_DEBUG = 2; + export constexpr uint8_t LEVEL_INFO = 3; + export constexpr uint8_t LEVEL_WARN = 4; + export constexpr uint8_t LEVEL_ERROR = 5; + export constexpr uint8_t LEVEL_FATAL = 6; + + export template + concept LoggerConcept = requires(T a, uint8_t level, const std::string& message) { + { a.log(level, message) } -> std::same_as; + }; + + export class NullLogger : public Logger { + public: + void log(const uint8_t level, const std::string& message) override { } + }; + + export class ConsoleLogger : public Logger { + public: + uint8_t activeLevel = LEVEL_INFO; + + explicit ConsoleLogger() = default; + explicit ConsoleLogger(const uint8_t level) : activeLevel(level) { } + + void log(const uint8_t level, const std::string& message) override { + if (level < activeLevel) { + return; + } + switch (level) { + case LEVEL_TRACE: + std::println("[TRACE] {}", message); + break; + case LEVEL_DEBUG: + std::println("[DEBUG] {}", message); + break; + case LEVEL_INFO: + std::println("[INFO] {}", message); + break; + case LEVEL_WARN: + std::println("[WARN] {}", message); + break; + case LEVEL_ERROR: + std::println("[ERROR] {}", message); + break; + case LEVEL_FATAL: + std::println("[FATAL] {}", message); + break; + default: + std::println("[UNKNOWN ({})] {}", level, message); + break; + } + } + }; + + export class SPDLogger : public Logger { + public: + void log(const uint8_t level, const std::string& message) override { + switch (level) { + case LEVEL_TRACE: + spdlog::trace(message); + break; + case LEVEL_DEBUG: + spdlog::debug(message); + break; + case LEVEL_INFO: + spdlog::info(message); + break; + case LEVEL_WARN: + spdlog::warn(message); + break; + case LEVEL_ERROR: + spdlog::error(message); + break; + case LEVEL_FATAL: + spdlog::critical(message); + break; + default: + spdlog::error("Unknown log level: {}", level); + break; + } + } + }; + + export std::shared_ptr DEFAULT_LOGGER = std::make_shared(); +} diff --git a/common/logging/tests/logging_test.cppm b/common/logging/tests/logging_test.cppm new file mode 100644 index 0000000..a28f3ea --- /dev/null +++ b/common/logging/tests/logging_test.cppm @@ -0,0 +1,10 @@ +module; + +#include +#include + +#include + +export module Packet_test; +import MMO.Packet; +import std; diff --git a/common/logging/tests/sanitizers.cpp b/common/logging/tests/sanitizers.cpp new file mode 100644 index 0000000..b9c673b --- /dev/null +++ b/common/logging/tests/sanitizers.cpp @@ -0,0 +1,14 @@ + +#include + +extern "C" { + void __ubsan_on_report() { + FAIL() << "Encountered an undefined behavior sanitizer error"; + } + void __asan_on_error() { + FAIL() << "Encountered an address sanitizer error"; + } + void __tsan_on_report() { + FAIL() << "Encountered a thread sanitizer error"; + } +} \ No newline at end of file diff --git a/common/packet/CMakeLists.txt b/common/packet/CMakeLists.txt new file mode 100644 index 0000000..9866f0f --- /dev/null +++ b/common/packet/CMakeLists.txt @@ -0,0 +1,26 @@ +project(packet) + + +set(${PROJECT_NAME}_src + src/Packet.cppm) +# this is the "object library" target: compiles the sources only once +add_library(${PROJECT_NAME}_lib STATIC ${${PROJECT_NAME}_src}) +target_include_directories(${PROJECT_NAME}_lib PUBLIC ./src) + +find_package(cryptopp CONFIG REQUIRED) +target_link_libraries(${PROJECT_NAME}_lib PRIVATE cryptopp::cryptopp) +# Setup tests +enable_testing() + +find_package(GTest CONFIG REQUIRED) +include(GoogleTest) + +file(GLOB_RECURSE tests_${PROJECT_NAME}_src CONFIGURE_DEPENDS tests/*.cppm tests/*/*.cppm) + +add_executable(tests_${PROJECT_NAME} ${tests_${PROJECT_NAME}_src} + tests/sanitizers.cpp) +target_link_libraries(tests_${PROJECT_NAME} PRIVATE GTest::gtest GTest::gtest_main GTest::gmock GTest::gmock_main ${PROJECT_NAME}_lib) +add_test(AllTestsIn${PROJECT_NAME} tests_${PROJECT_NAME}) + +gtest_discover_tests(tests_${PROJECT_NAME}) + diff --git a/common/packet/src/Packet.cppm b/common/packet/src/Packet.cppm new file mode 100644 index 0000000..54be5f7 --- /dev/null +++ b/common/packet/src/Packet.cppm @@ -0,0 +1,629 @@ +module; + +#include + +export module MMO.Packet; +import std; +import ; + +namespace MMO::Networking { + inline namespace v1 { + template + constexpr T enforceLittleEndian(T value) { + if constexpr (std::endian::native == std::endian::big) { + return std::byteswap(value); + } else { + return value; + } + } + + /** + * @brief Container concept + * + * A type that meets the requirements of the Container concept must have an + * emplace() function that matches a vector's emplace() function. + * The type must also have a size() function that returns the number of elements in the container. + * The type must also have a begin() function that returns an iterator to the beginning of the container. + * The type must also have an end() function that returns an iterator to the end of the container. + */ + export template + concept Container = requires(T t) { + { t.emplace(t.begin(), std::declval()) }; + { std::size(t) } -> std::convertible_to; + { std::begin(t) }; + { std::end(t) }; + }; + + /** + * @brief Encode a value + * + * Encodes the given value into a sequence of bytes using variable length + * encoding, and stores the resulting bytes in the given container. + * + * @param value The value to encode + * @param encoded The container to store the encoded bytes in + * @param offset The offset to start encoding at (-1 to start at the end of the container) + * @return The number of bytes added to the container + */ + + // export template + // size_t encodeValue(T value, C& encoded, std::int32_t offset = -1) = delete; + + /** + * @brief Variable length encode an integral value + * + * Encodes the given integral value into a sequence of bytes using + * variable length encoding, and stores the resulting bytes in the given + * container. + * + * @param value The value to encode + * @param encoded The container to store the encoded bytes in + * @return The number of bytes added to the container + */ + export template + size_t encodeValue(const T valueO, C& encoded, std::int32_t offset = -1) { + T value = enforceLittleEndian(valueO); + + auto insertPos = offset < 0 ? std::size(encoded) + offset + 1 : offset; + + size_t count = 0; + + T comparator = T(value < 0); + + std::uint8_t data; + + if constexpr (std::signed_integral) { + data = value & 0b00111111u; + if (value < 0) { + data |= 0b01000000u; + } + value >>= 6; + } else { + data = value & 0b01111111u; + value >>= 7; + } + + while (value + comparator) { + auto writeLocation = std::begin(encoded); + std::advance(writeLocation, insertPos + count); + encoded.emplace(writeLocation, data); + + + ++count; + + data = value & 0b01111111u; + value >>= 7; + } + + auto writeLocation = std::begin(encoded); + std::advance(writeLocation, insertPos + count); + ++count; + encoded.emplace(writeLocation, static_cast(data | 0b10000000u)); + + return count; + } + + export template + size_t encodeValue(const float value, C& encoded, std::int32_t offset = -1) { + // std::numeric_limits::digits is 24 + // std::numeric_limits::max_exponent is 128 (we convert this to 127) + // std::numeric_limits::min_exponent is -125 (we convert this to -126) + // `exp` is -128 (IEEE 754 this would be -1) is NaN when `mantisa` is 2 + // `exp` is -128 (IEEE 754 this would be -1) is Inf when `mantisa` is 0 or 1 (uses signbit in IEEE 754 but this would bloat the encoding since it would be part of `sign`) + // The final exp needs to be adjusted by adding 1 and subtracting 24 to get the correct exponent + size_t count = 0; + + auto insertPos = offset < 0 ? std::size(encoded) + offset + 1 : offset; + + if (std::isnan(value) || std::isinf(value)) { + count += encodeValue( + static_cast(-128), encoded, + static_cast(insertPos + count) + ); + } + + if (std::isnan(value)) { + count += encodeValue( + 2, encoded, + static_cast(insertPos + count) + ); + return count; + } + if (std::isinf(value)) { + count += encodeValue( + value < 0 ? 0 : 1, + encoded, + static_cast(insertPos + count) + ); + return count; + } + + int exp = 0; + const auto frac = std::frexp(value, &exp); + + // This adjustment is necessary to get the correct exponent. `exp` will be between 128 to -125, + // so this adjustment will force it back into being a valid int8_t + + const int adjExp = exp - 1; + count = encodeValue( + static_cast(adjExp), encoded, + static_cast(insertPos + count) + ); + + // If the exponent is 0, then the mantissa is 0, so we can return early + if (exp == 0) { + return count; + } + + const auto sign = std::scalbn(frac, std::numeric_limits::digits); + + count += encodeValue( + static_cast(sign), encoded, + static_cast(insertPos + count) + ); + return count; + } + + export template + size_t encodeValue(const double value, C& encoded, std::int32_t offset = -1) { + // std::numeric_limits::digits is 53 + // std::numeric_limits::max_exponent is 1024 + // std::numeric_limits::min_exponent is -1021 + // `exp` is -2048 (IEEE 754 this would be -1) is NaN when `mantisa` is 2 + // `exp` is -2048 (IEEE 754 this would be -1) is Inf when `mantisa` is 0 or 1 (uses signbit in IEEE 754 but this would bloat the encoding since it would be part of `sign`) + // The final exp needs to be adjusted by adding 1 and subtracting 24 to get the correct exponent + size_t count = 0; + + auto insertPos = offset < 0 ? std::size(encoded) + offset + 1 : offset; + + if (std::isnan(value) || std::isinf(value)) { + count += encodeValue( + static_cast(-2048), encoded, + static_cast(insertPos + count) + ); + } + + if (std::isnan(value)) { + count += encodeValue( + static_cast(2), encoded, + static_cast(insertPos + count) + ); + return count; + } else if (std::isinf(value)) { + if (value < 0) { + count += encodeValue( + static_cast(0), encoded, + static_cast(insertPos + count) + ); + } else { + count += encodeValue( + static_cast(1), encoded, + static_cast(insertPos + count) + ); + } + return count; + } + + int exp = 0; + const auto frac = std::frexp(value, &exp); + + // This adjustment is necessary to get the correct exponent. `exp` will be between 128 to -125, + // so this adjustment will force it back into being a valid int8_t + + count = encodeValue(exp, encoded, static_cast(insertPos + count)); + + // If the exponent is 0, then the mantissa is 0, so we can return early + if (exp == 0) { + return count; + } + + const auto sign = std::scalbn(frac, std::numeric_limits::digits); + + count += encodeValue( + static_cast(sign), encoded, + static_cast(insertPos + count) + ); + return count; + } + + export template + size_t encodeValue(const Box& value, C& encoded, std::int32_t offset = -1) { + size_t count = encodeValue(static_cast(std::size(value)), encoded, offset); + + for (const auto& data : value) { + auto adjustedOffset = offset < 0 + ? std::min(offset + static_cast(count), -1) + : offset + static_cast(count); + count += encodeValue(data, encoded, adjustedOffset); + } + + return count; + } + + export class ParseError : public std::runtime_error { + public: + static constexpr uint16_t NOT_ENOUGH_DATA = 1; + static constexpr uint16_t INVALID_ENCODING = 2; + const uint16_t reasonCode; + + ParseError(const std::string& what, uint16_t reasonCode) + : std::runtime_error(what), reasonCode(reasonCode) { } + + ParseError(const char* what, uint16_t reasonCode) + : std::runtime_error(what), reasonCode(reasonCode) { } + }; + + // export template + // std::expected decodeValue(const std::span& data, Counter& read) = delete; + + + export template + std::expected decodeValue(const std::span& data, Counter& read) { + if (data.size() == 0) { + return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA)); + } + + ++read; + if (data[0] & 0b10000000) { + if constexpr (std::signed_integral) { + if (data[0] & 0b01000000) { + return T((~data[0] & 0b00111111) ^ -1); + } + } + return T(data[0] & 0b01111111); + } + + T value = 0; + std::uint8_t shift = 0; + + if constexpr (std::signed_integral) { + value |= data[0] & 0b00111111; + shift = 6; + } else { + value = data[0] & 0b01111111; + shift = 7; + } + + const std::uint8_t maxShifts = sizeof(T) * 8; + + for (size_t i = 1; i < data.size(); ++i) { + ++read; + value |= (static_cast(data[i]) & 0b01111111) << shift; + + shift += 7; + + if (data[i] & 0b10000000) { + if constexpr (std::signed_integral) { + if (data[0] & 0b01000000) { + // Fill the leftover bits with ones since it's a negative number + value |= T(-1) << std::min(shift, maxShifts - 1); + } + } + return enforceLittleEndian(value); + } + + if (shift >= maxShifts) { + return std::unexpected(ParseError("Value too large to decode", ParseError::INVALID_ENCODING)); + } + } + + return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA)); + } + + template + concept IsFloat = std::same_as; + + export template + std::expected decodeValue(const std::span& data, Counter& read) { + if (data.size() < 1) { + return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA)); + } + + // 0b11111111 is -1 + if (data[0] == 0b11111111) { + ++read; + return 0.0f; + } + + if (data.size() < 2) { + return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA)); + } + + // Even though the encoded size is int8_t, we need to use int16_t to handle the additional adjustments if it is valid + // The encoding supports this cast/conversion + std::expected exp = decodeValue(data, read); + if (!exp) { + return std::unexpected(exp.error()); + } + + if (*exp == -128) { + std::expected mantissa = decodeValue(data.subspan(read), read); + if (!mantissa) { + return std::unexpected(mantissa.error()); + } + + if (*mantissa == 0) { + return -std::numeric_limits::infinity(); + } else if (*mantissa == 1) { + return std::numeric_limits::infinity(); + } else if (*mantissa == 2) { + return std::numeric_limits::quiet_NaN(); + } else { + return std::unexpected(ParseError("Invalid NaN/Inf encoding", ParseError::INVALID_ENCODING)); + } + } else if (*exp < -126 && *exp > 127) { + return std::unexpected(ParseError("Invalid encoding", ParseError::INVALID_ENCODING)); + } + + std::expected mantissa = decodeValue(data.subspan(read), read); + if (!mantissa) { + return std::unexpected(mantissa.error()); + } + + return std::scalbn(static_cast(*mantissa), *exp + 1 - std::numeric_limits::digits); + } + + template + concept IsDouble = std::same_as; + + export template + std::expected decodeValue(const std::span& data, Counter& read) { + if (data.size() == 0) { + return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA)); + } + + // 128 is 0 + if (data[0] == static_cast(0b10000000)) { + ++read; + return 0.0f; + } + + if (data.size() < 2) { + return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA)); + } + + std::expected exp = decodeValue(data, read); + if (!exp) { + return std::unexpected(exp.error()); + } + + if (*exp == -2048) { + std::expected mantissa = decodeValue(data.subspan(read), read); + if (!mantissa) { + return std::unexpected(mantissa.error()); + } + + if (*mantissa == 0) { + return -std::numeric_limits::infinity(); + } else if (*mantissa == 1) { + return std::numeric_limits::infinity(); + } else if (*mantissa == 2) { + return std::numeric_limits::quiet_NaN(); + } else { + return std::unexpected(ParseError("Invalid NaN/Inf encoding", ParseError::INVALID_ENCODING)); + } + } else if (*exp < -1021 && *exp > 1024) { + return std::unexpected(ParseError("Invalid encoding", ParseError::INVALID_ENCODING)); + } + + std::expected mantissa = decodeValue(data.subspan(read), read); + if (!mantissa) { + return std::unexpected(mantissa.error()); + } + + return std::scalbn(static_cast(*mantissa), *exp - std::numeric_limits::digits); + } + + template + concept IsCollection = requires(T t) { + typename T::value_type; + typename T::size_type; + { t.reserve(std::declval()) }; + { t.emplace_back(std::declval()) }; + }; + + export template + std::expected decodeValue(const std::span& data, Counter& read) { + if (data.size() == 0) { + return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA)); + } + + if (data[0] == static_cast(0b10000000)) { + return Box{ }; + } + + std::expected size = decodeValue(data, read); + if (!size) { + return std::unexpected(size.error()); + } + + Box box; + box.reserve(static_cast(*size)); + + for (std::uint32_t i = 0; i < *size; ++i) { + std::expected value = decodeValue( + data.subspan(read), read + ); + if (!value) { + return std::unexpected(value.error()); + } + + box.emplace_back(*value); + } + + return box; + } + + export template + constexpr std::expected decodeValue(const std::span& data) { + int read = 0; + return decodeValue(data, read); + } + + // Aim for ~350 byte packets + export class Packet { + public: + static constexpr std::uint32_t VERSION = 0; + + private: + std::uint32_t packetVersion = VERSION; + std::uint32_t crcHash = 0; + std::uint32_t size = 0; + + std::vector data = { }; + + std::int32_t readHead = 0; + std::int32_t writeHead = 0; + + public: + Packet() = default; + + + template + constexpr size_t write(const T& value) { + const auto written = encodeValue(value, data, writeHead); + size += static_cast(written); + writeHead += static_cast(written); + return written; + } + + template + constexpr std::expected read() { + return decodeValue(std::span(data).subspan(readHead), readHead);; + } + + void reset() { + data.clear(); + readHead = 0; + writeHead = 0; + size = 0; + } + + [[nodiscard]] std::span bytes() { + return std::span(data); + } + + [[nodiscard]] std::uint32_t packetSize() const { + return size; + } + + [[nodiscard]] std::uint32_t crc() const { + return crcHash; + } + + [[nodiscard]] std::uint32_t version() const { + return packetVersion; + } + + [[nodiscard]] std::int32_t readPosition() const { + return readHead; + } + + [[nodiscard]] std::int32_t writePosition() const { + return writeHead; + } + + void setReadPosition(std::int32_t position) { + readHead = position; + } + + void setWritePosition(std::int32_t position) { + writeHead = position; + } + + std::expected initializePacket(const std::span& encodedData, int& read) { + if (encodedData.size() < 6) { + return std::unexpected( + ParseError("Not enough data to initialize packet", ParseError::NOT_ENOUGH_DATA) + ); + } + + auto pVesion = decodeValue(encodedData, read); + if (!pVesion) { + return std::unexpected(pVesion.error()); + } + + + auto crc = decodeValue(encodedData.subspan(read), read); + if (!crc) { + return std::unexpected(crc.error()); + } + + + const auto crcHead = read; + + auto pSize = decodeValue(encodedData.subspan(read), read); + if (!pSize) { + return std::unexpected(pSize.error()); + } + + + if (encodedData.size() < *pSize + read) { + return std::unexpected(ParseError("Not enough data to initialize packet", ParseError::NOT_ENOUGH_DATA)); + } + + const auto dataOffset = read - crcHead; + + const auto tempCRC = *crc; + + if (CryptoPP::CRC32C crc32; !crc32.VerifyDigest( + reinterpret_cast(&tempCRC), + encodedData.subspan(crcHead).data(), *pSize + dataOffset + )) { + return std::unexpected(ParseError("Invalid CRC", ParseError::INVALID_ENCODING)); + } + + packetVersion = *pVesion; + crcHash = tempCRC; + size = *pSize; + + auto payload = encodedData.subspan(read, *pSize); + data = std::vector(payload.begin(), payload.end()); + + assert(data.size() == size); + + writeHead = static_cast(size); + + return { }; + } + + std::expected initializePacket(const std::span& encodedData) { + int read = 0; + return initializePacket(encodedData, read); + } + + + static std::expected createPacket(const std::span& encodedData, int& read) { + Packet packet; + if (auto result = packet.initializePacket(encodedData, read)) { + return packet; + } else { + return std::unexpected(result.error()); + } + } + + static std::expected createPacket(const std::span& encodedData) { + int read = 0; + return createPacket(encodedData, read); + } + + std::vector finalizePacket() { + assert(size == data.size()); + + std::vector encodedData; + encodeValue(size, encodedData); + encodedData.insert(encodedData.end(), std::begin(data), std::end(data)); + + CryptoPP::CRC32C crc32; + crc32.Update(encodedData.data(), encodedData.size()); + crc32.Final(reinterpret_cast(&crcHash)); + + encodeValue(crcHash, encodedData, 0); + encodeValue(packetVersion, encodedData, 0); + + + return encodedData; + } + }; + } +} diff --git a/common/packet/tests/Packet_test.cppm b/common/packet/tests/Packet_test.cppm new file mode 100644 index 0000000..db8fe30 --- /dev/null +++ b/common/packet/tests/Packet_test.cppm @@ -0,0 +1,991 @@ +module; + +#include +#include + +#include + +export module Packet_test; +import MMO.Packet; +import std; + +// Encodes +TEST(DataEncoding, uint8Min) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::min(), encoded), 1); + EXPECT_EQ(encoded, std::vector{0b10000000}); + encoded.clear(); +} + +TEST(DataEncoding, uint8Max) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::max(), encoded), 2); + EXPECT_EQ(encoded, std::vector({ 0b01111111, 0b10000001})); + encoded.clear(); +} + +TEST(DataEncoding, int8Min) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::min(), encoded), 2); + EXPECT_EQ(encoded, std::vector({ 0b01000000, 0b11111110})); + encoded.clear(); +} + +TEST(DataEncoding, int8Max) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::max(), encoded), 2); + EXPECT_EQ(encoded, std::vector({ 0b00111111,0b10000001,})); + encoded.clear(); +} + +TEST(DataEncoding, uint16Min) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::min(), encoded), 1); + EXPECT_EQ(encoded, std::vector{0b10000000}); + encoded.clear(); +} + +TEST(DataEncoding, uint16Max) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::max(), encoded), 3); + EXPECT_EQ(encoded, std::vector({ 0b01111111, 0b01111111, 0b10000011})); + encoded.clear(); +} + +TEST(DataEncoding, int16Min) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::min(), encoded), 3); + EXPECT_EQ(encoded, std::vector({ 0b01000000, 0, 0b11111100})); + encoded.clear(); +} + +TEST(DataEncoding, int16Max) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::max(), encoded), 3); + EXPECT_EQ(encoded, std::vector({ 0b00111111, 0b01111111, 0b10000011})); + encoded.clear(); +} + +TEST(DataEncoding, uint32Min) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::min(), encoded), 1); + EXPECT_EQ(encoded, std::vector{0b10000000}); + encoded.clear(); +} + +TEST(DataEncoding, uint32Max) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::max(), encoded), 5); + EXPECT_EQ(encoded, std::vector({ 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111})); + encoded.clear(); +} + +TEST(DataEncoding, int32Min) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::min(), encoded), 5); + EXPECT_EQ(encoded, std::vector({ 0b01000000, 0, 0, 0, 0b11110000})); + encoded.clear(); +} + +TEST(DataEncoding, int32Max) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::max(), encoded), 5); + EXPECT_EQ(encoded, std::vector({ 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111})); + encoded.clear(); +} + +TEST(DataEncoding, uint64Min) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::min(), encoded), 1); + EXPECT_EQ(encoded, std::vector{0b10000000}); + encoded.clear(); +} + +TEST(DataEncoding, uint64Max) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::max(), encoded), 10); + EXPECT_EQ(encoded, + std::vector({ 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, + 0b01111111, 0b01111111, 0b10000001})); + encoded.clear(); +} + +TEST(DataEncoding, int64Min) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::min(), encoded), 10); + EXPECT_EQ(encoded, + std::vector({0b01000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, + 0b00000000, 0b00000000, 0b11111110})); + encoded.clear(); +} + +TEST(DataEncoding, int64Max) { + std::vector encoded; + EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits::max(), encoded), 10); + EXPECT_EQ(encoded, + std::vector({ 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, + 0b01111111, + 0b01111111, 0b10000001})); + encoded.clear(); +} + + +TEST(DataEncoding, float0) { + const auto value = 0.0f; + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 1); + EXPECT_EQ(encoded, + std::vector({ 0b11111111})); + encoded.clear(); +} + +TEST(DataEncoding, floatMin) { + const auto value = std::numeric_limits::min(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 6); + EXPECT_EQ(encoded, + std::vector({ 0b01000010, 0b11111110, 0, 0, 0, 0b10001000})); + encoded.clear(); +} + +TEST(DataEncoding, floatLowest) { + const auto value = std::numeric_limits::lowest(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 6); + EXPECT_EQ(encoded, + std::vector({ 0b00111111, 0b10000001, 0b01000001, 0 ,0, 0b11110000})); + encoded.clear(); +} + +TEST(DataEncoding, floatMax) { + const auto value = std::numeric_limits::max(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 6); + EXPECT_EQ(encoded, + std::vector({ 0b00111111, 0b10000001, 0b00111111, 0b01111111, 0b01111111, 0b10001111})); + encoded.clear(); +} + + +TEST(DataEncoding, floatNan) { + const auto value = std::numeric_limits::quiet_NaN(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3); + EXPECT_EQ(encoded, + std::vector({ 0b01000000, 0b11111110, 0b10000010})); + encoded.clear(); +} + + +TEST(DataEncoding, floatInfPositive) { + const auto value = std::numeric_limits::infinity(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3); + EXPECT_EQ(encoded, + std::vector({ 0b01000000, 0b11111110, 0b10000001})); + encoded.clear(); +} + + +TEST(DataEncoding, floatInfNegative) { + const auto value = -std::numeric_limits::infinity(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3); + EXPECT_EQ(encoded, + std::vector({ 0b01000000, 0b11111110, 0b10000000})); + encoded.clear(); +} + + +TEST(DataEncoding, double0) { + const double value = 0.0; + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 1); + EXPECT_EQ(encoded, + std::vector({ 0b10000000 })); + encoded.clear(); +} + +TEST(DataEncoding, doubleMin) { + const auto value = std::numeric_limits::min(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 10); + EXPECT_EQ(encoded, + std::vector({ 0b01000011, 0b11110000, 0, 0, 0, 0,0,0,0,0b10010000})); + encoded.clear(); +} + +TEST(DataEncoding, doubleLowest) { + const auto value = std::numeric_limits::lowest(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 10); + EXPECT_EQ(encoded, + std::vector({ 0, 0b10010000, 0b01000001,0, 0 ,0,0,0,0, 0b11100000})); + encoded.clear(); +} + +TEST(DataEncoding, doubleMax) { + const auto value = std::numeric_limits::max(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 10); + EXPECT_EQ(encoded, + std::vector({ 0, 0b10010000, 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, + 0b01111111, 0b01111111, 0b10011111})); + encoded.clear(); +} + + +TEST(DataEncoding, doubleNan) { + const auto value = std::numeric_limits::quiet_NaN(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3); + EXPECT_EQ(encoded, + std::vector({ 0b01000000, 0b11100000, 0b10000010})); + encoded.clear(); +} + + +TEST(DataEncoding, doubleInfPositive) { + const auto value = std::numeric_limits::infinity(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3); + EXPECT_EQ(encoded, + std::vector({ 0b01000000, 0b11100000, 0b10000001})); + encoded.clear(); +} + + +TEST(DataEncoding, doubleInfNegative) { + const auto value = -std::numeric_limits::infinity(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + EXPECT_EQ(MMO::Networking::encodeValue(value, encoded), 3); + EXPECT_EQ(encoded, + std::vector({ 0b01000000, 0b11100000, 0b10000000})); + encoded.clear(); +} + + +// Decodes +TEST(DataDecoding, uint8Min) { + std::vector encoded = { 0b10000000 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::min()); +} + +TEST(DataDecoding, uint8Max) { + std::vector encoded = { 0b01111111, 0b10000001 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::max()); +} + +TEST(DataDecoding, int8Min) { + std::vector encoded = { 0b01000000, 0b11111110 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::min()); +} + +TEST(DataDecoding, int8Max) { + std::vector encoded = { 0b00111111, 0b10000001, }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::max()); +} + +TEST(DataDecoding, uint16Min) { + std::vector encoded = { 0b10000000 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::min()); +} + +TEST(DataDecoding, uint16Max) { + std::vector encoded = { 0b01111111, 0b01111111, 0b10000011 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::max()); +} + +TEST(DataDecoding, int16Min) { + std::vector encoded = { 0b01000000, 0, 0b11111100 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::min()); +} + +TEST(DataDecoding, int16Max) { + std::vector encoded = { 0b00111111, 0b01111111, 0b10000011 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::max()); +} + +TEST(DataDecoding, uint32Min) { + std::vector encoded = { 0b10000000 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::min()); +} + +TEST(DataDecoding, uint32Max) { + std::vector encoded = { 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::max()); +} + +TEST(DataDecoding, int32Min) { + std::vector encoded = { 0b01000000, 0, 0, 0, 0b11110000 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::min()); +} + +TEST(DataDecoding, int32Max) { + std::vector encoded = { 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::max()); +} + +TEST(DataDecoding, uint64Min) { + std::vector encoded = { 0b10000000 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::min()); +} + +TEST(DataDecoding, uint64Max) { + std::vector encoded = { + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b10000001 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::max()); +} + +TEST(DataDecoding, int64Min) { + std::vector encoded = { + 0b01000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b00000000, + 0b11111110 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::min()); +} + +TEST(DataDecoding, int64Max) { + std::vector encoded = { + 0b00111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b10000001 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::max()); +} + +TEST(DataDecoding, float0) { + std::vector encoded = { 0b11111111 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), 0.0f); +} + +TEST(DataDecoding, floatMin) { + std::vector encoded = { + 0b01000010, + 0b11111110, + 0, + 0, + 0, + 0b10001000 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::min()); +} + +TEST(DataDecoding, floatLowest) { + std::vector encoded = { + 0b00111111, + 0b10000001, + 0b01000001, + 0, + 0, + 0b11110000 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::lowest()); +} + +TEST(DataDecoding, floatMax) { + std::vector encoded = { + 0b00111111, + 0b10000001, + 0b00111111, + 0b01111111, + 0b01111111, + 0b10001111 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::max()); +} + +TEST(DataDecoding, floatNan) { + std::vector encoded = { + 0b01000000, + 0b11111110, + 0b10000010 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_TRUE(std::isnan(value.value())); +} + +TEST(DataDecoding, floatInfPositive) { + std::vector encoded = { + 0b01000000, + 0b11111110, + 0b10000001 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::infinity()); +} + +TEST(DataDecoding, floatInfNegative) { + std::vector encoded = { + 0b01000000, + 0b11111110, + 0b10000000 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), -std::numeric_limits::infinity()); +} + +TEST(DataDecoding, double0) { + std::vector encoded = { 0b10000000 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), 0.0f); +} + +TEST(DataDecoding, doubleMin) { + std::vector encoded = { 0b01000011, 0b11110000, 0, 0, 0, 0, 0, 0, 0, 0b10010000 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::min()); +} + +TEST(DataDecoding, doubleLowest) { + std::vector encoded = { 0, 0b10010000, 0b01000001, 0, 0, 0, 0, 0, 0, 0b11100000 }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::lowest()); +} + +TEST(DataDecoding, doubleMax) { + std::vector encoded = { + 0, + 0b10010000, + 0b00111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b01111111, + 0b10011111 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::max()); +} + +TEST(DataDecoding, doubleNan) { + std::vector encoded = { + 0b01000000, + 0b11100000, + 0b10000010 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_TRUE(std::isnan(value.value())); +} + +TEST(DataDecoding, doubleInfPositive) { + std::vector encoded = { + 0b01000000, + 0b11100000, + 0b10000001 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), std::numeric_limits::infinity()); +} + +TEST(DataDecoding, doubleInfNegative) { + std::vector encoded = { + 0b01000000, + 0b11100000, + 0b10000000 + }; + int read = 0; + auto value = MMO::Networking::decodeValue(encoded, read); + EXPECT_TRUE(value.has_value()); + EXPECT_EQ(read, encoded.size()); + EXPECT_EQ(value.value(), -std::numeric_limits::infinity()); +} + +template +T randomValue() { + std::random_device rd; + std::mt19937_64 gen(rd()); + + std::uniform_int_distribution, int64_t, uint64_t>> dis( + std::numeric_limits::min(), std::numeric_limits::max()); + return static_cast(dis(gen)); +} + +const auto valueAttempts = 100; + +TEST(DataEnDecoding, uint8) { + for (int i = 0; i < valueAttempts; ++i) { + const auto value = randomValue(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); + } +} + +TEST(DataEnDecoding, int8) { + for (int i = 0; i < valueAttempts; ++i) { + const auto value = randomValue(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); + } +} + +TEST(DataEnDecoding, uint16) { + for (int i = 0; i < valueAttempts; ++i) { + const auto value = randomValue(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); + } +} + +TEST(DataEnDecoding, int16) { + for (int i = 0; i < valueAttempts; ++i) { + const auto value = randomValue(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); + } +} + +TEST(DataEnDecoding, uint32) { + for (int i = 0; i < valueAttempts; ++i) { + const auto value = randomValue(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); + } +} + +TEST(DataEnDecoding, int32) { + for (int i = 0; i < valueAttempts; ++i) { + const auto value = randomValue(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); + } +} + +TEST(DataEnDecoding, uint64) { + for (int i = 0; i < valueAttempts; ++i) { + const auto value = randomValue(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); + } +} + +TEST(DataEnDecoding, int64) { + for (int i = 0; i < valueAttempts; ++i) { + const auto value = randomValue(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); + } +} + +TEST(DataEnDecoding, floats) { + for (int i = 0; i < valueAttempts; ++i) { + const auto value = static_cast(randomValue()) / (static_cast(randomValue()) + + 1); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); + } +} + + +TEST(DataEnDecoding, float0) { + const auto value = 0.0f; + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + + +TEST(DataEnDecoding, floatMin) { + const auto value = std::numeric_limits::min(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + + +TEST(DataEnDecoding, floatLowest) { + const auto value = std::numeric_limits::lowest(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + +TEST(DataEnDecoding, floatMax) { + const auto value = std::numeric_limits::min(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + + +TEST(DataEnDecoding, floatNan) { + const auto value = std::numeric_limits::quiet_NaN(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_TRUE(std::isnan(decoded.value())); +} + + +TEST(DataEnDecoding, floatInfPositive) { + const auto value = std::numeric_limits::infinity(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + + +TEST(DataEnDecoding, floatInfNegative) { + const auto value = -std::numeric_limits::infinity(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + +TEST(DataEnDecoding, doubles) { + for (int i = 0; i < valueAttempts; ++i) { + const auto value = static_cast(randomValue()) / (static_cast(randomValue()) + + 1); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); + } +} + + +TEST(DataEnDecoding, double0) { + const auto value = 0.0; + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + + +TEST(DataEnDecoding, doubleMin) { + const auto value = std::numeric_limits::min(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + + +TEST(DataEnDecoding, doubleLowest) { + const auto value = std::numeric_limits::lowest(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + +TEST(DataEnDecoding, doubleMax) { + const auto value = std::numeric_limits::min(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + + +TEST(DataEnDecoding, doubleNan) { + const auto value = std::numeric_limits::quiet_NaN(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_TRUE(std::isnan(decoded.value())); +} + + +TEST(DataEnDecoding, doubleInfPositive) { + const auto value = std::numeric_limits::infinity(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + + +TEST(DataEnDecoding, doubleInfNegative) { + const auto value = -std::numeric_limits::infinity(); + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + + +TEST(DataEnDecoding, arrayOfValue) { + const std::vector value{ + randomValue(), + randomValue(), + randomValue(), + randomValue(), + randomValue(), + randomValue(), + }; + std::vector encoded; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + MMO::Networking::encodeValue(value, encoded); + auto decoded = MMO::Networking::decodeValue>(encoded); + EXPECT_TRUE(decoded.has_value()); + EXPECT_EQ(decoded.value(), value); +} + +TEST(MultiWriteTest, outOfOrderIsInOrder) { + const std::vector value{ + randomValue(), + randomValue(), + randomValue(), + randomValue(), + randomValue(), + randomValue(), + }; + std::vector encodedInOrder; + std::vector encodedOutOrder; + // Length is variable so we can't predict it (easily, there is math for it but I've lost it) + for (const auto& v : value) { + MMO::Networking::encodeValue(v, encodedInOrder); + } + for (int i = static_cast(value.size()) - 1; i >= 0; --i) { + MMO::Networking::encodeValue(value[i], encodedOutOrder, 0); + } + EXPECT_EQ(encodedInOrder, encodedOutOrder); +} + + +TEST(Packet, CreateFrom) { + MMO::Networking::Packet packet; + + packet.write(0xDEADBEEF); + packet.write(-1); + + + auto packetData = packet.finalizePacket(); + + auto packet2Exp = MMO::Networking::Packet::createPacket(packetData); + + EXPECT_TRUE(packet2Exp.has_value()); + auto packet2 = packet2Exp.value(); + { + auto readValue = packet2.read(); + EXPECT_TRUE(readValue.has_value()); + EXPECT_EQ(*readValue, 0xDEADBEEF); + } + { + auto readValue = packet2.read(); + EXPECT_TRUE(readValue.has_value()); + EXPECT_EQ(*readValue, -1); + } +} diff --git a/common/packet/tests/sanitizers.cpp b/common/packet/tests/sanitizers.cpp new file mode 100644 index 0000000..b9c673b --- /dev/null +++ b/common/packet/tests/sanitizers.cpp @@ -0,0 +1,14 @@ + +#include + +extern "C" { + void __ubsan_on_report() { + FAIL() << "Encountered an undefined behavior sanitizer error"; + } + void __asan_on_error() { + FAIL() << "Encountered an address sanitizer error"; + } + void __tsan_on_report() { + FAIL() << "Encountered a thread sanitizer error"; + } +} \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..664f391 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,32 @@ +{ + "name": "mmo", + "version": "1.0.0", + "description": "MMOServers", + "builtin-baseline": "4a9af5e5efcb24d0c8fcdf82dacf3b2b780b99f8", + "dependencies": [ + { + "name": "msquic", + "features": [ + "0-rtt" + ] + }, + "gtest", + { + "name": "openssl", + "features": [ + "tools" + ] + }, + "cryptopp", + "cli11", + { + "name": "redis-plus-plus", + "features": [ + "cxx17", + "tls" + ] + }, + "yyjson", + "spdlog" + ] +} \ No newline at end of file