Initial commit

This commit is contained in:
BordedDev 2025-01-30 17:41:19 +01:00
commit fe3aa4a77e
No known key found for this signature in database
GPG Key ID: C5F495EAE56673BF
22 changed files with 3849 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
* text=auto
*.sh text eol=lf

303
.gitignore vendored Normal file
View File

@ -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

43
CMakeLists.txt Normal file
View File

@ -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)

38
README.md Normal file
View File

@ -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
`<vcpkg install dir>\ports\<cryptopp|cli11>\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.

3
common/CMakeLists.txt Normal file
View File

@ -0,0 +1,3 @@
add_subdirectory(logging)
add_subdirectory(connection)
add_subdirectory(packet)

View File

@ -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" $<$<BOOL:${OPENSSL_EXECUTABLE_CFG}>:-config> $<$<BOOL:${OPENSSL_EXECUTABLE_CFG}>:${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
)

View File

@ -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;
}
}

View File

@ -0,0 +1,850 @@
export module MMO.DataConnection:MSQuicConnection;
import <msquic.h>;
import std;
import :MSQuicGlobal;
import :MSQuicError;
import MMO.Logging;
namespace MMO::Networking::MSQUIC {
export template<class T>
concept DataStorage = requires(T a) {
sizeof(typename std::remove_reference_t<decltype(a)>::value_type) == 1;
{ reinterpret_cast<uint8_t*>(std::data(a)) } -> std::same_as<uint8_t*>;
{ static_cast<uint32_t>(std::size(a)) } -> std::same_as<uint32_t>;
};
// 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<MSQuicStream> {
friend MSQuicConnection;
std::shared_ptr<MSQuicStream> backReference = nullptr;
std::shared_ptr<HQUIC> connectionStream = nullptr;
std::shared_ptr<void> streamAutoCloser = nullptr;
QUIC_STATUS status = QUIC_STATUS_NOT_FOUND;
std::vector<uint8_t> incomingData = { };
std::weak_ptr<MSQuicConnection> origin;
MSQuicStream(
std::shared_ptr<HQUIC> connectionStream,
std::shared_ptr<void> streamAutoCloser,
std::weak_ptr<MSQuicConnection> origin,
const QUIC_STATUS status
) :
connectionStream(std::move(connectionStream)), streamAutoCloser(std::move(streamAutoCloser)),
status(status),
origin(std::move(origin)) { }
public:
[[nodiscard]]
std::vector<uint8_t> retrieveAndResetData() {
std::vector<uint8_t> outgoingData;
outgoingData.swap(incomingData);
return outgoingData;
}
[[nodiscard]]
bool hasData() const {
return !incomingData.empty();
}
std::weak_ptr<MSQuicConnection> getOrigin() {
return origin;
}
[[nodiscard]]
bool isConnected() const {
return status == QUIC_STATUS_SUCCESS;
}
};
struct Payload : public QUIC_BUFFER, std::enable_shared_from_this<Payload> {
// We use this persistent buffer to ensure that the data is not deallocated
std::any data;
template<DataStorage T>
explicit Payload(T& data) : data(data) {
this->Length = static_cast<uint32_t>(std::size(std::any_cast<T&>(this->data)));
this->Buffer = const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(
std::data(std::any_cast<T&>(this->data))));
}
};
export class MSQuicConnection : public std::enable_shared_from_this<MSQuicConnection> {
public:
std::shared_ptr<Logging::Logger> logger = MMO::Logging::DEFAULT_LOGGER
? MMO::Logging::DEFAULT_LOGGER
: std::make_shared<Logging::NullLogger>();
protected:
std::shared_ptr<HQUIC> configuration = nullptr;
std::shared_ptr<HQUIC> connectionHandle = nullptr;
std::vector<std::shared_ptr<HQUIC>> connectionStreams = { };
std::unordered_map<HQUIC, std::weak_ptr<MSQuicStream>> connectionStreamMap = { };
std::unordered_map<void*, std::shared_ptr<Payload>> queuedData = { };
std::vector<std::weak_ptr<std::function<void(
std::shared_ptr<MSQuicStream>,
std::shared_ptr<MSQuicConnection>
)>>> onRemoteStreamCallbacks;
QUIC_STATUS connectionStatus = QUIC_STATUS_NOT_FOUND;
public:
const std::string remoteAddress;
const uint16_t remotePort;
void addRemoteStreamCallbacks(
const std::weak_ptr<std::function<void(
std::shared_ptr<MSQuicStream>,
std::shared_ptr<MSQuicConnection>
)>>& 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<HQUIC> 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<std::shared_ptr<MSQuicConnection>, 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<HQUIC> 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<MSQuicConnection>(
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<std::shared_ptr<MSQuicConnection>, 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<MSQuicError> establishConnection(const std::span<std::uint8_t>& 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<uint32_t>(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<std::shared_ptr<MSQuicStream>, 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<HQUIC> 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<MSQuicStream> streamPtr(
new MSQuicStream(
stream,
std::shared_ptr<void>(
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<DataStorage T>
[[nodiscard]]
std::optional<MSQuicError> send(
T& data,
std::shared_ptr<MSQuicStream>& 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<HQUIC> 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<Payload> sendData = std::make_shared<Payload>(data);
queuedData.emplace(sendData.get(), sendData);
// QUIC_BUFFER* buffer = new QUIC_BUFFER{
// static_cast<uint32_t>(std::size(data)), reinterpret_cast<uint8_t*>(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<DataStorage T>
[[nodiscard]]
constexpr std::optional<MSQuicError> send(
T& data,
QUIC_SEND_FLAGS flags = QUIC_SEND_FLAG_START | QUIC_SEND_FLAG_FIN
) {
std::shared_ptr<MSQuicStream> stream;
return send(data, stream, flags | QUIC_SEND_FLAG_START | QUIC_SEND_FLAG_FIN);
}
[[nodiscard]]
std::optional<MSQuicError> 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<std::uintptr_t>(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<std::uint64_t>(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::string>(
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<std::uintptr_t>(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<std::uintptr_t>(event->SEND_COMPLETE.ClientContext);
logger->log(
Logging::LEVEL_DEBUG,
"[{}][stream][{:X}] Data sent {} bytes [{:X} {}]", side, streamId,
static_cast<Payload*>(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<HQUIC>& 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<std::uintptr_t>(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<uint64_t>(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<std::uintptr_t>(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<HQUIC> 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<MSQuicStream> client(
new MSQuicStream(
newStream, std::shared_ptr<void>(
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<MSQuicConnection*>(context)->connectionCallback(connection, event);
}
QUIC_STATUS msquicClientStreamCallbackProxy(HQUIC stream, void* context, QUIC_STREAM_EVENT* event) {
return static_cast<MSQuicConnection*>(context)->streamCallback(stream, event);
}
QUIC_STATUS msquicServerConnectionCallbackProxy(HQUIC connection, void* context, QUIC_CONNECTION_EVENT* event) {
return static_cast<MSQuicConnection*>(context)->serverConnectionCallback(connection, event);
}
QUIC_STATUS msquicServerStreamCallbackProxy(HQUIC connection, void* context, QUIC_STREAM_EVENT* event) {
return static_cast<MSQuicConnection*>(context)->serverStreamCallback(connection, event);
}
}

View File

@ -0,0 +1,17 @@
export module MMO.DataConnection:MSQuicError;
import <msquic.h>;
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<uint64_t>(status))
) { }
};
}

View File

@ -0,0 +1,62 @@
export module MMO.DataConnection:MSQuicGlobal;
import <msquic.h>;
import std;
import :MSQuicError;
namespace MMO::Networking::MSQUIC {
export class MSQuicGlobal {
public:
std::shared_ptr<const QUIC_API_TABLE*> msQuicApiTable = nullptr;
std::shared_ptr<HQUIC> 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<std::reference_wrapper<MSQuicGlobal>, MSQuicError> get() {
static MSQuicGlobal instance;
if (!instance.msQuicApiTable) {
instance.msQuicApiTable = std::shared_ptr<const QUIC_API_TABLE*>(
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;
}
};
}

View File

@ -0,0 +1,316 @@
export module MMO.DataConnection:MSQuicServer;
import <msquic.h>;
import std;
import :MSQuicConnection;
import :MSQuicGlobal;
import :MSQuicError;
import MMO.Logging;
namespace MMO::Networking::MSQUIC {
export class MSQuicServer : public std::enable_shared_from_this<MSQuicServer> {
public:
std::shared_ptr<Logging::Logger> logger = MMO::Logging::DEFAULT_LOGGER
? MMO::Logging::DEFAULT_LOGGER
: std::make_shared<Logging::NullLogger>();
std::shared_ptr<HQUIC> configuration = nullptr;
std::shared_ptr<HQUIC> listenerHandle = nullptr;
const std::string hostAddress;
const uint16_t hostPort;
std::vector<std::weak_ptr<std::function<void(std::shared_ptr<MSQuicConnection>)>>> 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<HQUIC> 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<std::shared_ptr<MSQuicServer>, 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<HQUIC> 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<MSQuicServer>(
new MSQuicServer(configuration, hostAddress, hostPort), [](const auto* v) {
delete v;
}
);
}
[[nodiscard]]
static std::expected<std::shared_ptr<MSQuicServer>, 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<MSQuicError> 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<MSQuicError> 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<std::function<void(std::shared_ptr<MSQuicConnection>)>>& 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<std::uintptr_t>(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<uint16_t>(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<std::uintptr_t>(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<MSQuicConnection> 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<MSQuicServer*>(context)->serverListenerCallback(connection, event);
}
}

View File

@ -0,0 +1,266 @@
module;
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <utility>
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<MMO::Networking::NetworkingConnection> serverConnection;
auto serverCB = std::make_shared<std::function<void(std::shared_ptr<MMO::Networking::NetworkingConnection>)>>(
[&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<MMO::Networking::NetworkingConnection> serverConnection;
auto serverCB = std::make_shared<std::function<void(std::shared_ptr<MMO::Networking::NetworkingConnection>)>>(
[&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<MMO::Networking::NetworkingDataLane> serverStream;
auto serverStreamCB = std::make_shared<std::function<void(std::shared_ptr<MMO::Networking::NetworkingDataLane>, std::shared_ptr<MMO::Networking::NetworkingConnection>)>>(
[&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<uint8_t> 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));
}

View File

@ -0,0 +1,14 @@
#include <gtest/gtest.h>
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";
}
}

View File

@ -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})

View File

@ -0,0 +1,105 @@
export module MMO.Logging;
import <spdlog/spdlog.h>;
import std;
namespace MMO::Logging {
export class Logger {
public:
virtual ~Logger() = default;
virtual void log(uint8_t level, const std::string& message) = 0;
template<class... _Types>
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<class T>
concept LoggerConcept = requires(T a, uint8_t level, const std::string& message) {
{ a.log(level, message) } -> std::same_as<void>;
};
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<Logger> DEFAULT_LOGGER = std::make_shared<NullLogger>();
}

View File

@ -0,0 +1,10 @@
module;
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <utility>
export module Packet_test;
import MMO.Packet;
import std;

View File

@ -0,0 +1,14 @@
#include <gtest/gtest.h>
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";
}
}

View File

@ -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})

View File

@ -0,0 +1,629 @@
module;
#include <cryptopp/crc.h>
export module MMO.Packet;
import std;
import <cassert>;
namespace MMO::Networking {
inline namespace v1 {
template<std::integral T>
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<typename T>
concept Container = requires(T t) {
{ t.emplace(t.begin(), std::declval<std::uint8_t>()) };
{ std::size(t) } -> std::convertible_to<std::size_t>;
{ 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<typename T, Container C>
// 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<std::integral T, Container C>
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<T>) {
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<uint8_t>(data | 0b10000000u));
return count;
}
export template<Container C>
size_t encodeValue(const float value, C& encoded, std::int32_t offset = -1) {
// std::numeric_limits<float>::digits is 24
// std::numeric_limits<float>::max_exponent is 128 (we convert this to 127)
// std::numeric_limits<float>::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<std::int8_t>(-128), encoded,
static_cast<std::int32_t>(insertPos + count)
);
}
if (std::isnan(value)) {
count += encodeValue(
2, encoded,
static_cast<std::int32_t>(insertPos + count)
);
return count;
}
if (std::isinf(value)) {
count += encodeValue(
value < 0 ? 0 : 1,
encoded,
static_cast<std::int32_t>(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<std::int8_t>(adjExp), encoded,
static_cast<std::int32_t>(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<float>::digits);
count += encodeValue(
static_cast<std::int32_t>(sign), encoded,
static_cast<std::int32_t>(insertPos + count)
);
return count;
}
export template<Container C>
size_t encodeValue(const double value, C& encoded, std::int32_t offset = -1) {
// std::numeric_limits<double>::digits is 53
// std::numeric_limits<double>::max_exponent is 1024
// std::numeric_limits<double>::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<std::int16_t>(-2048), encoded,
static_cast<std::int32_t>(insertPos + count)
);
}
if (std::isnan(value)) {
count += encodeValue(
static_cast<std::int64_t>(2), encoded,
static_cast<std::int32_t>(insertPos + count)
);
return count;
} else if (std::isinf(value)) {
if (value < 0) {
count += encodeValue(
static_cast<std::int64_t>(0), encoded,
static_cast<std::int32_t>(insertPos + count)
);
} else {
count += encodeValue(
static_cast<std::int64_t>(1), encoded,
static_cast<std::int32_t>(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<std::int32_t>(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<double>::digits);
count += encodeValue(
static_cast<std::int64_t>(sign), encoded,
static_cast<std::int32_t>(insertPos + count)
);
return count;
}
export template<std::ranges::random_access_range Box, Container C>
size_t encodeValue(const Box& value, C& encoded, std::int32_t offset = -1) {
size_t count = encodeValue(static_cast<std::uint32_t>(std::size(value)), encoded, offset);
for (const auto& data : value) {
auto adjustedOffset = offset < 0
? std::min(offset + static_cast<std::int32_t>(count), -1)
: offset + static_cast<std::int32_t>(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<typename T, std::integral Counter>
// std::expected<T, ParseError> decodeValue(const std::span<std::uint8_t>& data, Counter& read) = delete;
export template<std::integral T, std::integral Counter>
std::expected<T, ParseError> decodeValue(const std::span<std::uint8_t>& 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<T>) {
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<T>) {
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<T>(data[i]) & 0b01111111) << shift;
shift += 7;
if (data[i] & 0b10000000) {
if constexpr (std::signed_integral<T>) {
if (data[0] & 0b01000000) {
// Fill the leftover bits with ones since it's a negative number
value |= T(-1) << std::min<std::uint8_t>(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<typename T>
concept IsFloat = std::same_as<float, T>;
export template<IsFloat T, std::integral Counter>
std::expected<T, ParseError> decodeValue(const std::span<std::uint8_t>& 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<std::int16_t, ParseError> exp = decodeValue<std::int16_t>(data, read);
if (!exp) {
return std::unexpected(exp.error());
}
if (*exp == -128) {
std::expected<std::int32_t, ParseError> mantissa = decodeValue<std::int32_t>(data.subspan(read), read);
if (!mantissa) {
return std::unexpected(mantissa.error());
}
if (*mantissa == 0) {
return -std::numeric_limits<T>::infinity();
} else if (*mantissa == 1) {
return std::numeric_limits<T>::infinity();
} else if (*mantissa == 2) {
return std::numeric_limits<T>::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<std::int32_t, ParseError> mantissa = decodeValue<std::int32_t>(data.subspan(read), read);
if (!mantissa) {
return std::unexpected(mantissa.error());
}
return std::scalbn(static_cast<T>(*mantissa), *exp + 1 - std::numeric_limits<T>::digits);
}
template<typename T>
concept IsDouble = std::same_as<double, T>;
export template<IsDouble T, std::integral Counter>
std::expected<T, ParseError> decodeValue(const std::span<std::uint8_t>& 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<std::uint8_t>(0b10000000)) {
++read;
return 0.0f;
}
if (data.size() < 2) {
return std::unexpected(ParseError("Not enough data to decode", ParseError::NOT_ENOUGH_DATA));
}
std::expected<std::int16_t, ParseError> exp = decodeValue<std::int16_t>(data, read);
if (!exp) {
return std::unexpected(exp.error());
}
if (*exp == -2048) {
std::expected<std::int64_t, ParseError> mantissa = decodeValue<std::int64_t>(data.subspan(read), read);
if (!mantissa) {
return std::unexpected(mantissa.error());
}
if (*mantissa == 0) {
return -std::numeric_limits<T>::infinity();
} else if (*mantissa == 1) {
return std::numeric_limits<T>::infinity();
} else if (*mantissa == 2) {
return std::numeric_limits<T>::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<std::int64_t, ParseError> mantissa = decodeValue<std::int64_t>(data.subspan(read), read);
if (!mantissa) {
return std::unexpected(mantissa.error());
}
return std::scalbn(static_cast<T>(*mantissa), *exp - std::numeric_limits<T>::digits);
}
template<typename T>
concept IsCollection = requires(T t) {
typename T::value_type;
typename T::size_type;
{ t.reserve(std::declval<typename T::size_type>()) };
{ t.emplace_back(std::declval<typename T::value_type>()) };
};
export template<IsCollection Box, std::integral Counter>
std::expected<Box, ParseError> decodeValue(const std::span<std::uint8_t>& 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<std::uint8_t>(0b10000000)) {
return Box{ };
}
std::expected<std::uint32_t, ParseError> size = decodeValue<std::uint32_t>(data, read);
if (!size) {
return std::unexpected(size.error());
}
Box box;
box.reserve(static_cast<typename Box::size_type>(*size));
for (std::uint32_t i = 0; i < *size; ++i) {
std::expected<typename Box::value_type, ParseError> value = decodeValue<typename Box::value_type>(
data.subspan(read), read
);
if (!value) {
return std::unexpected(value.error());
}
box.emplace_back(*value);
}
return box;
}
export template<typename T>
constexpr std::expected<T, ParseError> decodeValue(const std::span<std::uint8_t>& data) {
int read = 0;
return decodeValue<T>(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<std::uint8_t> data = { };
std::int32_t readHead = 0;
std::int32_t writeHead = 0;
public:
Packet() = default;
template<typename T>
constexpr size_t write(const T& value) {
const auto written = encodeValue(value, data, writeHead);
size += static_cast<std::uint32_t>(written);
writeHead += static_cast<std::int32_t>(written);
return written;
}
template<typename T>
constexpr std::expected<T, ParseError> read() {
return decodeValue<T>(std::span(data).subspan(readHead), readHead);;
}
void reset() {
data.clear();
readHead = 0;
writeHead = 0;
size = 0;
}
[[nodiscard]] std::span<std::uint8_t> 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<void, ParseError> initializePacket(const std::span<std::uint8_t>& encodedData, int& read) {
if (encodedData.size() < 6) {
return std::unexpected(
ParseError("Not enough data to initialize packet", ParseError::NOT_ENOUGH_DATA)
);
}
auto pVesion = decodeValue<std::uint8_t>(encodedData, read);
if (!pVesion) {
return std::unexpected(pVesion.error());
}
auto crc = decodeValue<std::uint32_t>(encodedData.subspan(read), read);
if (!crc) {
return std::unexpected(crc.error());
}
const auto crcHead = read;
auto pSize = decodeValue<std::uint32_t>(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<const CryptoPP::byte*>(&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<std::int32_t>(size);
return { };
}
std::expected<void, ParseError> initializePacket(const std::span<std::uint8_t>& encodedData) {
int read = 0;
return initializePacket(encodedData, read);
}
static std::expected<Packet, ParseError> createPacket(const std::span<std::uint8_t>& encodedData, int& read) {
Packet packet;
if (auto result = packet.initializePacket(encodedData, read)) {
return packet;
} else {
return std::unexpected(result.error());
}
}
static std::expected<Packet, ParseError> createPacket(const std::span<std::uint8_t>& encodedData) {
int read = 0;
return createPacket(encodedData, read);
}
std::vector<std::uint8_t> finalizePacket() {
assert(size == data.size());
std::vector<std::uint8_t> 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<CryptoPP::byte*>(&crcHash));
encodeValue(crcHash, encodedData, 0);
encodeValue(packetVersion, encodedData, 0);
return encodedData;
}
};
}
}

View File

@ -0,0 +1,991 @@
module;
#include <gtest/gtest.h>
#include <gmock/gmock.h>
#include <utility>
export module Packet_test;
import MMO.Packet;
import std;
// Encodes
TEST(DataEncoding, uint8Min) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint8_t>::min(), encoded), 1);
EXPECT_EQ(encoded, std::vector<uint8_t>{0b10000000});
encoded.clear();
}
TEST(DataEncoding, uint8Max) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint8_t>::max(), encoded), 2);
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01111111, 0b10000001}));
encoded.clear();
}
TEST(DataEncoding, int8Min) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int8_t>::min(), encoded), 2);
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01000000, 0b11111110}));
encoded.clear();
}
TEST(DataEncoding, int8Max) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int8_t>::max(), encoded), 2);
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b00111111,0b10000001,}));
encoded.clear();
}
TEST(DataEncoding, uint16Min) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint16_t>::min(), encoded), 1);
EXPECT_EQ(encoded, std::vector<uint8_t>{0b10000000});
encoded.clear();
}
TEST(DataEncoding, uint16Max) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint16_t>::max(), encoded), 3);
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01111111, 0b01111111, 0b10000011}));
encoded.clear();
}
TEST(DataEncoding, int16Min) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int16_t>::min(), encoded), 3);
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01000000, 0, 0b11111100}));
encoded.clear();
}
TEST(DataEncoding, int16Max) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int16_t>::max(), encoded), 3);
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b00111111, 0b01111111, 0b10000011}));
encoded.clear();
}
TEST(DataEncoding, uint32Min) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint32_t>::min(), encoded), 1);
EXPECT_EQ(encoded, std::vector<uint8_t>{0b10000000});
encoded.clear();
}
TEST(DataEncoding, uint32Max) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint32_t>::max(), encoded), 5);
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111}));
encoded.clear();
}
TEST(DataEncoding, int32Min) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int32_t>::min(), encoded), 5);
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b01000000, 0, 0, 0, 0b11110000}));
encoded.clear();
}
TEST(DataEncoding, int32Max) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int32_t>::max(), encoded), 5);
EXPECT_EQ(encoded, std::vector<uint8_t>({ 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111}));
encoded.clear();
}
TEST(DataEncoding, uint64Min) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint64_t>::min(), encoded), 1);
EXPECT_EQ(encoded, std::vector<uint8_t>{0b10000000});
encoded.clear();
}
TEST(DataEncoding, uint64Max) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<uint64_t>::max(), encoded), 10);
EXPECT_EQ(encoded,
std::vector<uint8_t>({ 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111,
0b01111111, 0b01111111, 0b10000001}));
encoded.clear();
}
TEST(DataEncoding, int64Min) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int64_t>::min(), encoded), 10);
EXPECT_EQ(encoded,
std::vector<uint8_t>({0b01000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b11111110}));
encoded.clear();
}
TEST(DataEncoding, int64Max) {
std::vector<uint8_t> encoded;
EXPECT_EQ(MMO::Networking::encodeValue(std::numeric_limits<int64_t>::max(), encoded), 10);
EXPECT_EQ(encoded,
std::vector<uint8_t>({ 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111,
0b01111111,
0b01111111, 0b10000001}));
encoded.clear();
}
TEST(DataEncoding, float0) {
const auto value = 0.0f;
std::vector<uint8_t> 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<uint8_t>({ 0b11111111}));
encoded.clear();
}
TEST(DataEncoding, floatMin) {
const auto value = std::numeric_limits<float>::min();
std::vector<uint8_t> 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<uint8_t>({ 0b01000010, 0b11111110, 0, 0, 0, 0b10001000}));
encoded.clear();
}
TEST(DataEncoding, floatLowest) {
const auto value = std::numeric_limits<float>::lowest();
std::vector<uint8_t> 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<uint8_t>({ 0b00111111, 0b10000001, 0b01000001, 0 ,0, 0b11110000}));
encoded.clear();
}
TEST(DataEncoding, floatMax) {
const auto value = std::numeric_limits<float>::max();
std::vector<uint8_t> 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<uint8_t>({ 0b00111111, 0b10000001, 0b00111111, 0b01111111, 0b01111111, 0b10001111}));
encoded.clear();
}
TEST(DataEncoding, floatNan) {
const auto value = std::numeric_limits<float>::quiet_NaN();
std::vector<uint8_t> 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<uint8_t>({ 0b01000000, 0b11111110, 0b10000010}));
encoded.clear();
}
TEST(DataEncoding, floatInfPositive) {
const auto value = std::numeric_limits<float>::infinity();
std::vector<uint8_t> 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<uint8_t>({ 0b01000000, 0b11111110, 0b10000001}));
encoded.clear();
}
TEST(DataEncoding, floatInfNegative) {
const auto value = -std::numeric_limits<float>::infinity();
std::vector<uint8_t> 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<uint8_t>({ 0b01000000, 0b11111110, 0b10000000}));
encoded.clear();
}
TEST(DataEncoding, double0) {
const double value = 0.0;
std::vector<uint8_t> 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<uint8_t>({ 0b10000000 }));
encoded.clear();
}
TEST(DataEncoding, doubleMin) {
const auto value = std::numeric_limits<double>::min();
std::vector<uint8_t> 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<uint8_t>({ 0b01000011, 0b11110000, 0, 0, 0, 0,0,0,0,0b10010000}));
encoded.clear();
}
TEST(DataEncoding, doubleLowest) {
const auto value = std::numeric_limits<double>::lowest();
std::vector<uint8_t> 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<uint8_t>({ 0, 0b10010000, 0b01000001,0, 0 ,0,0,0,0, 0b11100000}));
encoded.clear();
}
TEST(DataEncoding, doubleMax) {
const auto value = std::numeric_limits<double>::max();
std::vector<uint8_t> 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<uint8_t>({ 0, 0b10010000, 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b01111111,
0b01111111, 0b01111111, 0b10011111}));
encoded.clear();
}
TEST(DataEncoding, doubleNan) {
const auto value = std::numeric_limits<double>::quiet_NaN();
std::vector<uint8_t> 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<uint8_t>({ 0b01000000, 0b11100000, 0b10000010}));
encoded.clear();
}
TEST(DataEncoding, doubleInfPositive) {
const auto value = std::numeric_limits<double>::infinity();
std::vector<uint8_t> 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<uint8_t>({ 0b01000000, 0b11100000, 0b10000001}));
encoded.clear();
}
TEST(DataEncoding, doubleInfNegative) {
const auto value = -std::numeric_limits<double>::infinity();
std::vector<uint8_t> 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<uint8_t>({ 0b01000000, 0b11100000, 0b10000000}));
encoded.clear();
}
// Decodes
TEST(DataDecoding, uint8Min) {
std::vector<uint8_t> encoded = { 0b10000000 };
int read = 0;
auto value = MMO::Networking::decodeValue<uint8_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<uint8_t>::min());
}
TEST(DataDecoding, uint8Max) {
std::vector<uint8_t> encoded = { 0b01111111, 0b10000001 };
int read = 0;
auto value = MMO::Networking::decodeValue<uint8_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<uint8_t>::max());
}
TEST(DataDecoding, int8Min) {
std::vector<uint8_t> encoded = { 0b01000000, 0b11111110 };
int read = 0;
auto value = MMO::Networking::decodeValue<int8_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<int8_t>::min());
}
TEST(DataDecoding, int8Max) {
std::vector<uint8_t> encoded = { 0b00111111, 0b10000001, };
int read = 0;
auto value = MMO::Networking::decodeValue<int8_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<int8_t>::max());
}
TEST(DataDecoding, uint16Min) {
std::vector<uint8_t> encoded = { 0b10000000 };
int read = 0;
auto value = MMO::Networking::decodeValue<uint16_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<uint16_t>::min());
}
TEST(DataDecoding, uint16Max) {
std::vector<uint8_t> encoded = { 0b01111111, 0b01111111, 0b10000011 };
int read = 0;
auto value = MMO::Networking::decodeValue<uint16_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<uint16_t>::max());
}
TEST(DataDecoding, int16Min) {
std::vector<uint8_t> encoded = { 0b01000000, 0, 0b11111100 };
int read = 0;
auto value = MMO::Networking::decodeValue<int16_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<int16_t>::min());
}
TEST(DataDecoding, int16Max) {
std::vector<uint8_t> encoded = { 0b00111111, 0b01111111, 0b10000011 };
int read = 0;
auto value = MMO::Networking::decodeValue<int16_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<int16_t>::max());
}
TEST(DataDecoding, uint32Min) {
std::vector<uint8_t> encoded = { 0b10000000 };
int read = 0;
auto value = MMO::Networking::decodeValue<uint32_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<uint32_t>::min());
}
TEST(DataDecoding, uint32Max) {
std::vector<uint8_t> encoded = { 0b01111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111 };
int read = 0;
auto value = MMO::Networking::decodeValue<uint32_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<uint32_t>::max());
}
TEST(DataDecoding, int32Min) {
std::vector<uint8_t> encoded = { 0b01000000, 0, 0, 0, 0b11110000 };
int read = 0;
auto value = MMO::Networking::decodeValue<int32_t, int>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<int32_t>::min());
}
TEST(DataDecoding, int32Max) {
std::vector<uint8_t> encoded = { 0b00111111, 0b01111111, 0b01111111, 0b01111111, 0b10001111 };
int read = 0;
auto value = MMO::Networking::decodeValue<int32_t, int>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<int32_t>::max());
}
TEST(DataDecoding, uint64Min) {
std::vector<uint8_t> encoded = { 0b10000000 };
int read = 0;
auto value = MMO::Networking::decodeValue<uint64_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<uint64_t>::min());
}
TEST(DataDecoding, uint64Max) {
std::vector<uint8_t> encoded = {
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b10000001
};
int read = 0;
auto value = MMO::Networking::decodeValue<uint64_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<uint64_t>::max());
}
TEST(DataDecoding, int64Min) {
std::vector<uint8_t> encoded = {
0b01000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b00000000,
0b11111110
};
int read = 0;
auto value = MMO::Networking::decodeValue<int64_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<int64_t>::min());
}
TEST(DataDecoding, int64Max) {
std::vector<uint8_t> encoded = {
0b00111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b10000001
};
int read = 0;
auto value = MMO::Networking::decodeValue<int64_t>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<int64_t>::max());
}
TEST(DataDecoding, float0) {
std::vector<uint8_t> encoded = { 0b11111111 };
int read = 0;
auto value = MMO::Networking::decodeValue<float>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), 0.0f);
}
TEST(DataDecoding, floatMin) {
std::vector<uint8_t> encoded = {
0b01000010,
0b11111110,
0,
0,
0,
0b10001000
};
int read = 0;
auto value = MMO::Networking::decodeValue<float>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<float>::min());
}
TEST(DataDecoding, floatLowest) {
std::vector<uint8_t> encoded = {
0b00111111,
0b10000001,
0b01000001,
0,
0,
0b11110000
};
int read = 0;
auto value = MMO::Networking::decodeValue<float>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<float>::lowest());
}
TEST(DataDecoding, floatMax) {
std::vector<uint8_t> encoded = {
0b00111111,
0b10000001,
0b00111111,
0b01111111,
0b01111111,
0b10001111
};
int read = 0;
auto value = MMO::Networking::decodeValue<float>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<float>::max());
}
TEST(DataDecoding, floatNan) {
std::vector<uint8_t> encoded = {
0b01000000,
0b11111110,
0b10000010
};
int read = 0;
auto value = MMO::Networking::decodeValue<float>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_TRUE(std::isnan(value.value()));
}
TEST(DataDecoding, floatInfPositive) {
std::vector<uint8_t> encoded = {
0b01000000,
0b11111110,
0b10000001
};
int read = 0;
auto value = MMO::Networking::decodeValue<float>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<float>::infinity());
}
TEST(DataDecoding, floatInfNegative) {
std::vector<uint8_t> encoded = {
0b01000000,
0b11111110,
0b10000000
};
int read = 0;
auto value = MMO::Networking::decodeValue<float>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), -std::numeric_limits<float>::infinity());
}
TEST(DataDecoding, double0) {
std::vector<uint8_t> encoded = { 0b10000000 };
int read = 0;
auto value = MMO::Networking::decodeValue<double>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), 0.0f);
}
TEST(DataDecoding, doubleMin) {
std::vector<uint8_t> encoded = { 0b01000011, 0b11110000, 0, 0, 0, 0, 0, 0, 0, 0b10010000 };
int read = 0;
auto value = MMO::Networking::decodeValue<double>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<double>::min());
}
TEST(DataDecoding, doubleLowest) {
std::vector<uint8_t> encoded = { 0, 0b10010000, 0b01000001, 0, 0, 0, 0, 0, 0, 0b11100000 };
int read = 0;
auto value = MMO::Networking::decodeValue<double>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<double>::lowest());
}
TEST(DataDecoding, doubleMax) {
std::vector<uint8_t> encoded = {
0,
0b10010000,
0b00111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b01111111,
0b10011111
};
int read = 0;
auto value = MMO::Networking::decodeValue<double>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<double>::max());
}
TEST(DataDecoding, doubleNan) {
std::vector<uint8_t> encoded = {
0b01000000,
0b11100000,
0b10000010
};
int read = 0;
auto value = MMO::Networking::decodeValue<double>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_TRUE(std::isnan(value.value()));
}
TEST(DataDecoding, doubleInfPositive) {
std::vector<uint8_t> encoded = {
0b01000000,
0b11100000,
0b10000001
};
int read = 0;
auto value = MMO::Networking::decodeValue<double>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), std::numeric_limits<double>::infinity());
}
TEST(DataDecoding, doubleInfNegative) {
std::vector<uint8_t> encoded = {
0b01000000,
0b11100000,
0b10000000
};
int read = 0;
auto value = MMO::Networking::decodeValue<double>(encoded, read);
EXPECT_TRUE(value.has_value());
EXPECT_EQ(read, encoded.size());
EXPECT_EQ(value.value(), -std::numeric_limits<double>::infinity());
}
template<std::integral T>
T randomValue() {
std::random_device rd;
std::mt19937_64 gen(rd());
std::uniform_int_distribution<std::conditional_t<std::signed_integral<T>, int64_t, uint64_t>> dis(
std::numeric_limits<T>::min(), std::numeric_limits<T>::max());
return static_cast<T>(dis(gen));
}
const auto valueAttempts = 100;
TEST(DataEnDecoding, uint8) {
for (int i = 0; i < valueAttempts; ++i) {
const auto value = randomValue<uint8_t>();
std::vector<uint8_t> 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<uint8_t>(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<int8_t>();
std::vector<uint8_t> 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<int8_t>(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<uint16_t>();
std::vector<uint8_t> 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<uint16_t>(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<int16_t>();
std::vector<uint8_t> 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<int16_t>(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<uint32_t>();
std::vector<uint8_t> 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<uint32_t>(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<int32_t>();
std::vector<uint8_t> 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<int32_t>(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<uint64_t>();
std::vector<uint8_t> 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<uint64_t>(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<int64_t>();
std::vector<uint8_t> 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<int64_t>(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<float>(randomValue<int64_t>()) / (static_cast<float>(randomValue<uint32_t>()) +
1);
std::vector<uint8_t> 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<float>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
}
TEST(DataEnDecoding, float0) {
const auto value = 0.0f;
std::vector<uint8_t> 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<float>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, floatMin) {
const auto value = std::numeric_limits<float>::min();
std::vector<uint8_t> 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<float>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, floatLowest) {
const auto value = std::numeric_limits<float>::lowest();
std::vector<uint8_t> 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<float>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, floatMax) {
const auto value = std::numeric_limits<float>::min();
std::vector<uint8_t> 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<float>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, floatNan) {
const auto value = std::numeric_limits<float>::quiet_NaN();
std::vector<uint8_t> 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<float>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_TRUE(std::isnan(decoded.value()));
}
TEST(DataEnDecoding, floatInfPositive) {
const auto value = std::numeric_limits<float>::infinity();
std::vector<uint8_t> 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<float>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, floatInfNegative) {
const auto value = -std::numeric_limits<float>::infinity();
std::vector<uint8_t> 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<float>(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<double>(randomValue<int64_t>()) / (static_cast<double>(randomValue<uint32_t>()) +
1);
std::vector<uint8_t> 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<double>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
}
TEST(DataEnDecoding, double0) {
const auto value = 0.0;
std::vector<uint8_t> 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<double>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, doubleMin) {
const auto value = std::numeric_limits<double>::min();
std::vector<uint8_t> 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<double>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, doubleLowest) {
const auto value = std::numeric_limits<double>::lowest();
std::vector<uint8_t> 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<double>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, doubleMax) {
const auto value = std::numeric_limits<double>::min();
std::vector<uint8_t> 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<double>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, doubleNan) {
const auto value = std::numeric_limits<double>::quiet_NaN();
std::vector<uint8_t> 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<double>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_TRUE(std::isnan(decoded.value()));
}
TEST(DataEnDecoding, doubleInfPositive) {
const auto value = std::numeric_limits<double>::infinity();
std::vector<uint8_t> 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<double>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, doubleInfNegative) {
const auto value = -std::numeric_limits<double>::infinity();
std::vector<uint8_t> 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<double>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(DataEnDecoding, arrayOfValue) {
const std::vector<int32_t> value{
randomValue<int32_t>(),
randomValue<int32_t>(),
randomValue<int32_t>(),
randomValue<int32_t>(),
randomValue<int32_t>(),
randomValue<int32_t>(),
};
std::vector<uint8_t> 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<std::vector<int32_t>>(encoded);
EXPECT_TRUE(decoded.has_value());
EXPECT_EQ(decoded.value(), value);
}
TEST(MultiWriteTest, outOfOrderIsInOrder) {
const std::vector<int32_t> value{
randomValue<int32_t>(),
randomValue<int32_t>(),
randomValue<int32_t>(),
randomValue<int32_t>(),
randomValue<int32_t>(),
randomValue<int32_t>(),
};
std::vector<uint8_t> encodedInOrder;
std::vector<uint8_t> 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<int>(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<uint32_t>(0xDEADBEEF);
packet.write<int32_t>(-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<uint32_t>();
EXPECT_TRUE(readValue.has_value());
EXPECT_EQ(*readValue, 0xDEADBEEF);
}
{
auto readValue = packet2.read<int32_t>();
EXPECT_TRUE(readValue.has_value());
EXPECT_EQ(*readValue, -1);
}
}

View File

@ -0,0 +1,14 @@
#include <gtest/gtest.h>
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";
}
}

32
vcpkg.json Normal file
View File

@ -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"
]
}