diff --git a/.clang-tidy b/.clang-tidy index 7435c8c3..0afbfc8e 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -16,13 +16,11 @@ Checks: > performance-*, portability-*, readability-*, - -cppcoreguidelines-avoid-non-const-global-variables, -misc-use-anonymous-namespace, -readability-braces-around-statements, -modernize-use-trailing-return-type, -hicpp-braces-around-statements, -misc-const-correctness, - -cert-err58-cpp, -bugprone-easily-swappable-parameters, -bugprone-narrowing-conversions, -cppcoreguidelines-narrowing-conversions, @@ -37,7 +35,9 @@ Checks: > -readability-magic-numbers, -readability-avoid-return-with-void-value, -portability-avoid-pragma-once, - -bugprone-throwing-static-initialization + -bugprone-throwing-static-initialization, + -google-build-using-namespace, + -cppcoreguidelines-rvalue-reference-param-not-moved CheckOptions: - key: readability-identifier-naming.ClassCase value: CamelCase diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 00000000..efdd4fd5 --- /dev/null +++ b/.codespellrc @@ -0,0 +1,4 @@ +[codespell] +skip = .git,out,vcpkg,__fiddler_captures,resources +count = +quiet-level = 1 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 006eb800..4d4165b5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore - +pragmabackend-docker.tar # User-specific files *.rsuser *.suo @@ -526,3 +526,5 @@ vcpkg_installed /.idea/modules.xml /.idea/pragmabackend.iml /.idea/vcs.xml +Launcher/.qtcreator +Launcher/build diff --git a/CMakeLists.txt b/CMakeLists.txt index 7aeaedf0..1c3ff901 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,7 @@ cmake_minimum_required(VERSION 4.0) project(pragmabackend_core) set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) if(WIN32) add_compile_definitions(_WIN32_WINNT=0x0A00) # target at least win10 add_compile_definitions(_WIN32=1) @@ -40,6 +41,7 @@ find_package(Protobuf CONFIG REQUIRED) find_program(PROTOC_COMPILER protoc REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) find_package(stduuid CONFIG REQUIRED) +find_package(Drogon REQUIRED) target_compile_definitions(pragmabackend_core PUBLIC BOOST_ERROR_CODE_HEADER_ONLY) file(GLOB_RECURSE packetimplementors CMAKE_CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Packets/Implementation/*.cpp") target_sources(pragmabackend_core PRIVATE ${packetimplementors}) @@ -51,6 +53,8 @@ target_link_libraries(pragmabackend_core PUBLIC SQLiteCpp) target_link_libraries(pragmabackend_core PUBLIC protobuf::libprotoc protobuf::libprotobuf) target_link_libraries(pragmabackend_core PUBLIC nlohmann_json::nlohmann_json) target_link_libraries(pragmabackend_core PUBLIC stduuid) +target_link_libraries(pragmabackend_core PUBLIC Drogon::Drogon) +target_compile_features(pragmabackend_core PUBLIC cxx_std_20) target_include_directories(pragmabackend_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/Packets/Model") target_include_directories(pragmabackend_core PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/Persistence/Model") @@ -102,9 +106,8 @@ target_sources(pragmabackend_core PRIVATE ${GENERATED_PROTO_SRCS}) target_include_directories(pragmabackend_core PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) add_subdirectory(tests) - if(MSVC) add_compile_options(/W4 /WX /permissive-) else () add_compile_options(-Wall -Wextra -Wpedantic -Werror) -endif () \ No newline at end of file +endif () diff --git a/CMakePresets.json b/CMakePresets.json index 4c0a5f63..d4444d2c 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -14,6 +14,7 @@ "CMAKE_BUILD_TYPE": "Debug", "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake", "VCPKG_TARGET_TRIPLET": "x64-windows", + "VCPKG_INSTALL_OPTIONS": "--x-buildtrees-root=C:/b;--x-packages-root=C:/p;--x-downloads-root=C:/d", "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", "CMAKE_C_COMPILER": "cl", "CMAKE_CXX_COMPILER": "cl" @@ -37,6 +38,7 @@ "CMAKE_BUILD_TYPE": "Release", "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake", "VCPKG_TARGET_TRIPLET": "x64-windows", + "VCPKG_INSTALL_OPTIONS": "--x-buildtrees-root=C:/b;--x-packages-root=C:/p;--x-downloads-root=C:/d", "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", "CMAKE_C_COMPILER": "cl", "CMAKE_CXX_COMPILER": "cl" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0029b799 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM ubuntu:24.04 + +WORKDIR /server + +COPY out/build/x64-release-linux/ . +RUN chmod +x ./pragmabackend +EXPOSE 8081 +EXPOSE 8082 +EXPOSE 80 +CMD ["./pragmabackend"] diff --git a/Jenkinsfile b/Jenkinsfile index 0f5583cb..f7ad314d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -2,6 +2,14 @@ pipeline { agent none stages { + stage("Spell check") { + agent { label 'linux' } + steps { + checkout scm + sh 'codespell' + } + } + stage('Build Matrix') { matrix { axes { @@ -16,6 +24,7 @@ pipeline { } agent { label "${OS}" } + options { throttle(['RamIntensiveJob']) } @@ -57,7 +66,7 @@ pipeline { ]) { script { if (isUnix()) { - sh "echo \"{\\\"steamApiKey\\\": \\\"$STEAM_KEY\\\"}\" > auth.json" + sh "echo \"{\\\"steamApiKey\\\": \\\"\$STEAM_KEY\\\"}\" > auth.json" } else { powershell ''' $content = "{`"steamApiKey`": `"$env:STEAM_KEY`"}" @@ -73,16 +82,12 @@ pipeline { steps { script { if (env.OS == 'windows') { - bat "if exist out rmdir /s /q out" bat """ - call \"C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\VC\\Auxiliary\\Build\\vcvarsall.bat\" x64 + call "C:\\BuildTools\\VC\\Auxiliary\\Build\\vcvarsall.bat" x64 cmake --preset x64-${BUILD_TYPE}-win """ } else { - sh """ - rm -rf out - cmake --preset x64-${BUILD_TYPE}-linux - """ + sh "cmake --preset x64-${BUILD_TYPE}-linux" } } } @@ -93,7 +98,7 @@ pipeline { script { if (env.OS == 'windows') { bat """ - call \"C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\VC\\Auxiliary\\Build\\vcvarsall.bat\" x64 + call "C:\\BuildTools\\VC\\Auxiliary\\Build\\vcvarsall.bat" x64 cmake --build out/build/x64-${BUILD_TYPE}-win """ } else { @@ -116,44 +121,67 @@ pipeline { } } } - } - } + + stage("Stash") { + when { + expression { BUILD_TYPE == "release" } + } + steps { + script { + if (env.OS == 'windows') { + stash name: 'winbuild', includes: 'package-release-win/**' + } else { + stash name: 'linuxbuild', includes: 'package-release-linux/**' + } + } + } + } + } + } } - stage("code analysis") { + stage("Post-matrix parallel tasks") { parallel { + stage("Build docker server container") { + agent { label 'docker-linux' } + steps { + checkout scm + dir('out/build') { + sh "rm -rf x64-release-linux" + sh "rm -rf package-release-linux" + unstash 'linuxbuild' + sh "mv package-release-linux x64-release-linux" + } + sh "docker build -t pragmabackend:latest ." + sh "docker tag pragmabackend:latest ohmivr/pragmabackend:latest" + sh "docker push ohmivr/pragmabackend:latest" + sh "docker save pragmabackend:latest -o pragmabackend-docker.tar" + archiveArtifacts artifacts: 'pragmabackend-docker.tar', fingerprint: true + sh "rm pragmabackend-docker.tar" + } + } + stage("Code formatter") { agent { label 'linux' } steps { script { - stage("Checkout") { - checkout scm - } - stage("Run formatter") { - sh """ - FILES=\$(find . -type f -regex '\\./\\(Packets\\|Persistence\\|tests\\)/.*\\.\\(h\\|cpp\\)\$') - clang-format -i \$FILES main.cpp StaticHTTPPackets.cpp StaticWSPackets.cpp - """ - } - stage("Create diff") { - sh """ - if ! git diff --quiet; then - git diff > clang-format.patch - echo \"Patch created, apply the patch from the artifacts section to fix\" - else - echo \"No changes required\" - fi - """ - } - stage("Upload diff if exists") { - if (fileExists('clang-format.patch')) { - archiveArtifacts artifacts: 'clang-format.patch', fingerprint: true - } - } - stage("Fail if diff exists") { - if (fileExists('clang-format.patch')) { - sh "exit 1" - } + checkout scm + sh """ + FILES=\$(find . -type f -regex '\\./\\(Packets\\|Persistence\\|tests\\)/.*\\.\\(h\\|cpp\\)\$') + clang-format -i \$FILES main.cpp StaticHTTPPackets.cpp StaticWSPackets.cpp + """ + sh """ + if ! git diff --quiet; then + git diff > clang-format.patch + echo "Patch created, apply the patch from the artifacts section to fix" + else + echo "No changes required" + fi + """ + if (fileExists('clang-format.patch')) { + archiveArtifacts artifacts: 'clang-format.patch', fingerprint: true + sh "rm clang-format.patch" + error("Formatting changes required") } } } @@ -161,59 +189,83 @@ pipeline { stage("Code linter") { agent { label 'linux' } - options { - throttle(['RamIntensiveJob']) - } + options { throttle(['RamIntensiveJob']) } steps { script { - stage("Checkout") { - checkout scm - sh "git submodule sync --recursive" - sh "git submodule update --init --recursive" - } - stage("Configure") { - sh "cmake --preset x64-debug-linux" - } - stage("Build protobuf files") { - sh "cmake --build out/build/x64-debug-linux --target generate_protos" - } - stage("Run linter") { + checkout scm + sh "git submodule sync --recursive" + sh "git submodule update --init --recursive" + sh "cmake --preset x64-debug-linux" + sh "cmake --build out/build/x64-debug-linux --target generate_protos" + + catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { sh """ FILES=\$(find . -type f -regex '\\./\\(Packets\\|Persistence\\|tests\\)/.*\\.\\(h\\|cpp\\)\$') run-clang-tidy \$FILES main.cpp StaticHTTPPackets.cpp StaticWSPackets.cpp -fix -p out/build/x64-debug-linux -extra-arg=-Werror """ } - stage("Create diff") { - sh """ - if ! git diff --quiet; then - git diff > clang-tidy.patch - echo \"Patch created, apply the patch from the artifacts section to fix\" - else - echo \"No changes required\" - fi - """ + sh """ + if ! git diff --quiet; then + git diff > clang-tidy.patch + echo "Patch created, apply the patch from the artifacts section to fix" + else + echo "No changes required" + fi + """ + if (fileExists('clang-tidy.patch')) { + archiveArtifacts artifacts: 'clang-tidy.patch', fingerprint: true + sh "rm clang-tidy.patch" + error("Linter changes required") } - stage("Upload diff if exists") { - if (fileExists('clang-tidy.patch')) { - archiveArtifacts artifacts: 'clang-tidy.patch', fingerprint: true - } + } + } + } + + stage("Unit tests - Linux") { + agent { label 'linux' } + steps { + script { + sh "mkdir -p testdir" + sh "rm -rf testdir/package-release-linux" + dir('testdir') { + unstash 'linuxbuild' } - stage("Fail if diff exists") { - if (fileExists('clang-tidy.patch')) { - sh "exit 1" - } + catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { + sh "testdir/package-release-linux/tests/tests" } + sh "pkill -9 pragmabackend || true" } } } - } + + stage("Unit tests - Windows") { + agent { label 'windows' } + steps { + script { + bat "if not exist testdir mkdir testdir" + dir('testdir') { + unstash 'winbuild' + } + catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') { + bat "testdir\\package-release-win\\tests\\tests.exe" + } + bat "taskkill /f /im pragmabackend.exe || exit 0" + } + } + } + } } - } + } + post { - always { - node('linux'){ - step([$class: 'GitHubCommitStatusSetter', contextSource: [$class: 'ManuallyEnteredCommitContextSource', context: 'jenkins/build-status'], reposSource: [$class: 'ManuallyEnteredRepositorySource', url: 'https://github.com/SpectreRevival/pragmabackend']]) + always { + node('linux') { + step([ + $class: 'GitHubCommitStatusSetter', + contextSource: [$class: 'ManuallyEnteredCommitContextSource', context: 'jenkins/build-status'], + reposSource: [$class: 'ManuallyEnteredRepositorySource', url: 'https://github.com/SpectreRevival/pragmabackend'] + ]) + } } - } } } diff --git a/Packets/Implementation/AuthLatch.cpp b/Packets/Implementation/AuthLatch.cpp index 6d9e10a8..539be79b 100644 --- a/Packets/Implementation/AuthLatch.cpp +++ b/Packets/Implementation/AuthLatch.cpp @@ -24,4 +24,8 @@ std::string AuthLatch::TakeIfFresh(const std::string& ip) { auto out = it->second.providerId; map.erase(it); return out; +} + +void AuthLatch::Clear() { + map.clear(); } \ No newline at end of file diff --git a/Packets/Implementation/AuthenticateHandler.cpp b/Packets/Implementation/AuthenticateHandler.cpp index d2565763..6a07a4f0 100644 --- a/Packets/Implementation/AuthenticateHandler.cpp +++ b/Packets/Implementation/AuthenticateHandler.cpp @@ -1,3 +1,5 @@ +#include "BanDatabase.h" +#include "ProviderLinkDatabase.h" #include "ResourcesUtilities.h" #include @@ -20,6 +22,7 @@ #include #include #include +#include using tcp = boost::asio::ip::tcp; @@ -34,7 +37,7 @@ static const AuthCfg& GetAuthCfg() { AuthCfg c{}; auto tryPath = [&](const char* p) { if (std::ifstream f(p); f.is_open()) { - auto j = json::parse(f, nullptr, false); + auto j = nlohmann::json::parse(f, nullptr, false); if (!j.is_discarded() && j.contains("steamApiKey") && j.at("steamApiKey").is_string()) { c.steamApiKey = j.at("steamApiKey").get(); } @@ -46,8 +49,8 @@ static const AuthCfg& GetAuthCfg() { return cfg; } -AuthenticateHandler::AuthenticateHandler(std::string route) - : HTTPPacketProcessor(std::move(route)) { +AuthenticateHandler::AuthenticateHandler(HTTPRequestIdentifier id) + : HTTPPacketProcessor(std::move(id)) { } static std::string ClientIp(const tcp::socket& sock) { @@ -61,34 +64,18 @@ static std::string PlayerUuidFromSteam64(const std::string& steam64) { return boost::lexical_cast(id); } -void AuthenticateHandler::Process(const http::request& req, tcp::socket& sock) { - http::response res; - res.version(req.version()); - res.keep_alive(req.keep_alive()); - res.set(http::field::content_type, "application/json; charset=UTF-8"); - res.set(http::field::vary, "Origin"); - - auto reply = [&](http::status st, std::string body) { - res.result(st); - res.body() = std::move(body); - res.prepare_payload(); - http::write(sock, res); - }; +std::optional AuthenticateHandler::Process(const drogon::HttpRequestPtr& req) { const std::string& steamKey = GetAuthCfg().steamApiKey; - - if (req.method() != http::verb::post) { - res.set(http::field::allow, "POST"); - return reply(http::status::method_not_allowed, R"({"error":"method not allowed"})"); - } - - const auto ip = ClientIp(sock); - const auto steam64 = AuthLatch::Get().TakeIfFresh(ip); + const std::string ip = req->peerAddr().toIp(); + const std::string steam64 = AuthLatch::Get().TakeIfFresh(ip); if (steam64.empty()) { - return reply(http::status::bad_request, R"({"error":"NOSTEAMID"})"); + auto res = HttpResponse::newHttpResponse(); + res->setBody(R"({"error":"NOSTEAMID"})"); + res->setStatusCode(k400BadRequest); + return res; } - auto& db = PlayerDatabase::Get(); - auto playerId = db.LookupPlayerByProvider("STEAM", steam64); + auto playerId = ProviderLinkDatabase::Get().LookupPlayerByProvider(AuthProvider::STEAM, steam64); if (playerId.empty()) { std::string persona = "Player"; @@ -97,24 +84,29 @@ void AuthenticateHandler::Process(const http::request& req, t if (auto info = v.ValidateSteamId(steam64)) persona = info->personaName; } playerId = CreatePlayerFromSteam(steam64, persona); - db.UpsertProviderMap("STEAM", steam64, playerId); + ProviderLinkDatabase::Get().UpsertProviderMap(AuthProvider::STEAM, steam64, playerId); } - if (db.IsBanned(playerId)) { - return reply(http::status::forbidden, R"({"error":"ACCOUNT BANNED. CONTACT ASTROVAL0 ON DISCORD"})"); + if (BanDatabase::Get().IsBanned(playerId)) { + auto res = HttpResponse::newHttpResponse(); + res->setBody(R"({"error":"ACCOUNT BANNED. CONTACT ASTROVAL0 ON DISCORD"})"); + res->setStatusCode(k403Forbidden); + return res; } - auto prof = db.GetField(FieldKey::PROFILE_DATA, playerId); + auto prof = PlayerDatabase::Get().GetField(FieldKey::PROFILE_DATA, playerId); const std::string display = prof ? prof->displayname().displayname() : "Player"; const std::string disc = prof ? prof->displayname().discriminator() : "0000"; const std::string socialId = playerId; - json tokens = { + nlohmann::json tokens = { {"pragmaGameToken", BuildJwt("GAME", playerId, socialId, display, disc)}, {"pragmaSocialToken", BuildJwt("SOCIAL", playerId, socialId, display, disc)}}; - json out = {{"pragmaTokens", tokens}}; - return reply(http::status::ok, out.dump()); + nlohmann::json out = {{"pragmaTokens", tokens}}; + auto res = HttpResponse::newHttpResponse(); + res->setBody(out.dump()); + return res; } std::string AuthenticateHandler::CreatePlayerFromSteam(const std::string& steam64, const std::string& displayName) { diff --git a/Packets/Implementation/CreatePartyProcessor.cpp b/Packets/Implementation/CreatePartyProcessor.cpp index 77fa7619..71b52f11 100644 --- a/Packets/Implementation/CreatePartyProcessor.cpp +++ b/Packets/Implementation/CreatePartyProcessor.cpp @@ -1,3 +1,5 @@ +#include "ProviderLinkDatabase.h" + #include #include #include @@ -34,7 +36,7 @@ std::string CreatePartyProcessor::GetNewInviteCode() { } } -void CreatePartyProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional CreatePartyProcessor::Process(SpectreWebsocketRequest& packet) { std::unique_ptr req = packet.GetPayloadAsMessage(); PartyResponse createdPartyRes; @@ -54,10 +56,10 @@ void CreatePartyProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebso creatingPlayer->set_isleader(true); creatingPlayer->set_isready(false); - std::unique_ptr playerProfile = PlayerDatabase::Get().GetField(FieldKey::PROFILE_DATA, sock.GetPlayerId()); + std::unique_ptr playerProfile = PlayerDatabase::Get().GetField(FieldKey::PROFILE_DATA, packet.GetPlayerId()); creatingPlayer->mutable_displayname()->CopyFrom(playerProfile->displayname()); - creatingPlayer->set_playerid(sock.GetPlayerId()); - creatingPlayer->set_socialid(sock.GetPlayerId()); + creatingPlayer->set_playerid(packet.GetPlayerId()); + creatingPlayer->set_socialid(packet.GetPlayerId()); PartyMemberExtraInfo* creatingPlayerExtra = creatingPlayer->mutable_ext(); creatingPlayerExtra->set_version("173322"); @@ -65,15 +67,15 @@ void CreatePartyProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebso creatingPlayerExtra->set_rankedmodeunlocked(true); PartyMemberPlayerData* partyPlayerDat = creatingPlayerExtra->mutable_playerdata(); - std::unique_ptr playerDat = PlayerDatabase::Get().GetField(FieldKey::PLAYER_DATA, sock.GetPlayerId()); - partyPlayerDat->mutable_defenderweaponloadout()->set_playerid(sock.GetPlayerId()); + std::unique_ptr playerDat = PlayerDatabase::Get().GetField(FieldKey::PLAYER_DATA, packet.GetPlayerId()); + partyPlayerDat->mutable_defenderweaponloadout()->set_playerid(packet.GetPlayerId()); partyPlayerDat->mutable_defenderweaponloadout()->set_loadoutid(playerDat->defenderweaponloadoutid()); - partyPlayerDat->mutable_attackerweaponloadout()->set_playerid(sock.GetPlayerId()); + partyPlayerDat->mutable_attackerweaponloadout()->set_playerid(packet.GetPlayerId()); partyPlayerDat->mutable_attackerweaponloadout()->set_loadoutid(playerDat->attackerweaponloadoutid()); partyPlayerDat->mutable_matchmakingdata()->CopyFrom(playerDat->matchmakingdata()); partyPlayerDat->mutable_banner()->CopyFrom(playerDat->banner()); - std::unique_ptr outfitLoadouts = PlayerDatabase::Get().GetField(FieldKey::PLAYER_OUTFIT_LOADOUT, sock.GetPlayerId()); + std::unique_ptr outfitLoadouts = PlayerDatabase::Get().GetField(FieldKey::PLAYER_OUTFIT_LOADOUT, packet.GetPlayerId()); OutfitLoadout* selectedAttackerOutfit = nullptr; OutfitLoadout* selectedDefenderOutfit = nullptr; for (int i = 0; i < outfitLoadouts->loadouts_size(); i++) { @@ -88,13 +90,13 @@ void CreatePartyProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebso } } if (selectedAttackerOutfit == nullptr || selectedDefenderOutfit == nullptr) { - spdlog::error("Could not find selected outfit loadouts for player {}", sock.GetPlayerId()); + spdlog::error("Could not find selected outfit loadouts for player {}", packet.GetPlayerId()); throw std::runtime_error("Could not find selected outfit loadouts"); } partyPlayerDat->mutable_attackeroutfitloadout()->CopyFrom(*selectedAttackerOutfit); partyPlayerDat->mutable_defenderoutfitloadout()->CopyFrom(*selectedDefenderOutfit); - std::unique_ptr invstruct = PlayerDatabase::Get().GetField(FieldKey::PLAYER_INVENTORY, sock.GetPlayerId()); + std::unique_ptr invstruct = PlayerDatabase::Get().GetField(FieldKey::PLAYER_INVENTORY, packet.GetPlayerId()); const FullInventory& inv = invstruct->full(); for (int i = 0; i < inv.instanced_size(); i++) { // TODO(ohm): make this only actually return the items needed for performance, but for MVP this should be fine @@ -106,12 +108,12 @@ void CreatePartyProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebso sharedData->set_platformname("STEAM"); sharedData->set_crossplayplatformkind("CROSS_PLAY_PLATFORM_PC"); - std::string steamId = PlayerDatabase::Get().GetProviderIdByPlayerId(sock.GetPlayerId(), "STEAM"); + std::string steamId = ProviderLinkDatabase::Get().GetProviderIdByPlayerId(packet.GetPlayerId(), AuthProvider::STEAM); if (steamId.empty()) { spdlog::error("no steamId, investigate me!!!!!!!"); } sharedData->set_currentprovideraccountid(steamId); PartyDatabase::Get().SaveParty(createdPartyRes.party()); - sock.SendPacket(PartyDatabase::SerializePartyToString(createdPartyRes), packet.GetRequestId(), packet.GetResponseType()); + return PartyDatabase::SerializePartyToString(createdPartyRes); } \ No newline at end of file diff --git a/Packets/Implementation/EnterMatchmakingProcessor.cpp b/Packets/Implementation/EnterMatchmakingProcessor.cpp index 26164d5f..aeaebe78 100644 --- a/Packets/Implementation/EnterMatchmakingProcessor.cpp +++ b/Packets/Implementation/EnterMatchmakingProcessor.cpp @@ -6,8 +6,8 @@ EnterMatchmakingProcessor::EnterMatchmakingProcessor(const SpectreRpcType& rpcTy : WebsocketPacketProcessor(rpcType) { } -void EnterMatchmakingProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional EnterMatchmakingProcessor::Process(SpectreWebsocketRequest& packet) { const std::unique_ptr req = packet.GetPayloadAsMessage(); const PartyResponse res = PartyDatabase::Get().GetPartyRes(req->partyid()); - sock.SendPacket(PartyDatabase::SerializePartyToString(res), packet.GetRequestId(), packet.GetResponseType()); + return PartyDatabase::SerializePartyToString(res); } \ No newline at end of file diff --git a/Packets/Implementation/FetchPlayerLoadoutsProcessor.cpp b/Packets/Implementation/FetchPlayerLoadoutsProcessor.cpp index 7f70d5cb..cd66f1ee 100644 --- a/Packets/Implementation/FetchPlayerLoadoutsProcessor.cpp +++ b/Packets/Implementation/FetchPlayerLoadoutsProcessor.cpp @@ -7,7 +7,7 @@ FetchPlayerLoadoutsProcessor::FetchPlayerLoadoutsProcessor(const SpectreRpcType& : WebsocketPacketProcessor(rpcType) { } -void FetchPlayerLoadoutsProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional FetchPlayerLoadoutsProcessor::Process(SpectreWebsocketRequest& packet) { const std::unique_ptr req = packet.GetPayloadAsMessage(); const std::unique_ptr loadouts = PlayerDatabase::Get().GetField(FieldKey::PLAYER_WEAPON_LOADOUT, req->playerid()); FetchLoadoutsResponse res; @@ -16,5 +16,5 @@ void FetchPlayerLoadoutsProcessor::Process(SpectreWebsocketRequest& packet, Spec cur->set_playerid(req->playerid()); cur->set_loadoutid(loadouts->weaponloadoutdata(i).loadoutid()); } - sock.SendPacket(res, packet.GetResponseType(), packet.GetRequestId()); + return res; } \ No newline at end of file diff --git a/Packets/Implementation/GetBulkProfileDataProcessor.cpp b/Packets/Implementation/GetBulkProfileDataProcessor.cpp index 25ae6b91..2900df44 100644 --- a/Packets/Implementation/GetBulkProfileDataProcessor.cpp +++ b/Packets/Implementation/GetBulkProfileDataProcessor.cpp @@ -7,12 +7,12 @@ GetBulkProfileDataProcessor::GetBulkProfileDataProcessor(const SpectreRpcType& r : WebsocketPacketProcessor(rpcType) { } -void GetBulkProfileDataProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional GetBulkProfileDataProcessor::Process(SpectreWebsocketRequest& packet) { const std::unique_ptr req = packet.GetPayloadAsMessage(); BulkProfileDataResponse res; for (int i = 0; i < req->playerids_size(); i++) { res.add_bulkprofiledata()->CopyFrom( *PlayerDatabase::Get().GetField(FieldKey::PROFILE_DATA, req->playerids(i))); } - sock.SendPacket(res, packet.GetResponseType(), packet.GetRequestId()); + return res; } \ No newline at end of file diff --git a/Packets/Implementation/GetFriendsListAndRegisterOnlineHandler.cpp b/Packets/Implementation/GetFriendsListAndRegisterOnlineHandler.cpp new file mode 100644 index 00000000..0763495b --- /dev/null +++ b/Packets/Implementation/GetFriendsListAndRegisterOnlineHandler.cpp @@ -0,0 +1,22 @@ +#include "PlayerDatabase.h" +#include "SetPlayerPresenceHandler.h" + +#include +#include +#include +#include + +GetFriendsListAndRegisterOnlineHandler::GetFriendsListAndRegisterOnlineHandler(SpectreRpcType rpcType) + : WebsocketPacketProcessor(rpcType) { +} + +std::optional GetFriendsListAndRegisterOnlineHandler::Process(SpectreWebsocketRequest& packet) { + std::unique_ptr friendsList = PlayerDatabase::Get().GetField(FieldKey::FRIENDS_LIST, packet.GetPlayerId()); + std::unique_ptr presence = PlayerDatabase::Get().GetField(FieldKey::PLAYER_PRESENCE, packet.GetPlayerId()); + presence->set_basicpresence(Online); + presence->set_version(std::to_string(std::stoi(presence->version()) + 1)); + PlayerDatabase::Get().SetField(FieldKey::PLAYER_PRESENCE, presence.get(), packet.GetPlayerId()); + GetFriendsListAndRegisterOnlineResponse response; + response.mutable_friendlist()->CopyFrom(*friendsList); + return WebsocketPayload(response, {CreatePresenceNotification(*presence, packet.GetPlayerId())}); +} \ No newline at end of file diff --git a/Packets/Implementation/GetLoginDataProcessor.cpp b/Packets/Implementation/GetLoginDataProcessor.cpp index 93194d8a..ee17bdab 100644 --- a/Packets/Implementation/GetLoginDataProcessor.cpp +++ b/Packets/Implementation/GetLoginDataProcessor.cpp @@ -10,13 +10,13 @@ GetLoginDataProcessor::GetLoginDataProcessor(const SpectreRpcType& rpcType) : WebsocketPacketProcessor(rpcType) { } -void GetLoginDataProcessor::Process(SpectreWebsocketRequest& request, SpectreWebsocket& sock) { +std::optional GetLoginDataProcessor::Process(SpectreWebsocketRequest& packet) { std::string loginDataRes = R"({"loginData":{"ext":{"inboxMessages":[],"playerData":)"; - std::unique_ptr playerData = PlayerDatabase::Get().GetField(FieldKey::PLAYER_DATA, sock.GetPlayerId()); + std::unique_ptr playerData = PlayerDatabase::Get().GetField(FieldKey::PLAYER_DATA, packet.GetPlayerId()); loginDataRes += GetPlayerDataProcessor::GetPlayerDataAsString(*playerData); auto now = std::chrono::system_clock::now(); loginDataRes += R"(,"noCrew":-1.0,"nextCrewAutomationDate":"2025-03-04T09:00","crewAutomationInProcess":false,"currentServiceTimestampMillis":")" + std::to_string(duration_cast(now.time_since_epoch()).count()) + R"("},"inventoryData":{"issuedLimitedGrantTrackingIds":[],"inventoryContent":)"; loginDataRes += *GameDataStore::Get().InventoryStoreBuf(); loginDataRes += "}}}"; - sock.SendPacket(loginDataRes, request.GetRequestId(), request.GetResponseType()); + return loginDataRes; } \ No newline at end of file diff --git a/Packets/Implementation/GetPlayerDataProcessor.cpp b/Packets/Implementation/GetPlayerDataProcessor.cpp index b5d57e34..7edd7390 100644 --- a/Packets/Implementation/GetPlayerDataProcessor.cpp +++ b/Packets/Implementation/GetPlayerDataProcessor.cpp @@ -45,10 +45,10 @@ std::string GetPlayerDataProcessor::GetPlayerDataAsString(const PlayerData& play return finalPlayerDataComponent; } -void GetPlayerDataProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { - const std::unique_ptr data = PlayerDatabase::Get().GetField(FieldKey::PLAYER_DATA, sock.GetPlayerId()); +std::optional GetPlayerDataProcessor::Process(SpectreWebsocketRequest& packet) { + const std::unique_ptr data = PlayerDatabase::Get().GetField(FieldKey::PLAYER_DATA, packet.GetPlayerId()); data->set_serverdata("{}"); std::string fullPayload = "{\"data\":"; fullPayload += GetPlayerDataAsString(*data) + "}"; - sock.SendPacket(fullPayload, packet.GetRequestId(), packet.GetResponseType()); + return fullPayload; } \ No newline at end of file diff --git a/Packets/Implementation/HTTPRequestIdentifier.cpp b/Packets/Implementation/HTTPRequestIdentifier.cpp new file mode 100644 index 00000000..61535546 --- /dev/null +++ b/Packets/Implementation/HTTPRequestIdentifier.cpp @@ -0,0 +1,25 @@ +#include "spdlog/spdlog.h" + +#include +#include + +HTTPRequestIdentifier::HTTPRequestIdentifier(std::string route, drogon::HttpMethod reqType) + : route(std::move(std::move(route))), reqType(reqType) { +} + +HTTPRequestIdentifier::HTTPRequestIdentifier(std::string route) + : route(route), reqType(drogon::Get) { + spdlog::warn("No request type provided for route {}, assuming GET", route); +} + +const drogon::HttpMethod& HTTPRequestIdentifier::GetRequestType() const { + return reqType; +} + +const std::string& HTTPRequestIdentifier::GetRoute() const { + return route; +} + +bool HTTPRequestIdentifier::operator==(const HTTPRequestIdentifier& other) const { + return route == other.route && reqType == other.reqType; +} diff --git a/Packets/Implementation/HeartbeatProcessor.cpp b/Packets/Implementation/HeartbeatProcessor.cpp new file mode 100644 index 00000000..5850a6e7 --- /dev/null +++ b/Packets/Implementation/HeartbeatProcessor.cpp @@ -0,0 +1,8 @@ +#include + +HeartbeatProcessor::HeartbeatProcessor(const SpectreRpcType& rpcType) + : WebsocketPacketProcessor(rpcType){}; + +std::optional HeartbeatProcessor::Process(SpectreWebsocketRequest& /*packet*/) { + return nlohmann::json::object(); +} \ No newline at end of file diff --git a/Packets/Implementation/IsInPartyHandler.cpp b/Packets/Implementation/IsInPartyHandler.cpp index cb69952c..333a64e1 100644 --- a/Packets/Implementation/IsInPartyHandler.cpp +++ b/Packets/Implementation/IsInPartyHandler.cpp @@ -6,19 +6,18 @@ IsInPartyHandler::IsInPartyHandler(const SpectreRpcType& rpcType) : WebsocketPacketProcessor(rpcType) { } -void IsInPartyHandler::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional IsInPartyHandler::Process(SpectreWebsocketRequest& packet) { IsInPartyResponse res; sql::Statement getAllMembersStatement = PartyDatabase::Get().FormatStatement( "SELECT {col} FROM {table}", FieldKey::PARTY_MEMBERS); // This is gonna be really slow if ever used at scale, should probably refactor the party API at some point, but it works for now for (std::unique_ptr& party : PartyDatabase::Get().GetFields(getAllMembersStatement, FieldKey::PARTY_MEMBERS)) { for (int i = 0; i < party->members_size(); i++) { - if (party->members(i).playerid() == sock.GetPlayerId()) { + if (party->members(i).playerid() == packet.GetPlayerId()) { res.set_isinparty(true); - sock.SendPacket(res, packet.GetResponseType(), packet.GetRequestId()); - return; + return WebsocketPayload(res); } } } - sock.SendPacket(res, packet.GetResponseType(), packet.GetRequestId()); + return res; } \ No newline at end of file diff --git a/Packets/Implementation/Notification.cpp b/Packets/Implementation/Notification.cpp index b76b7c69..a4121273 100644 --- a/Packets/Implementation/Notification.cpp +++ b/Packets/Implementation/Notification.cpp @@ -1,9 +1,35 @@ +#include "spdlog/spdlog.h" + #include +#include +#include +#include + +static google::protobuf::util::JsonPrintOptions opts = { + .always_print_fields_with_no_presence = true}; + +static std::mt19937 rng = std::mt19937{std::random_device{}()}; +static uuids::uuid_random_generator gen{rng}; + +Notification::Notification(const SpectreRpcType& notificationType, const google::protobuf::Message& notificationData) + : notificationType(notificationType), notificationId(uuids::to_string(gen())) { + if (!google::protobuf::util::MessageToJsonString(notificationData, &(this->notificationData), opts).ok()) { + spdlog::error("Failed to turn protobuf notification data into json string"); + } +} -Notification::Notification(const SpectreRpcType notificationType) - : notificationType(notificationType) { +Notification::Notification(const SpectreRpcType& notificationType, std::string notificationId, std::string notificationPayload) + : notificationType(notificationType), notificationId(std::move(notificationId)), notificationData(std::move(notificationPayload)) { } const SpectreRpcType& Notification::GetNotificationType() const { return notificationType; +} + +const std::string& Notification::GetNotificationData() const { + return notificationData; +} + +const std::string& Notification::GetNotificationId() const { + return notificationId; } \ No newline at end of file diff --git a/Packets/Implementation/PacketProcessor.cpp b/Packets/Implementation/PacketProcessor.cpp new file mode 100644 index 00000000..c9467cd2 --- /dev/null +++ b/Packets/Implementation/PacketProcessor.cpp @@ -0,0 +1,25 @@ +#include +#include +#include +using namespace drogon; + +HTTPPacketProcessor::HTTPPacketProcessor(HTTPRequestIdentifier routeId) + : routeId(std::move(std::move(routeId))) { + app().registerHandler(this->routeId.GetRoute(), + [this](const HttpRequestPtr& req, std::function&& callback) { + if (auto res = Process(req); res.has_value()) { + callback(res.value()); + } else { + spdlog::warn("Packet of route {} type {} dropped because processor refused to respond", this->routeId.GetRoute(), static_cast(this->routeId.GetRequestType())); + auto errorRes = HttpResponse::newHttpResponse(); + errorRes->setBody("err refused to respond"); + errorRes->setStatusCode(k500InternalServerError); + callback(errorRes); + } + }, + {this->routeId.GetRequestType()}); +} + +HTTPPacketProcessor::HTTPPacketProcessor(HTTPRequestIdentifier routeId, uint16_t /*port*/) + : routeId(std::move(std::move(routeId))) { +} \ No newline at end of file diff --git a/Packets/Implementation/PartyDetailsNotification.cpp b/Packets/Implementation/PartyDetailsNotification.cpp deleted file mode 100644 index 431cf79b..00000000 --- a/Packets/Implementation/PartyDetailsNotification.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include -#include - -PartyDetailsNotification::PartyDetailsNotification(const std::string& partyId, SpectreRpcType notificationType) - : Notification(notificationType) { - payload = PartyDatabase::Get().GetPartyRes(partyId); -} - -PartyDetailsNotification::PartyDetailsNotification(PartyResponse partyRes, const SpectreRpcType notificationType) - : Notification(notificationType), payload(std::move(partyRes)) { -} - -PartyDetailsNotification::PartyDetailsNotification(const Party& party, const SpectreRpcType notificationType) - : Notification(notificationType) { - payload.mutable_party()->CopyFrom(party); -} - -void PartyDetailsNotification::SendTo(SpectreWebsocket& sock) const { - sock.SendNotification(PartyDatabase::SerializePartyToString(payload), GetNotificationType()); -} \ No newline at end of file diff --git a/Packets/Implementation/RegexPacketProcessorHTTP.cpp b/Packets/Implementation/RegexPacketProcessorHTTP.cpp index dac2f0f5..1f6be7fa 100644 --- a/Packets/Implementation/RegexPacketProcessorHTTP.cpp +++ b/Packets/Implementation/RegexPacketProcessorHTTP.cpp @@ -1,20 +1,14 @@ #include #include -void RegexPayloadProcessorHTTP::Process(const http::request& req, tcp::socket& sock) { - http::response res; - res.version(req.version()); - res.keep_alive(req.keep_alive()); - res.result(http::status::ok); - res.set(http::field::content_type, "application/json; charset=UTF-8"); - res.set(http::field::vary, "Origin"); +std::optional RegexPayloadProcessorHTTP::Process(const drogon::HttpRequestPtr& req) { for (const auto& [regex, payload] : resMap) { - if (std::regex_search(req.body(), regex.rx)) { - res.body() = payload->dump(); - res.prepare_payload(); - http::write(sock, res); - return; + if (std::regex_search(req->body().begin(), req->body().end(), regex.rx)) { + auto res = HttpResponse::newHttpResponse(); + res->setBody(payload->dump()); + return res; } } - spdlog::error("Regex processor {} failed to find any valid response for the packet, dropping the packet.\nPacket contents: {}", GetRoute(), req.body()); + return std::nullopt; + spdlog::error("Regex processor {} failed to find any valid response for the packet, dropping the packet.\nPacket contents: {}", GetRoute(), req->body()); } \ No newline at end of file diff --git a/Packets/Implementation/RegexPayloadProcessorWS.cpp b/Packets/Implementation/RegexPayloadProcessorWS.cpp index 2ff161dd..5c4b8e8a 100644 --- a/Packets/Implementation/RegexPayloadProcessorWS.cpp +++ b/Packets/Implementation/RegexPayloadProcessorWS.cpp @@ -1,13 +1,14 @@ #include -void RegexPayloadProcessorWS::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { - std::shared_ptr res = packet.GetBaseJsonResponse(); +std::optional RegexPayloadProcessorWS::Process(SpectreWebsocketRequest& packet) { + nlohmann::json res{}; std::string payloadStr = packet.GetPayload()->dump(); for (const auto& [regex, payload] : resMap) { if (std::regex_search(payloadStr, regex.rx)) { - (*res)["payload"] = *payload; - sock.SendPacket(res); - return; + res = *payload; + return WebsocketPayload(res); } } + spdlog::warn("Failed to find something to send for regex processor, sending empty packet"); + return nlohmann::json::object(); } \ No newline at end of file diff --git a/Packets/Implementation/SaveOutfitLoadoutProcessor.cpp b/Packets/Implementation/SaveOutfitLoadoutProcessor.cpp index f58c0533..7bc75672 100644 --- a/Packets/Implementation/SaveOutfitLoadoutProcessor.cpp +++ b/Packets/Implementation/SaveOutfitLoadoutProcessor.cpp @@ -7,9 +7,9 @@ SaveOutfitLoadoutProcessor::SaveOutfitLoadoutProcessor(SpectreRpcType rpcType) : WebsocketPacketProcessor(rpcType) { } -void SaveOutfitLoadoutProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional SaveOutfitLoadoutProcessor::Process(SpectreWebsocketRequest& packet) { std::unique_ptr loadoutToSave = packet.GetPayloadAsMessage(); - std::unique_ptr loadouts = PlayerDatabase::Get().GetField(FieldKey::PLAYER_OUTFIT_LOADOUT, sock.GetPlayerId()); + std::unique_ptr loadouts = PlayerDatabase::Get().GetField(FieldKey::PLAYER_OUTFIT_LOADOUT, packet.GetPlayerId()); if (!loadouts) loadouts = std::make_unique(); bool dataWritten = false; @@ -17,7 +17,7 @@ void SaveOutfitLoadoutProcessor::Process(SpectreWebsocketRequest& packet, Spectr if (CaseInsensitiveEquals(loadouts->loadouts(i).loadoutid(), loadoutToSave->loadoutid())) { loadouts->mutable_loadouts(i)->CopyFrom(*loadoutToSave); - loadouts->mutable_loadouts(i)->set_playerid(sock.GetPlayerId()); + loadouts->mutable_loadouts(i)->set_playerid(packet.GetPlayerId()); dataWritten = true; break; @@ -27,11 +27,11 @@ void SaveOutfitLoadoutProcessor::Process(SpectreWebsocketRequest& packet, Spectr spdlog::warn("didn't find the outfit loadout the game was trying to edit, added it as a new loadout\nLoadout id: {}", loadoutToSave->loadoutid()); OutfitLoadout* newLoadout = loadouts->add_loadouts(); newLoadout->CopyFrom(*loadoutToSave); - newLoadout->set_playerid(sock.GetPlayerId()); + newLoadout->set_playerid(packet.GetPlayerId()); } - PlayerDatabase::Get().SetField(FieldKey::PLAYER_OUTFIT_LOADOUT, loadouts.get(), sock.GetPlayerId()); - std::shared_ptr res = packet.GetBaseJsonResponse(); - res->at("payload")["success"] = true; - res->at("payload")["savedLoadoutId"] = loadoutToSave->loadoutid(); - sock.SendPacket(res); + PlayerDatabase::Get().SetField(FieldKey::PLAYER_OUTFIT_LOADOUT, loadouts.get(), packet.GetPlayerId()); + nlohmann::json res; + res["success"] = true; + res["savedLoadoutId"] = loadoutToSave->loadoutid(); + return res; } \ No newline at end of file diff --git a/Packets/Implementation/SavePlayerDataProcessor.cpp b/Packets/Implementation/SavePlayerDataProcessor.cpp index 696f399b..d9c51b9e 100644 --- a/Packets/Implementation/SavePlayerDataProcessor.cpp +++ b/Packets/Implementation/SavePlayerDataProcessor.cpp @@ -9,19 +9,19 @@ SavePlayerDataProcessor::SavePlayerDataProcessor(SpectreRpcType rpcType) : WebsocketPacketProcessor(rpcType) { } -void SavePlayerDataProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { - const char* strbegin = static_cast(packet.GetRawBuffer()->cdata().data()); - std::string reqFormatted = R"({"playerId": ")" + sock.GetPlayerId() + R"(","data":)"; +std::optional SavePlayerDataProcessor::Process(SpectreWebsocketRequest& packet) { + const char* strbegin = packet.GetBody().c_str(); + std::string reqFormatted = R"({"playerId": ")" + packet.GetPlayerId() + R"(","data":)"; int index = 0; char curChar = 0; - for (; index < packet.GetRawBuffer()->size(); index++) { + for (; index < packet.GetBody().size(); index++) { curChar = strbegin[index]; if (curChar == '\"' && strbegin[index + 1] == '{') { index++; // skip the " character at the start of data str break; } } - for (; index < packet.GetRawBuffer()->size(); index++) { + for (; index < packet.GetBody().size(); index++) { curChar = strbegin[index]; if (curChar == '\\') { if (strbegin[index + 1] == '\"') { @@ -40,7 +40,7 @@ void SavePlayerDataProcessor::Process(SpectreWebsocketRequest& packet, SpectreWe reqFormatted += curChar; } } - for (; index < packet.GetRawBuffer()->size() - 1; index++) { + for (; index < packet.GetBody().size() - 1; index++) { curChar = strbegin[index]; reqFormatted += curChar; } @@ -53,14 +53,14 @@ void SavePlayerDataProcessor::Process(SpectreWebsocketRequest& packet, SpectreWe // mt does this thing where they leave all the other fields blank if they just want to update the data str if (playerData.attackeroutfitloadoutid().empty()) { // Only copy the extra data field - std::unique_ptr existingData = PlayerDatabase::Get().GetField(FieldKey::PLAYER_DATA, sock.GetPlayerId()); + std::unique_ptr existingData = PlayerDatabase::Get().GetField(FieldKey::PLAYER_DATA, packet.GetPlayerId()); existingData->mutable_data()->CopyFrom(playerData.data()); - PlayerDatabase::Get().SetField(FieldKey::PLAYER_DATA, existingData.get(), sock.GetPlayerId()); + PlayerDatabase::Get().SetField(FieldKey::PLAYER_DATA, existingData.get(), packet.GetPlayerId()); } else { - spdlog::warn("attacker outfit loadout id set in SetPlayerData request from {}, this is not seen behavior, likely to cause bugs", sock.GetPlayerId()); - PlayerDatabase::Get().SetField(FieldKey::PLAYER_DATA, &playerData, sock.GetPlayerId()); + spdlog::warn("attacker outfit loadout id set in SetPlayerData request from {}, this is not seen behavior, likely to cause bugs", packet.GetPlayerId()); + PlayerDatabase::Get().SetField(FieldKey::PLAYER_DATA, &playerData, packet.GetPlayerId()); } - std::shared_ptr res = packet.GetBaseJsonResponse(); - res->at("payload")["success"] = true; - sock.SendPacket(res); + nlohmann::json res{}; + res["success"] = true; + return res; } \ No newline at end of file diff --git a/Packets/Implementation/SaveWeaponLoadoutProcessor.cpp b/Packets/Implementation/SaveWeaponLoadoutProcessor.cpp index 3eb58ae4..8cfa9132 100644 --- a/Packets/Implementation/SaveWeaponLoadoutProcessor.cpp +++ b/Packets/Implementation/SaveWeaponLoadoutProcessor.cpp @@ -8,13 +8,13 @@ SaveWeaponLoadoutProcessor::SaveWeaponLoadoutProcessor(SpectreRpcType rpcType) : WebsocketPacketProcessor(rpcType) { } -void SaveWeaponLoadoutProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional SaveWeaponLoadoutProcessor::Process(SpectreWebsocketRequest& packet) { std::unique_ptr dataToSave = packet.GetPayloadAsMessage(); const WeaponLoadout& loadoutToSave = dataToSave->weaponloadoutdata(); std::unique_ptr loadouts = PlayerDatabase::Get().GetField( FieldKey::PLAYER_WEAPON_LOADOUT, - sock.GetPlayerId()); + packet.GetPlayerId()); if (!loadouts) loadouts = std::make_unique(); @@ -24,7 +24,7 @@ void SaveWeaponLoadoutProcessor::Process(SpectreWebsocketRequest& packet, Spectr if (CaseInsensitiveEquals(loadouts->weaponloadoutdata(i).loadoutid(), loadoutToSave.loadoutid())) { loadouts->mutable_weaponloadoutdata(i)->CopyFrom(loadoutToSave); - loadouts->mutable_weaponloadoutdata(i)->set_playerid(sock.GetPlayerId()); + loadouts->mutable_weaponloadoutdata(i)->set_playerid(packet.GetPlayerId()); dataWritten = true; break; @@ -34,11 +34,11 @@ void SaveWeaponLoadoutProcessor::Process(SpectreWebsocketRequest& packet, Spectr spdlog::warn("didn't find the weapon loadout the game was trying to edit, added it as a new loadout\nLoadout id: {}", loadoutToSave.loadoutid()); WeaponLoadout* newLoadout = loadouts->add_weaponloadoutdata(); newLoadout->CopyFrom(loadoutToSave); - newLoadout->set_playerid(sock.GetPlayerId()); + newLoadout->set_playerid(packet.GetPlayerId()); } - PlayerDatabase::Get().SetField(FieldKey::PLAYER_WEAPON_LOADOUT, loadouts.get(), sock.GetPlayerId()); - std::shared_ptr res = packet.GetBaseJsonResponse(); - res->at("payload")["success"] = true; - res->at("payload")["savedLoadoutId"] = loadoutToSave.loadoutid(); - sock.SendPacket(res); + PlayerDatabase::Get().SetField(FieldKey::PLAYER_WEAPON_LOADOUT, loadouts.get(), packet.GetPlayerId()); + nlohmann::json res{}; + res["success"] = true; + res["savedLoadoutId"] = loadoutToSave.loadoutid(); + return res; } \ No newline at end of file diff --git a/Packets/Implementation/ServerMainThread.cpp b/Packets/Implementation/ServerMainThread.cpp new file mode 100644 index 00000000..2734468f --- /dev/null +++ b/Packets/Implementation/ServerMainThread.cpp @@ -0,0 +1,213 @@ +// +// Created by astro on 27/09/2025. +// + +#include "GetFriendsListAndRegisterOnlineHandler.h" +#include "ResourcesUtilities.h" +#include "SubmitProviderIdHandler.h" +#include "UpdatePresenceForPlayerHandler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static uint16_t gamePort = 8081; +static uint16_t socialPort = 8082; +static uint16_t wsPort = 80; + +namespace fs = std::filesystem; + +static std::shared_ptr logger; + +static void SetupLogger() { + auto consoleSink = std::make_shared(); + auto fileSink = std::make_shared((PersistenceUtilities::GetSavePath() / "logs" / "app.log").string(), true); + + // Optional: Customize sink formats + consoleSink->set_pattern("[%T] [%^%l%$] %v"); + fileSink->set_pattern("[%Y-%m-%d %T] [%l] %v"); + + // Combine sinks into one logger + std::vector sinks{consoleSink, fileSink}; + logger = std::make_shared("pragma", sinks.begin(), sinks.end()); + + // Register and use the logger + spdlog::register_logger(logger); + spdlog::set_default_logger(logger); +} + +static void HandleInterrupt(int /*sigint*/) { + drogon::app().quit(); +} + +static void ShutdownServer() { +} + +// the main accept loop +// binds to 127.0.0.1:443, accepts a connection, spins a thread, repeat +std::atomic_bool serverOnline = false; +std::atomic_bool handlersInitialized = false; +std::atomic_bool loggerSetup = false; +std::ofstream lockFile; + +void InitializeHandlers() { + (void)signal(2, HandleInterrupt); + (void)signal(9, HandleInterrupt); + (void)signal(15, HandleInterrupt); + if (!loggerSetup) { + loggerSetup = true; + SetupLogger(); + } + RegisterStaticHTTPHandlers(); + RegisterStaticWSHandlers(); + + // feel like this needs cleaning up T_T + + new SubmitProviderIdHandler(HTTPRequestIdentifier("/v1/submitproviderid", Post)); + new AuthenticateHandler(HTTPRequestIdentifier("/v1/account/authenticateorcreatev2", Post)); + new HeartbeatProcessor(SpectreRpcType("PlayerSessionRpc.HeartbeatV1Request")); + new FieldFetchProcessor( + SpectreRpcType("InventoryRpc.GetInventoryV2Request"), + FieldKey::PLAYER_INVENTORY); + logger->info("Finished registering handlers"); + new UpdateItemsV0Processor(SpectreRpcType("InventoryRpc.UpdateItemsV0Request")); + new UpdateItemV4Processor(SpectreRpcType("InventoryRpc.UpdateItemV4Request")); + new FieldFetchProcessor( + SpectreRpcType("MtnLoadoutServiceRpc.FetchPlayerOutfitLoadoutsV1Request"), + FieldKey::PLAYER_OUTFIT_LOADOUT); + new FetchPlayerLoadoutsProcessor( + SpectreRpcType("MtnLoadoutServiceRpc.FetchPlayerWeaponLoadoutsV1Request")); + new SaveWeaponLoadoutProcessor( + SpectreRpcType("MtnLoadoutServiceRpc.SaveWeaponLoadoutV1Request")); + new SaveOutfitLoadoutProcessor( + SpectreRpcType("MtnLoadoutServiceRpc.SaveOutfitLoadoutV1Request")); + new GetPlayerDataProcessor( + SpectreRpcType("MtnPlayerDataServiceRpc.GetAllPlayerDataClientV1Request")); + new GetBulkProfileDataProcessor( + SpectreRpcType("MtnPlayerDataServiceRpc.GetBulkProfileDataClientV1Request")); + new SavePlayerDataProcessor( + SpectreRpcType("MtnPlayerDataServiceRpc.SavePlayerDataForClientV1Request")); + new FieldFetchProcessor( + SpectreRpcType("MtnPlayerDataServiceRpc.GetPlayerLegacyDataV1Request"), + FieldKey::PLAYER_LEGACY_DATA); + new GetLoginDataProcessor( + SpectreRpcType("GameDataRpc.GetLoginDataV3Request")); + new CreatePartyProcessor( + SpectreRpcType("PartyRpc.CreateV1Request")); + new SetReadyProcessor( + SpectreRpcType("PartyRpc.SetReadyStateV1Request")); + new UpdatePartyProcessor( + SpectreRpcType("PartyRpc.UpdatePartyV1Request")); + new UpdatePartyPlayerProcessor( + SpectreRpcType("PartyRpc.UpdatePartyPlayerV1Request")); + new EnterMatchmakingProcessor( + SpectreRpcType("PartyRpc.EnterMatchmakingV1Request")); + new IsInPartyHandler( + SpectreRpcType("MultiplayerRpc.InitializePartyV1Request")); + new IsInPartyHandler( + SpectreRpcType("MultiplayerRpc.SyncPartyV1Request")); + new SetPlayerPresenceHandler( + SpectreRpcType("FriendRpc.SetPresenceV1Request")); + new GetFriendsListAndRegisterOnlineHandler( + SpectreRpcType("FriendRpc.GetFriendListAndRegisterOnlineV1Request")); + new UpdatePresenceForPlayerHandler(SpectreRpcType("MtnPlayerPresenceServiceRpc.UpdatePresenceForPlayerV1Request")); + drogon::app().setClientMaxBodySize(static_cast(100 * 1024 * 1024)); + drogon::app().setIdleConnectionTimeout(0); + drogon::app().addListener("0.0.0.0", gamePort); + drogon::app().addListener("0.0.0.0", socialPort); + drogon::app().addListener("0.0.0.0", wsPort); + drogon::app().registerBeginningAdvice([]() { + logger->info("Server bound!"); + serverOnline = true; + lockFile = std::ofstream(ResourcesUtilities::GetCurrentExecutablePath().parent_path() / "server.lock"); + lockFile << "locked"; + }); + drogon::app().setThreadNum(4); +} + +int MainThread(int argc, char** argv, std::stop_token st) { + if (argc == 4) { + gamePort = std::stoi(std::string(argv[1])); + socialPort = std::stoi(std::string(argv[2])); + wsPort = std::stoi(std::string(argv[3])); + } + if (!loggerSetup) { + loggerSetup = true; + SetupLogger(); + } + if (!handlersInitialized) { + handlersInitialized = true; + InitializeHandlers(); + } + std::stop_callback callback(st, []() { + drogon::app().quit(); + }); + logger->info("starting server..."); + try { + std::shared_ptr wsController = std::make_shared(); + logger->info("starting server..."); + drogon::app().run(); + lockFile.close(); + fs::remove(ResourcesUtilities::GetCurrentExecutablePath().parent_path() / "server.lock"); + serverOnline = false; + logger->info("Starting shutdown..."); + } catch (const std::exception& e) { + logger->error("unhandled exception caught in main: {}", e.what()); + } + spdlog::info("Shutting down server..."); + ShutdownServer(); + spdlog::info("Shutting down logging..."); + spdlog::shutdown(); + return 0; +} \ No newline at end of file diff --git a/Packets/Implementation/SetPlayerPresenceHandler.cpp b/Packets/Implementation/SetPlayerPresenceHandler.cpp new file mode 100644 index 00000000..1ec92525 --- /dev/null +++ b/Packets/Implementation/SetPlayerPresenceHandler.cpp @@ -0,0 +1,34 @@ +#include "PresenceUpdateNotification.pb.h" +#include "SpectreWebsocket.h" + +#include +#include +#include + +SetPlayerPresenceHandler::SetPlayerPresenceHandler(SpectreRpcType rpcType) + : WebsocketPacketProcessor(rpcType) { +} + +std::optional SetPlayerPresenceHandler::Process(SpectreWebsocketRequest& packet) { + std::unique_ptr req = packet.GetPayloadAsMessage(); + + std::unique_ptr presence = PlayerDatabase::Get().GetField(FieldKey::PLAYER_PRESENCE, packet.GetPlayerId()); + presence->set_basicpresence(req->basicpresence()); + int version = std::stoi(presence->version()); + version++; + presence->set_version(std::to_string(version)); + PlayerDatabase::Get().SetField(FieldKey::PLAYER_PRESENCE, presence.get(), packet.GetPlayerId()); + nlohmann::json res{}; + res["response"] = "Ok"; + return WebsocketPayload(res, {CreatePresenceNotification(*presence, packet.GetPlayerId())}); +} + +Notification CreatePresenceNotification(PlayerPresence& newPresence, const std::string& playerId) { + PresenceUpdateNotification updateNotification; + updateNotification.mutable_newpresence()->set_gameshardid("00000000-0000-0000-0000-000000000001"); + updateNotification.mutable_newpresence()->set_gametitleid("00000000-0000-0000-0000-000000000001"); + updateNotification.mutable_newpresence()->set_playerid(playerId); + updateNotification.mutable_newpresence()->set_version(newPresence.version()); + updateNotification.mutable_newpresence()->set_basicpresence(newPresence.basicpresence()); + return Notification(SpectreRpcType("FriendRpc.PresenceUpdateV1Notification"), updateNotification); +} \ No newline at end of file diff --git a/Packets/Implementation/SetReadyProcessor.cpp b/Packets/Implementation/SetReadyProcessor.cpp index 2a1a57bf..3584c356 100644 --- a/Packets/Implementation/SetReadyProcessor.cpp +++ b/Packets/Implementation/SetReadyProcessor.cpp @@ -7,20 +7,20 @@ SetReadyProcessor::SetReadyProcessor(SpectreRpcType rpcType) : WebsocketPacketProcessor(rpcType) { } -void SetReadyProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional SetReadyProcessor::Process(SpectreWebsocketRequest& packet) { std::unique_ptr readymsg = packet.GetPayloadAsMessage(); PartyResponse res = PartyDatabase::Get().GetPartyRes(readymsg->partyid()); bool playerFound = false; for (int i = 0; i < res.party().partymembers_size(); i++) { - if (res.party().partymembers(i).playerid() == sock.GetPlayerId()) { + if (res.party().partymembers(i).playerid() == packet.GetPlayerId()) { res.mutable_party()->mutable_partymembers(i)->set_isready(readymsg->ready()); playerFound = true; break; } } if (!playerFound) { - spdlog::warn("Didn't find player to ready up, ignoring and returning party response\nParty ID: {}\nPlayer ID: {}", readymsg->partyid(), sock.GetPlayerId()); + spdlog::warn("Didn't find player to ready up, ignoring and returning party response\nParty ID: {}\nPlayer ID: {}", readymsg->partyid(), packet.GetPlayerId()); } PartyDatabase::Get().SaveParty(res.party()); - sock.SendPacket(PartyDatabase::SerializePartyToString(res), packet.GetRequestId(), packet.GetResponseType()); + return PartyDatabase::SerializePartyToString(res); } \ No newline at end of file diff --git a/Packets/Implementation/SpectreWebsocket.cpp b/Packets/Implementation/SpectreWebsocket.cpp index c62b7129..37fe0fff 100644 --- a/Packets/Implementation/SpectreWebsocket.cpp +++ b/Packets/Implementation/SpectreWebsocket.cpp @@ -1,9 +1,18 @@ +#include "PlayerDatabase.h" +#include "SavedNotificationData.pb.h" + +#include #include +#include #include #include #include #include #include +#include + +std::unordered_map SpectreWebsocketController::connectionsByPlayerId{}; +std::mutex SpectreWebsocketController::connectionsMapMutex{}; static pbuf::util::JsonPrintOptions opts = []() { static pbuf::util::JsonPrintOptions options; @@ -11,11 +20,10 @@ static pbuf::util::JsonPrintOptions opts = []() { return options; }(); -static std::string ExtractBearer(const http::request& req) { - auto auth = req.base().at(http::field::authorization); - if (auth.empty()) return {}; +static std::string ExtractBearer(const std::string& authField) { + if (authField.empty()) return ""; static constexpr std::string_view prefix = "Bearer "; - const std::string s = std::string(auth); + const std::string s = std::string(authField); if (!s.starts_with(prefix)) return {}; return s.substr(prefix.size()); } @@ -41,88 +49,157 @@ static std::string DecodePlayerIdNoverify(const std::string& token) { return {}; } -SpectreWebsocket::SpectreWebsocket(ws& sock, const http::request& req) - : socket(sock), curSequenceNumber(0) { - socket.auto_fragment(false); // ideally we dont want to have to use this but we should be fine for now - const auto bearer = ExtractBearer(req); - const auto pid = bearer.empty() ? std::string() : DecodePlayerIdNoverify(bearer); +void SpectreWebsocket::NotificationThread(const std::stop_token& st) { + while (!st.stop_requested()) { + std::unique_lock lock(notificationQueueLock); + notificationQueueCondition.wait(lock, [&] { + return st.stop_requested() || (!notificationsToDeliver.empty() && !con.expired() && con.lock()->connected()); + }); + std::shared_ptr connection = con.lock(); + if (st.stop_requested()) break; - if (!pid.empty()) { - playerId = pid; - } else { - spdlog::error("no playerid ???? investigate me!"); - playerId = "1"; + Notification notification = notificationsToDeliver.front(); + notificationsToDeliver.pop_front(); + + connection->send(FormulateFinalNotification(notification)); + + SavedNotificationData savedData; + for (const Notification& notif : notificationsToDeliver) { + auto* newNotif = savedData.add_notificationstodeliver(); + newNotif->set_notificationdata(notif.GetNotificationData()); + newNotif->set_notificationid(notif.GetNotificationId()); + newNotif->set_rpctype(notif.GetNotificationType().GetName()); + } + + PlayerDatabase::Get().SetField(FieldKey::NOTIFICATION_DATA, &savedData, playerId); } -}; +} -const ws& SpectreWebsocket::GetRawSocket() const { - return socket; +void SpectreWebsocketController::handleNewMessage(const drogon::WebSocketConnectionPtr& wsCon, std::string&& message, const drogon::WebSocketMessageType& messageType) { + if (messageType != WebSocketMessageType::Binary && messageType != WebSocketMessageType::Text) { + return; + } + std::shared_ptr ctx = wsCon->getContext(); + std::string playerId = ctx->GetPlayerId(); + SpectreWebsocketRequest request(message, playerId); + WebsocketPacketProcessor* processor = WebsocketPacketProcessor::GetProcessorForRpc(request.GetRequestType()); + if (processor == nullptr) { + spdlog::warn("Failed to find websocket message for rpc type {}", request.GetRequestType().GetName()); + return; + } + std::optional response = processor->Process(request); + if (!response.has_value()) { + return; + } + std::string finalResponse = ctx->FormulateFinalResponse(response.value().GetPayload(), request.GetRequestId(), request.GetResponseType()); + wsCon->send(finalResponse); + for (const Notification& notification : response.value().GetPostSendNotifications()) { + ScheduleNotificationForPlayer(ctx->GetPlayerId(), notification); + } } -void SpectreWebsocket::SendPacket(const std::shared_ptr& res) { +std::string SpectreWebsocket::FormulateFinalResponse(const std::shared_ptr& res) { json packet; - packet["sequenceNumber"] = curSequenceNumber; + packet["sequenceNumber"] = curSequenceNumber.fetch_add(1); packet["response"] = *res; - curSequenceNumber++; - socket.text(true); // dont pass temporary because beast will fragment it std::string msg = packet.dump(); - socket.write(boost::asio::buffer(msg)); + return msg; } -void SpectreWebsocket::SendPacket(const pbuf::Message& payload, const std::string& resType, int requestId) { +std::string SpectreWebsocket::FormulateFinalResponse(const pbuf::Message& payload, const std::string& resType, int requestId) { // you shan't comment on this cursedness - std::string finalRes = "{\"sequenceNumber\":" + std::to_string(curSequenceNumber) + R"(,"response":{"requestId":)" + std::to_string(requestId) + R"(,"type":")" + resType + R"(","payload":)"; + std::string finalRes = "{\"sequenceNumber\":" + std::to_string(curSequenceNumber.fetch_add(1)) + R"(,"response":{"requestId":)" + std::to_string(requestId) + R"(,"type":")" + resType + R"(","payload":)"; std::string resComponent; if (!pbuf::util::MessageToJsonString(payload, &resComponent, opts).ok()) { spdlog::error("Failed to serialize pbuf message to string in SendPacket"); throw; } finalRes += resComponent + "}}"; - curSequenceNumber++; - socket.text(true); - socket.write(boost::asio::buffer(finalRes)); + return finalRes; } -void SpectreWebsocket::SendPacket(const std::string& resPayload, int requestId, const std::string& resType) { - std::string finalRes = "{\"sequenceNumber\":" + std::to_string(curSequenceNumber) + R"(,"response":{"requestId":)" + std::to_string(requestId) + R"(,"type":")" + resType + R"(","payload":)"; +std::string SpectreWebsocket::FormulateFinalResponse(const std::string& resPayload, int requestId, const std::string& resType) { + std::string finalRes = "{\"sequenceNumber\":" + std::to_string(curSequenceNumber.fetch_add(1)) + R"(,"response":{"requestId":)" + std::to_string(requestId) + R"(,"type":")" + resType + R"(","payload":)"; finalRes += resPayload + "}}"; - curSequenceNumber++; - socket.text(true); - socket.write(boost::asio::buffer(finalRes)); + return finalRes; } -void SpectreWebsocket::SendNotification(const std::shared_ptr& notifPayload, const SpectreRpcType& notificationType) { - json packet; - packet["sequenceNumber"] = curSequenceNumber; - packet["notification"]["type"] = notificationType.GetName(); - packet["notification"]["payload"] = *notifPayload; - curSequenceNumber++; - socket.text(true); - std::string msg = packet.dump(); - socket.write(boost::asio::buffer(msg)); +std::string SpectreWebsocket::FormulateFinalNotification(Notification& notification) { + return "{\"sequenceNumber\":" + std::to_string(curSequenceNumber.fetch_add(1)) + R"(,"notification":{"type":")" + notification.GetNotificationType().GetName() + R"(","payload":)" + notification.GetNotificationData() + "}}"; } -void SpectreWebsocket::SendNotification(const std::string& notifPayload, const SpectreRpcType& notificationType) { - std::string finalPayload = "{\"sequenceNumber\":" + std::to_string(curSequenceNumber) + R"(,"notification":{"type":")" + notificationType.GetName() + R"(","payload":)" + notifPayload + "}}"; - curSequenceNumber++; - socket.text(true); - socket.write(boost::asio::buffer(finalPayload)); +const std::string& SpectreWebsocket::GetPlayerId() { + return playerId; } -void SpectreWebsocket::SendNotification(const pbuf::Message& notif, const SpectreRpcType& notificationType) { - std::string finalRes = "{\"sequenceNumber\":" + std::to_string(curSequenceNumber) + R"(,"notification":"type":")" + notificationType.GetName() + R"(","payload":)"; - std::string payloadComponent; - if (!pbuf::util::MessageToJsonString(notif, &payloadComponent, opts).ok()) { - spdlog::error("Failed to serialize pbuf message to string in SendPacket"); - throw; +std::optional SpectreWebsocketController::GetConnectionForPlayer(const std::string& playerId) { + std::unique_lock lock(connectionsMapMutex); + auto it = connectionsByPlayerId.find(playerId); + if (it == connectionsByPlayerId.end()) { + return std::nullopt; + } + if (!it->second) { + return std::nullopt; } - finalRes += payloadComponent + "}}"; - curSequenceNumber++; - socket.text(true); - socket.write(boost::asio::buffer(finalRes)); + return it->second; } -const std::string& SpectreWebsocket::GetPlayerId() { - return playerId; +void SpectreWebsocket::ScheduleNotification(const Notification& notif) { + { + std::lock_guard lock(notificationQueueLock); + notificationsToDeliver.push_back(notif); + } + notificationQueueCondition.notify_one(); +} + +void SpectreWebsocketController::ScheduleNotificationForPlayer(const std::string& playerId, const Notification& notif) { + std::optional connection = GetConnectionForPlayer(playerId); + if (!connection.has_value()) { + // player is not logged in, save the notification to disk instead + std::unique_ptr notifData = PlayerDatabase::Get().GetField(FieldKey::NOTIFICATION_DATA, playerId); + SavedNotification* newNotif = notifData->add_notificationstodeliver(); + newNotif->set_notificationdata(notif.GetNotificationData()); + newNotif->set_notificationid(notif.GetNotificationId()); + newNotif->set_rpctype(notif.GetNotificationType().GetName()); + PlayerDatabase::Get().SetField(FieldKey::NOTIFICATION_DATA, notifData.get(), playerId); + } else { + connection.value()->getContext()->ScheduleNotification(notif); + } +} + +SpectreWebsocket::SpectreWebsocket(const drogon::HttpRequestPtr& req, const drogon::WebSocketConnectionPtr& connection) + : con(connection), curSequenceNumber(0) { + + const auto bearer = ExtractBearer(req->getHeader("Authorization")); + const auto pid = bearer.empty() ? std::string() : DecodePlayerIdNoverify(bearer); + + if (!pid.empty()) { + playerId = pid; + } else { + spdlog::error("no playerid ???? investigate me!, thinks will be SEVERELY wrong for connection with {}", req->peerAddr().toIpPort()); + playerId = "1"; + } + std::unique_ptr notificationsFromDisk = PlayerDatabase::Get().GetField(FieldKey::NOTIFICATION_DATA, playerId); + for (int i = 0; i < notificationsFromDisk->notificationstodeliver_size(); i++) { + const SavedNotification& currentNotif = notificationsFromDisk->notificationstodeliver(i); + notificationsToDeliver.emplace_back(SpectreRpcType(currentNotif.rpctype()), currentNotif.notificationid(), currentNotif.notificationdata()); + } + notificationWorkerThread = std::jthread([this](const std::stop_token& st) { + NotificationThread(st); + }); + SpectreWebsocketController::AddConnection(playerId, connection); +} + +void SpectreWebsocketController::AddConnection(const std::string& playerId, WebSocketConnectionPtr con) { + std::unique_lock connectionsMapLock(connectionsMapMutex); + connectionsByPlayerId.insert_or_assign(playerId, con); +} + +void SpectreWebsocketController::handleNewConnection(const drogon::HttpRequestPtr& req, const drogon::WebSocketConnectionPtr& con) { + std::shared_ptr wsCtx = std::make_shared(req, con); + con->setContext(wsCtx); +} + +void SpectreWebsocketController::handleConnectionClosed(const WebSocketConnectionPtr& conPtr) { } \ No newline at end of file diff --git a/Packets/Implementation/SpectreWebsocketRequest.cpp b/Packets/Implementation/SpectreWebsocketRequest.cpp index 1da0cbb6..25f2098e 100644 --- a/Packets/Implementation/SpectreWebsocketRequest.cpp +++ b/Packets/Implementation/SpectreWebsocketRequest.cpp @@ -3,33 +3,21 @@ #include #include -SpectreWebsocketRequest::SpectreWebsocketRequest(SpectreWebsocket& sock, reqbuf req) - : websocket(sock), requestBuf(std::move(req)) { - json reqjson = json::parse(static_cast(requestBuf.data().data()), static_cast(requestBuf.data().data()) + requestBuf.size()); - reqJson = std::make_shared(reqjson); +SpectreWebsocketRequest::SpectreWebsocketRequest(std::string& reqBody, std::string& playerId) + : reqBody(reqBody), playerId(playerId) { + nlohmann::json reqjson = nlohmann::json::parse(reqBody); + reqJson = std::make_shared(reqjson); try { requestType = SpectreRpcType(std::string(reqJson->at("type"))); } catch (std::exception& e) { spdlog::warn("log type not found for " + reqJson->at("type").get()); } - m_requestId = reqJson->at("requestId"); + requestId = reqJson->at("requestId"); payloadAsStr = reqJson->at("payload").dump(); } -std::shared_ptr SpectreWebsocketRequest::GetPayload() const { - return std::make_shared((reqJson->at("payload"))); -} - -std::shared_ptr SpectreWebsocketRequest::GetBaseJsonResponse() { - json response; - response["requestId"] = m_requestId; - response["type"] = GetResponseType(); - response["payload"] = json::object(); - return std::make_shared(std::move(response)); -} - -void SpectreWebsocketRequest::SendEmptyResponse() { - websocket.SendPacket(GetBaseJsonResponse()); +std::shared_ptr SpectreWebsocketRequest::GetPayload() const { + return std::make_shared((reqJson->at("payload"))); } std::string SpectreWebsocketRequest::GetResponseType() const { @@ -40,3 +28,19 @@ std::string SpectreWebsocketRequest::GetResponseType() const { resType += "Response"; return resType; } + +const std::string& SpectreWebsocketRequest::GetBody() const { + return reqBody; +} + +SpectreRpcType SpectreWebsocketRequest::GetRequestType() const { + return requestType; +} + +int SpectreWebsocketRequest::GetRequestId() const { + return requestId; +} + +const std::string& SpectreWebsocketRequest::GetPlayerId() const { + return playerId; +} \ No newline at end of file diff --git a/StaticHTTPPackets.cpp b/Packets/Implementation/StaticHTTPPackets.cpp similarity index 82% rename from StaticHTTPPackets.cpp rename to Packets/Implementation/StaticHTTPPackets.cpp index ebd9bcbf..bd26f800 100644 --- a/StaticHTTPPackets.cpp +++ b/Packets/Implementation/StaticHTTPPackets.cpp @@ -1,4 +1,3 @@ -#pragma once #include #include #include @@ -10,19 +9,20 @@ namespace fs = std::filesystem; #pragma warning(push) // disable msvc's complaining about us not saving the processors in vars, they'll be cleaned up when our program ends. #pragma warning(disable : 4101) -static void RegisterStaticHTTPHandlerFromFile(std::string route, std::string filename) { +void RegisterStaticHTTPHandlerFromFile(std::string route, std::string filename) { std::ifstream fileres(filename); if (!fileres.is_open()) { throw std::runtime_error("failed to open file"); } - json res = json::parse(fileres); + nlohmann::json res = nlohmann::json::parse(fileres); fileres.close(); spdlog::info("registered static HTTP route {} from json file at {}", route, filename); - new StaticResponseProcessorHTTP(route, std::make_shared(std::move(res))); + new StaticResponseProcessorHTTP(HTTPRequestIdentifier(route, Get), res); } #pragma warning(pop) -static void RegisterStaticHTTPHandlers() { +void RegisterStaticHTTPHandlers() { + RegisterStaticHTTPHandlerFromFile("//v1/info", (ResourcesUtilities::GetResourcesFolder() / "payloads" / "static" / "game" / "v1" / "info.json").string()); for (const auto& file : fs::recursive_directory_iterator(ResourcesUtilities::GetResourcesFolder() / "payloads" / "static" / "game")) { if (!fs::is_regular_file(file)) continue; std::string route = (fs::absolute(file.path().parent_path()) / file.path().stem()).string(); diff --git a/Packets/Implementation/StaticResponseProcessorHTTP.cpp b/Packets/Implementation/StaticResponseProcessorHTTP.cpp index 8c5c2ebd..deb756a1 100644 --- a/Packets/Implementation/StaticResponseProcessorHTTP.cpp +++ b/Packets/Implementation/StaticResponseProcessorHTTP.cpp @@ -1,13 +1,12 @@ #include +#include -void StaticResponseProcessorHTTP::Process(const http::request& req, tcp::socket& sock) { - http::response res; - res.version(req.version()); - res.keep_alive(req.keep_alive()); - res.result(http::status::ok); - res.set(http::field::content_type, "application/json; charset=UTF-8"); - res.set(http::field::vary, "Origin"); - res.body() = staticRes->dump(); - res.prepare_payload(); - http::write(sock, res); +StaticResponseProcessorHTTP::StaticResponseProcessorHTTP(HTTPRequestIdentifier id, const nlohmann::json& res) + : HTTPPacketProcessor(std::move(id)), staticRes(res.dump()) { +} + +std::optional StaticResponseProcessorHTTP::Process(const drogon::HttpRequestPtr& /*req*/) { + auto res = HttpResponse::newHttpResponse(); + res->setBody(staticRes); + return res; } \ No newline at end of file diff --git a/Packets/Implementation/StaticResponseProcessorWS.cpp b/Packets/Implementation/StaticResponseProcessorWS.cpp index 07532f36..1d21659b 100644 --- a/Packets/Implementation/StaticResponseProcessorWS.cpp +++ b/Packets/Implementation/StaticResponseProcessorWS.cpp @@ -1,8 +1,10 @@ #include #include -void StaticResponseProcessorWS::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { - std::shared_ptr res = packet.GetBaseJsonResponse(); - (*res)["payload"] = *staticRes; - sock.SendPacket(res); +StaticResponseProcessorWS::StaticResponseProcessorWS(const SpectreRpcType rpcType, const nlohmann::json& res) + : WebsocketPacketProcessor(rpcType), staticRes(res.dump()) { +} + +std::optional StaticResponseProcessorWS::Process(SpectreWebsocketRequest& /*packet*/) { + return staticRes; } \ No newline at end of file diff --git a/StaticWSPackets.cpp b/Packets/Implementation/StaticWSPackets.cpp similarity index 79% rename from StaticWSPackets.cpp rename to Packets/Implementation/StaticWSPackets.cpp index 268d4636..9893815c 100644 --- a/StaticWSPackets.cpp +++ b/Packets/Implementation/StaticWSPackets.cpp @@ -1,33 +1,35 @@ -#pragma once #include #include #include +#include #include #include #include #include -static void RegisterStaticHandlerFromFile(std::string filename, SpectreRpcType rpcType) { +namespace fs = std::filesystem; + +void RegisterStaticHandlerFromFile(std::string filename, SpectreRpcType rpcType) { std::ifstream resfile(filename); if (!resfile.is_open()) { throw std::runtime_error("failed to open response file"); } - json res = json::parse(resfile); + nlohmann::json res = nlohmann::json::parse(resfile); resfile.close(); spdlog::info("registered static WS {} from json file at {}", rpcType.GetName(), filename); - new StaticResponseProcessorWS(rpcType, std::make_shared(std::move(res))); + new StaticResponseProcessorWS(rpcType, res); } -static void RegisterRegexHandlerFromFiles(SpectreRpcType rpcType, std::initializer_list> map) { - std::unordered_map> map2; +void RegisterRegexHandlerFromFiles(SpectreRpcType rpcType, std::initializer_list> map) { + std::unordered_map> map2; for (const auto& [regex, filename] : map) { std::ifstream resfile(filename); if (!resfile.is_open()) { throw std::runtime_error("failed to open res file"); } - json res = json::parse(resfile); + nlohmann::json res = nlohmann::json::parse(resfile); resfile.close(); - map2.insert({regex, std::make_shared(std::move(res))}); + map2.insert({regex, std::make_shared(std::move(res))}); } spdlog::info("registered static WS {} using regex handler", rpcType.GetName()); new RegexPayloadProcessorWS(rpcType, map2); diff --git a/Packets/Implementation/SubmitProviderIdHandler.cpp b/Packets/Implementation/SubmitProviderIdHandler.cpp index 0f37baec..affdef13 100644 --- a/Packets/Implementation/SubmitProviderIdHandler.cpp +++ b/Packets/Implementation/SubmitProviderIdHandler.cpp @@ -5,59 +5,55 @@ #include #include #include +#include -SubmitProviderIdHandler::SubmitProviderIdHandler(const std::string& route) - : HTTPPacketProcessor(route) { +SubmitProviderIdHandler::SubmitProviderIdHandler(HTTPRequestIdentifier id) + : HTTPPacketProcessor(std::move(id)) { } static std::string GetSteamApiKey() { std::ifstream authFile(ResourcesUtilities::GetResourcesFolder() / "../" / "auth.json"); std::stringstream ss; ss << authFile.rdbuf(); - json authJson = json::parse(ss.str()); + nlohmann::json authJson = nlohmann::json::parse(ss.str()); return authJson.at("steamApiKey").get(); } -static std::string ClientIp(const tcp::socket& sock) { - return sock.remote_endpoint().address().to_string(); -} - -void SubmitProviderIdHandler::Process(const http::request& req, tcp::socket& sock) { - http::response res; - res.version(req.version()); - res.keep_alive(req.keep_alive()); - res.set(http::field::content_type, "application/json; charset=UTF-8"); - res.set(http::field::vary, "Origin"); - auto reply = [&](http::status st, std::string body) { - res.result(st); - res.body() = std::move(body); - res.prepare_payload(); - http::write(sock, res); - }; - if (req.method() != http::verb::post) { - res.set(http::field::allow, "POST"); - return reply(http::status::method_not_allowed, R"({"error":"method not allowed"})"); - } +std::optional SubmitProviderIdHandler::Process(const drogon::HttpRequestPtr& req) { if (GetSteamApiKey().empty()) { - return reply(http::status::bad_request, R"({"error":"steamApiKey not configured"})"); + auto res = HttpResponse::newHttpResponse(); + res->setBody("no steam api key provided"); + res->setStatusCode(k400BadRequest); + return res; } - const auto body = json::parse(req.body(), nullptr, false); + const auto body = nlohmann::json::parse(req->body(), nullptr, false); if (body.is_discarded() || !body.contains("providerId") || !body.at("providerId").is_string()) { - return reply(http::status::bad_request, R"({"error":"providerId required"})"); + auto res = HttpResponse::newHttpResponse(); + res->setBody("err: provider id required"); + res->setStatusCode(k400BadRequest); + return res; } const std::string steam64 = body.at("providerId"); if (steam64.empty()) { - return reply(http::status::bad_request, R"({"error":"providerId required"})"); + auto res = HttpResponse::newHttpResponse(); + res->setBody("err: steam id required"); + res->setStatusCode(k400BadRequest); + return res; } SteamValidator v(GetSteamApiKey()); auto info = v.ValidateSteamId(steam64); if (!info) { - return reply(http::status::bad_request, R"({"error":"invalid steam id"})"); + auto res = HttpResponse::newHttpResponse(); + res->setBody("err: invalid steam id"); + res->setStatusCode(k400BadRequest); + return res; } - AuthLatch::Get().Put(ClientIp(sock), steam64, /*latch timer in seconds*/ 120); + AuthLatch::Get().Put(req->peerAddr().toIp(), steam64, /*latch timer in seconds*/ 120); // 120s for now until i sort the launcher out - astro - return reply(http::status::ok, R"({"ok":true})"); -} \ No newline at end of file + auto res = HttpResponse::newHttpResponse(); + res->setBody(R"({"ok":true})"); + return res; +} diff --git a/Packets/Implementation/UpdateItemV4Processor.cpp b/Packets/Implementation/UpdateItemV4Processor.cpp index e2a87722..edf8388e 100644 --- a/Packets/Implementation/UpdateItemV4Processor.cpp +++ b/Packets/Implementation/UpdateItemV4Processor.cpp @@ -8,17 +8,17 @@ UpdateItemV4Processor::UpdateItemV4Processor(SpectreRpcType rpcType) : WebsocketPacketProcessor(rpcType) { } -static void SendSuccessfulUpdate(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { - std::shared_ptr resjson = packet.GetBaseJsonResponse(); - resjson->at("payload")["success"] = true; - sock.SendPacket(resjson); +static nlohmann::json SendSuccessfulUpdate() { + nlohmann::json res{}; + res["success"] = true; + return res; } -void UpdateItemV4Processor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional UpdateItemV4Processor::Process(SpectreWebsocketRequest& packet) { sql::Statement invQuery = PlayerDatabase::Get().FormatStatement( "SELECT {col} from {table} WHERE PlayerId = ?", FieldKey::PLAYER_INVENTORY); - invQuery.bind(1, sock.GetPlayerId()); + invQuery.bind(1, packet.GetPlayerId()); std::unique_ptr playerI = PlayerDatabase::Get().GetField(invQuery, FieldKey::PLAYER_INVENTORY); std::unique_ptr itemUpdate = packet.GetPayloadAsMessage(); FullInventory* playerInv = playerI->mutable_full(); @@ -32,8 +32,7 @@ void UpdateItemV4Processor::Process(SpectreWebsocketRequest& packet, SpectreWebs } if (curItem == nullptr) { spdlog::warn("Couldn't find item with instance id {} in a item update request, skipping", itemUpdate->instanceditemupdate().instanceid()); - SendSuccessfulUpdate(packet, sock); - return; + return SendSuccessfulUpdate(); } if (itemUpdate->instanceditemupdate().ext().setviewed()) { curItem->mutable_ext()->set_viewed(true); @@ -53,11 +52,10 @@ void UpdateItemV4Processor::Process(SpectreWebsocketRequest& packet, SpectreWebs } if (curItem == nullptr) { spdlog::warn("Couldn't find item with instance id {} in a item update request, skipping", itemUpdate->stackeditemupdate().instanceid()); - SendSuccessfulUpdate(packet, sock); - return; + return SendSuccessfulUpdate(); } curItem->set_amount(itemUpdate->stackeditemupdate().newamount()); } - PlayerDatabase::Get().SetField(FieldKey::PLAYER_INVENTORY, playerI.get(), sock.GetPlayerId()); - SendSuccessfulUpdate(packet, sock); + PlayerDatabase::Get().SetField(FieldKey::PLAYER_INVENTORY, playerI.get(), packet.GetPlayerId()); + return SendSuccessfulUpdate(); } \ No newline at end of file diff --git a/Packets/Implementation/UpdateItemsV0Processor.cpp b/Packets/Implementation/UpdateItemsV0Processor.cpp index 1fae239e..0b0c3cd8 100644 --- a/Packets/Implementation/UpdateItemsV0Processor.cpp +++ b/Packets/Implementation/UpdateItemsV0Processor.cpp @@ -16,26 +16,26 @@ static void PerformItemUpdate(InstancedItem& item, const InstancedItemUpdate& up item.mutable_ext()->set_viewed(update.ext().setviewed()); } -void UpdateItemsV0Processor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { - std::shared_ptr res = packet.GetBaseJsonResponse(); +std::optional UpdateItemsV0Processor::Process(SpectreWebsocketRequest& packet) { + nlohmann::json res{}; sql::Statement invQuery = PlayerDatabase::Get().FormatStatement( "SELECT {col} from {table} WHERE PlayerId = ?", FieldKey::PLAYER_INVENTORY); - invQuery.bind(1, sock.GetPlayerId()); + invQuery.bind(1, packet.GetPlayerId()); std::unique_ptr playerI = PlayerDatabase::Get().GetField(invQuery, FieldKey::PLAYER_INVENTORY); pbu::JsonPrintOptions opts; opts.always_print_fields_with_no_presence = true; FullInventory* playerInv = playerI->mutable_full(); int invLevel = stoi(playerInv->version()); playerInv->set_version(std::to_string(invLevel + 1)); - res->at("payload")["segment"]["removedStackables"] = json::array(); - res->at("payload")["segment"]["removedInstanced"] = json::array(); - res->at("payload")["segment"]["previousVersion"] = std::to_string(invLevel); - res->at("payload")["segment"]["instanced"] = json::array(); - res->at("payload")["segment"]["stackables"] = json::array(); - res->at("payload")["segment"]["version"] = playerInv->version(); - res->at("payload")["delta"]["instanced"] = json::array(); - res->at("payload")["delta"]["stackables"] = json::array(); + res["segment"]["removedStackables"] = nlohmann::json::array(); + res["segment"]["removedInstanced"] = nlohmann::json::array(); + res["segment"]["previousVersion"] = std::to_string(invLevel); + res["segment"]["instanced"] = nlohmann::json::array(); + res["segment"]["stackables"] = nlohmann::json::array(); + res["segment"]["version"] = playerInv->version(); + res["delta"]["instanced"] = nlohmann::json::array(); + res["delta"]["stackables"] = nlohmann::json::array(); std::unique_ptr itemUpdates = packet.GetPayloadAsMessage(); pbuf::util::JsonPrintOptions options; options.always_print_fields_with_no_presence = true; @@ -55,16 +55,16 @@ void UpdateItemsV0Processor::Process(SpectreWebsocketRequest& packet, SpectreWeb spdlog::warn("Couldn't find item with instance id {} in a item update request, skipping", instanceId); continue; } - json curDelta; + nlohmann::json curDelta; curDelta["catalogId"] = curItem->catalogid(); curDelta["operation"] = "UPDATED"; - curDelta["tags"] = json::array(); + curDelta["tags"] = nlohmann::json::array(); std::string itemInitialStr; if (!pbu::MessageToJsonString(*curItem, &itemInitialStr, opts).ok()) { spdlog::error("Failed to convert item to string"); throw std::runtime_error("Failed to convert item to string"); } - json itemInitial = json::parse(itemInitialStr); + nlohmann::json itemInitial = nlohmann::json::parse(itemInitialStr); curDelta["initial"] = itemInitial; PerformItemUpdate(*curItem, itemUpdate); std::string finalItemStr; @@ -72,11 +72,11 @@ void UpdateItemsV0Processor::Process(SpectreWebsocketRequest& packet, SpectreWeb spdlog::error("Failed to convert item to string"); throw std::runtime_error("Failed to convert item to string"); } - json finalItem = json::parse(finalItemStr); + nlohmann::json finalItem = nlohmann::json::parse(finalItemStr); curDelta["final"] = finalItem; - res->at("payload")["delta"]["instanced"].push_back(curDelta); - res->at("payload")["segment"]["instanced"].push_back(finalItem); + res["delta"]["instanced"].push_back(curDelta); + res["segment"]["instanced"].push_back(finalItem); } - PlayerDatabase::Get().SetField(FieldKey::PLAYER_INVENTORY, playerI.get(), sock.GetPlayerId()); - sock.SendPacket(res); + PlayerDatabase::Get().SetField(FieldKey::PLAYER_INVENTORY, playerI.get(), packet.GetPlayerId()); + return res; } diff --git a/Packets/Implementation/UpdatePartyPlayerProcessor.cpp b/Packets/Implementation/UpdatePartyPlayerProcessor.cpp index 5402146b..b9bd4de2 100644 --- a/Packets/Implementation/UpdatePartyPlayerProcessor.cpp +++ b/Packets/Implementation/UpdatePartyPlayerProcessor.cpp @@ -7,24 +7,24 @@ UpdatePartyPlayerProcessor::UpdatePartyPlayerProcessor(SpectreRpcType rpcType) : WebsocketPacketProcessor(rpcType) { } -void UpdatePartyPlayerProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional UpdatePartyPlayerProcessor::Process(SpectreWebsocketRequest& packet) { std::unique_ptr req = packet.GetPayloadAsMessage(); PartyResponse res = PartyDatabase::Get().GetPartyRes(req->partyid()); Party* party = res.mutable_party(); PartyMemberExtraInfo* memberExt = nullptr; for (int i = 0; i < party->partymembers_size(); i++) { - if (party->partymembers(i).playerid() == sock.GetPlayerId()) { + if (party->partymembers(i).playerid() == packet.GetPlayerId()) { memberExt = party->mutable_partymembers(i)->mutable_ext(); } } if (memberExt == nullptr) { spdlog::warn("could not find player(id: {}) in party(id: {}) that was being updated by UpdatePartyPlayerProcessor"); - return; + return std::nullopt; } memberExt->set_version(req->requestext().version()); memberExt->set_region(req->requestext().region()); memberExt->set_preferredteam(req->requestext().preferredteam()); // ignoring refreshData, preferredRegions, regionPings, sharedClientData for the moment as they don't seem to be important or used for customgame PartyDatabase::Get().SaveParty(res.party()); - sock.SendPacket(PartyDatabase::SerializePartyToString(res), packet.GetRequestId(), packet.GetResponseType()); + return PartyDatabase::SerializePartyToString(res); } \ No newline at end of file diff --git a/Packets/Implementation/UpdatePartyProcessor.cpp b/Packets/Implementation/UpdatePartyProcessor.cpp index 8c2ce9c4..685df009 100644 --- a/Packets/Implementation/UpdatePartyProcessor.cpp +++ b/Packets/Implementation/UpdatePartyProcessor.cpp @@ -22,7 +22,7 @@ namespace { } } // namespace -void UpdatePartyProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) { +std::optional UpdatePartyProcessor::Process(SpectreWebsocketRequest& packet) { std::unique_ptr req = packet.GetPayloadAsMessage(); PartyResponse res = PartyDatabase::Get().GetPartyRes(req->partyid()); Party* party = res.mutable_party(); @@ -51,5 +51,5 @@ void UpdatePartyProcessor::Process(SpectreWebsocketRequest& packet, SpectreWebso } BumpPartyVersion(party); PartyDatabase::Get().SaveParty(res.party()); - sock.SendPacket(PartyDatabase::SerializePartyToString(res), packet.GetRequestId(), packet.GetResponseType()); + return PartyDatabase::SerializePartyToString(res); } diff --git a/Packets/Implementation/UpdatePresenceForPlayerHandler.cpp b/Packets/Implementation/UpdatePresenceForPlayerHandler.cpp new file mode 100644 index 00000000..1cf755ea --- /dev/null +++ b/Packets/Implementation/UpdatePresenceForPlayerHandler.cpp @@ -0,0 +1,22 @@ +#include "PlayerDatabase.h" +#include "PlayerPresence.pb.h" +#include "SetPlayerPresenceHandler.h" + +#include +#include + +UpdatePresenceForPlayerHandler::UpdatePresenceForPlayerHandler(SpectreRpcType rpcType) + : WebsocketPacketProcessor(rpcType) { +} + +std::optional UpdatePresenceForPlayerHandler::Process(SpectreWebsocketRequest& packet) { + std::unique_ptr request = packet.GetPayloadAsMessage(); + std::unique_ptr playerPresence = PlayerDatabase::Get().GetField(FieldKey::PLAYER_PRESENCE, packet.GetPlayerId()); + playerPresence->mutable_ext()->set_mainpresenceid(request->playerpresence().mainpresenceid()); + playerPresence->mutable_ext()->set_lastupdated(std::to_string(std::chrono::system_clock::now().time_since_epoch().count())); + playerPresence->mutable_ext()->set_presencecontext(request->playerpresence().presencecontext()); + PlayerDatabase::Get().SetField(FieldKey::PLAYER_PRESENCE, playerPresence.get(), packet.GetPlayerId()); + nlohmann::json res{}; + res["success"] = true; + return WebsocketPayload(res, {CreatePresenceNotification(*playerPresence, packet.GetPlayerId())}); +} \ No newline at end of file diff --git a/Packets/Implementation/WebsocketPayload.cpp b/Packets/Implementation/WebsocketPayload.cpp new file mode 100644 index 00000000..f77296b1 --- /dev/null +++ b/Packets/Implementation/WebsocketPayload.cpp @@ -0,0 +1,59 @@ +#include +#include +#include +#include + +const std::string& WebsocketPayload::GetPayload() const { + return payload; +} + +WebsocketPayload::WebsocketPayload(std::string payload) + : WebsocketPayload(std::move(payload), {}) { +} + +WebsocketPayload::WebsocketPayload(const nlohmann::json& json) + : WebsocketPayload(json, {}) { +} + +WebsocketPayload::WebsocketPayload(std::string payload, std::initializer_list postSendNotifications) + : payload(std::move(payload)), postSendNotifications(postSendNotifications) { +} + +WebsocketPayload::WebsocketPayload(const nlohmann::json& json, std::initializer_list postSendNotifications) + : payload(json.dump()), postSendNotifications(postSendNotifications) { +} + +WebsocketPayload::WebsocketPayload(const google::protobuf::Message& message, std::initializer_list postSendNotifications) + : postSendNotifications(postSendNotifications) { + static google::protobuf::util::JsonPrintOptions opts{}; + opts.always_print_fields_with_no_presence = true; + if (!google::protobuf::util::MessageToJsonString(message, &payload, opts).ok()) { + spdlog::error("Failed to convert protobuf message to websocket payload"); + payload = ""; + } +} + +WebsocketPayload::WebsocketPayload(std::string message, std::vector postSendNotifications) + : payload(std::move(std::move(message))), postSendNotifications(std::move(std::move(postSendNotifications))) { +} + +WebsocketPayload::WebsocketPayload(const google::protobuf::Message& message, std::vector postSendNotifications) + : postSendNotifications(std::move(std::move(postSendNotifications))) { + static google::protobuf::util::JsonPrintOptions opts{}; + opts.always_print_fields_with_no_presence = true; + if (!google::protobuf::util::MessageToJsonString(message, &payload, opts).ok()) { + spdlog::error("Failed to convert protobuf message to websocket payload"); + payload = ""; + } +} + +WebsocketPayload::WebsocketPayload(const nlohmann::json& json, std::vector postSendNotifications) + : payload(json.dump()), postSendNotifications(std::move(std::move(postSendNotifications))) { +} + +WebsocketPayload::WebsocketPayload(const google::protobuf::Message& message) + : WebsocketPayload(message, {}) {} + +const std::vector& WebsocketPayload::GetPostSendNotifications() const { + return postSendNotifications; +} \ No newline at end of file diff --git a/Packets/Model/AuthLatch.h b/Packets/Model/AuthLatch.h index 9510a763..eff7c2a1 100644 --- a/Packets/Model/AuthLatch.h +++ b/Packets/Model/AuthLatch.h @@ -14,6 +14,7 @@ class AuthLatch { static AuthLatch& Get(); void Put(const std::string& ip, const std::string& providerId, int seconds); std::string TakeIfFresh(const std::string& ip); + void Clear(); private: std::mutex mtx; diff --git a/Packets/Model/AuthenticateHandler.h b/Packets/Model/AuthenticateHandler.h index a8a920eb..0f027635 100644 --- a/Packets/Model/AuthenticateHandler.h +++ b/Packets/Model/AuthenticateHandler.h @@ -3,8 +3,8 @@ class AuthenticateHandler : public HTTPPacketProcessor { public: - explicit AuthenticateHandler(std::string route); - void Process(const http::request& req, tcp::socket& sock) override; + explicit AuthenticateHandler(HTTPRequestIdentifier id); + std::optional Process(const drogon::HttpRequestPtr& req) override; private: static std::string CreatePlayerFromSteam(const std::string& steam64, const std::string& displayName); diff --git a/Packets/Model/CreatePartyProcessor.h b/Packets/Model/CreatePartyProcessor.h index a6cafe34..1d4333c5 100644 --- a/Packets/Model/CreatePartyProcessor.h +++ b/Packets/Model/CreatePartyProcessor.h @@ -11,5 +11,5 @@ class CreatePartyProcessor : public WebsocketPacketProcessor { public: explicit CreatePartyProcessor(const SpectreRpcType& rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/EnterMatchmakingProcessor.h b/Packets/Model/EnterMatchmakingProcessor.h index e0b4dcaa..97ad1f01 100644 --- a/Packets/Model/EnterMatchmakingProcessor.h +++ b/Packets/Model/EnterMatchmakingProcessor.h @@ -4,5 +4,5 @@ class EnterMatchmakingProcessor : public WebsocketPacketProcessor { public: explicit EnterMatchmakingProcessor(const SpectreRpcType& rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/FetchPlayerLoadoutsProcessor.h b/Packets/Model/FetchPlayerLoadoutsProcessor.h index 491b8a66..edfb66c4 100644 --- a/Packets/Model/FetchPlayerLoadoutsProcessor.h +++ b/Packets/Model/FetchPlayerLoadoutsProcessor.h @@ -4,5 +4,5 @@ class FetchPlayerLoadoutsProcessor : public WebsocketPacketProcessor { public: explicit FetchPlayerLoadoutsProcessor(const SpectreRpcType& rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/FieldFetchProcessor.h b/Packets/Model/FieldFetchProcessor.h index 9f57270f..2b879b7a 100644 --- a/Packets/Model/FieldFetchProcessor.h +++ b/Packets/Model/FieldFetchProcessor.h @@ -20,15 +20,15 @@ class FieldFetchProcessor : public WebsocketPacketProcessor { : FieldFetchProcessor(rpcType, key, PlayerDatabase::Get()) { } - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override { + std::optional Process(SpectreWebsocketRequest& packet) override { sql::Statement query = dbRef.FormatStatement("SELECT {col} FROM {table} WHERE PlayerId=? LIMIT 1", field); - query.bind(1, sock.GetPlayerId()); + query.bind(1, packet.GetPlayerId()); std::unique_ptr data = dbRef.GetField(query, field); if (data == nullptr) { spdlog::error("No field value to return"); throw; } - sock.SendPacket(*data, packet.GetResponseType(), packet.GetRequestId()); + return *data; } static_assert(std::is_base_of::value, "Type provided to FieldFetchProcessor must inherit from protobuf::Message"); }; \ No newline at end of file diff --git a/Packets/Model/GetBulkProfileDataProcessor.h b/Packets/Model/GetBulkProfileDataProcessor.h index 99b6507e..e7931fff 100644 --- a/Packets/Model/GetBulkProfileDataProcessor.h +++ b/Packets/Model/GetBulkProfileDataProcessor.h @@ -5,5 +5,5 @@ class GetBulkProfileDataProcessor : public WebsocketPacketProcessor { public: explicit GetBulkProfileDataProcessor(const SpectreRpcType& rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/GetFriendsListAndRegisterOnlineHandler.h b/Packets/Model/GetFriendsListAndRegisterOnlineHandler.h new file mode 100644 index 00000000..be481a98 --- /dev/null +++ b/Packets/Model/GetFriendsListAndRegisterOnlineHandler.h @@ -0,0 +1,9 @@ +#pragma once +#include + +class GetFriendsListAndRegisterOnlineHandler : public WebsocketPacketProcessor { + public: + explicit GetFriendsListAndRegisterOnlineHandler(SpectreRpcType rpcType); + + std::optional Process(SpectreWebsocketRequest& packet) override; +}; \ No newline at end of file diff --git a/Packets/Model/GetLoginDataProcessor.h b/Packets/Model/GetLoginDataProcessor.h index c68f1269..6f1a4cea 100644 --- a/Packets/Model/GetLoginDataProcessor.h +++ b/Packets/Model/GetLoginDataProcessor.h @@ -5,5 +5,5 @@ class GetLoginDataProcessor : public WebsocketPacketProcessor { public: explicit GetLoginDataProcessor(const SpectreRpcType& rpcType); - void Process(SpectreWebsocketRequest& request, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/GetPlayerDataProcessor.h b/Packets/Model/GetPlayerDataProcessor.h index 9e23931a..c2491e2e 100644 --- a/Packets/Model/GetPlayerDataProcessor.h +++ b/Packets/Model/GetPlayerDataProcessor.h @@ -6,6 +6,6 @@ class GetPlayerDataProcessor : public WebsocketPacketProcessor { public: explicit GetPlayerDataProcessor(const SpectreRpcType& rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; static std::string GetPlayerDataAsString(const PlayerData& playerData); }; \ No newline at end of file diff --git a/Packets/Model/HTTPRequestIdentifier.h b/Packets/Model/HTTPRequestIdentifier.h new file mode 100644 index 00000000..51b6ff03 --- /dev/null +++ b/Packets/Model/HTTPRequestIdentifier.h @@ -0,0 +1,32 @@ +#pragma once +#include +#include +#include + +class HTTPRequestIdentifier { + private: + std::string route; + drogon::HttpMethod reqType; + + public: + explicit HTTPRequestIdentifier(std::string route, drogon::HttpMethod reqType); + HTTPRequestIdentifier(std::string route); + bool operator==(const HTTPRequestIdentifier& other) const; + [[nodiscard]] const std::string& GetRoute() const; + [[nodiscard]] const drogon::HttpMethod& GetRequestType() const; +}; + +namespace std { + template <> + struct hash { + std::size_t operator()(const HTTPRequestIdentifier& requestIdentifier) const noexcept { + std::size_t h1 = std::hash{}(requestIdentifier.GetRoute()); + std::size_t h2 = std::hash{}( + static_cast(requestIdentifier.GetRequestType())); + + // Combine hashes + h1 ^= h2 + 0x9e3779b97f4a7c15ULL + (h1 << 6) + (h1 >> 2); + return h1; + } + }; +} // namespace std \ No newline at end of file diff --git a/Packets/Model/HeartbeatProcessor.h b/Packets/Model/HeartbeatProcessor.h index 4bfeecf2..634a3a04 100644 --- a/Packets/Model/HeartbeatProcessor.h +++ b/Packets/Model/HeartbeatProcessor.h @@ -3,9 +3,6 @@ class HeartbeatProcessor : public WebsocketPacketProcessor { public: - explicit HeartbeatProcessor(const SpectreRpcType& rpcType) - : WebsocketPacketProcessor(rpcType){}; - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& /*sock*/) override { - packet.SendEmptyResponse(); - } + explicit HeartbeatProcessor(const SpectreRpcType& rpcType); + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/IsInPartyHandler.h b/Packets/Model/IsInPartyHandler.h index a8671e5b..ba96df0a 100644 --- a/Packets/Model/IsInPartyHandler.h +++ b/Packets/Model/IsInPartyHandler.h @@ -4,5 +4,5 @@ class IsInPartyHandler : public WebsocketPacketProcessor { public: explicit IsInPartyHandler(const SpectreRpcType& rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/Notification.h b/Packets/Model/Notification.h index d1d1a74c..80e020f2 100644 --- a/Packets/Model/Notification.h +++ b/Packets/Model/Notification.h @@ -1,17 +1,17 @@ #pragma once #include -#include +#include class Notification { private: SpectreRpcType notificationType; + const std::string notificationId; + std::string notificationData; public: - virtual ~Notification() = default; - Notification(Notification& other) = default; - Notification(Notification&& other) = default; - explicit Notification(SpectreRpcType notificationType); - - [[nodiscard]] const SpectreRpcType& GetNotificationType() const; - virtual void SendTo(SpectreWebsocket& sock) const = 0; + Notification(const SpectreRpcType& notificationType, const google::protobuf::Message& notificationData); + Notification(const SpectreRpcType& notificationType, std::string notificationId, std::string notificationPayload); + const SpectreRpcType& GetNotificationType() const; + const std::string& GetNotificationId() const; + const std::string& GetNotificationData() const; }; \ No newline at end of file diff --git a/Packets/Model/PacketProcessor.h b/Packets/Model/PacketProcessor.h index bc1b6473..41aa9e05 100644 --- a/Packets/Model/PacketProcessor.h +++ b/Packets/Model/PacketProcessor.h @@ -1,32 +1,29 @@ #pragma once +#include #include #include -#include -#include +#include +#include #include +#include + +using namespace drogon; class HTTPPacketProcessor { private: - std::string route; - inline static std::unordered_map httpRoutes = {}; - + HTTPRequestIdentifier routeId; public: - explicit HTTPPacketProcessor(std::string route) - : route(std::move(route)) { - httpRoutes[this->route] = this; - }; + virtual ~HTTPPacketProcessor() = default; + explicit HTTPPacketProcessor(HTTPRequestIdentifier routeId); + explicit HTTPPacketProcessor(HTTPRequestIdentifier routeId, uint16_t port); HTTPPacketProcessor(HTTPPacketProcessor& other) = delete; HTTPPacketProcessor(HTTPPacketProcessor&& other) = delete; - virtual void Process(const http::request& req, tcp::socket& sock) = 0; - virtual ~HTTPPacketProcessor() { - httpRoutes.erase(route); - } + virtual std::optional Process(const drogon::HttpRequestPtr& req) = 0; [[nodiscard]] const std::string& GetRoute() const { - return route; + return routeId.GetRoute(); } - static HTTPPacketProcessor* GetProcessorForRoute(const std::string& route) { - auto it = httpRoutes.find(route); - return it == httpRoutes.end() ? nullptr : it->second; + [[nodiscard]] drogon::HttpMethod GetMethod() const { + return routeId.GetRequestType(); } }; @@ -40,7 +37,7 @@ class WebsocketPacketProcessor { : rpcType(rpcType) { websocketRoutes[rpcType] = this; } - virtual void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) = 0; + virtual std::optional Process(SpectreWebsocketRequest& packet) = 0; virtual ~WebsocketPacketProcessor() = default; [[nodiscard]] const SpectreRpcType& GetType() const { return rpcType; diff --git a/Packets/Model/PartyDetailsNotification.h b/Packets/Model/PartyDetailsNotification.h deleted file mode 100644 index c7576646..00000000 --- a/Packets/Model/PartyDetailsNotification.h +++ /dev/null @@ -1,15 +0,0 @@ -#pragma once -#include -#include - -class PartyDetailsNotification : public Notification { - private: - PartyResponse payload; - - public: - PartyDetailsNotification(const std::string& partyId, SpectreRpcType notificationType); - PartyDetailsNotification(PartyResponse partyRes, SpectreRpcType notificationType); - PartyDetailsNotification(const Party& party, SpectreRpcType notificationType); - - void SendTo(SpectreWebsocket& sock) const override; -}; \ No newline at end of file diff --git a/Packets/Model/RegexPayloadProcessorHTTP.h b/Packets/Model/RegexPayloadProcessorHTTP.h index 55752228..281d0e5d 100644 --- a/Packets/Model/RegexPayloadProcessorHTTP.h +++ b/Packets/Model/RegexPayloadProcessorHTTP.h @@ -6,11 +6,11 @@ class RegexPayloadProcessorHTTP : public HTTPPacketProcessor { private: - std::unordered_map> resMap; + std::unordered_map> resMap; public: - RegexPayloadProcessorHTTP(const std::string& route, const std::unordered_map>& resMap) - : HTTPPacketProcessor(route), resMap(resMap){}; + RegexPayloadProcessorHTTP(HTTPRequestIdentifier id, const std::unordered_map>& resMap) + : HTTPPacketProcessor(id), resMap(resMap){}; - void Process(const http::request& req, tcp::socket& sock) override; + std::optional Process(const drogon::HttpRequestPtr& req) override; }; \ No newline at end of file diff --git a/Packets/Model/RegexPayloadProcessorWS.h b/Packets/Model/RegexPayloadProcessorWS.h index 726d0c0c..56fe7555 100644 --- a/Packets/Model/RegexPayloadProcessorWS.h +++ b/Packets/Model/RegexPayloadProcessorWS.h @@ -8,11 +8,11 @@ class RegexPayloadProcessorWS : public WebsocketPacketProcessor { private: - std::unordered_map> resMap; + std::unordered_map> resMap; public: - RegexPayloadProcessorWS(const SpectreRpcType& rpcType, const std::unordered_map>& resMap) + RegexPayloadProcessorWS(const SpectreRpcType& rpcType, const std::unordered_map>& resMap) : WebsocketPacketProcessor(rpcType), resMap(resMap){}; - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/SaveOutfitLoadoutProcessor.h b/Packets/Model/SaveOutfitLoadoutProcessor.h index b86da07e..163c48da 100644 --- a/Packets/Model/SaveOutfitLoadoutProcessor.h +++ b/Packets/Model/SaveOutfitLoadoutProcessor.h @@ -5,5 +5,5 @@ class SaveOutfitLoadoutProcessor : public WebsocketPacketProcessor { public: explicit SaveOutfitLoadoutProcessor(SpectreRpcType rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/SavePlayerDataProcessor.h b/Packets/Model/SavePlayerDataProcessor.h index b1adadbf..f34f0a28 100644 --- a/Packets/Model/SavePlayerDataProcessor.h +++ b/Packets/Model/SavePlayerDataProcessor.h @@ -5,5 +5,5 @@ class SavePlayerDataProcessor : public WebsocketPacketProcessor { public: explicit SavePlayerDataProcessor(SpectreRpcType rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/SaveWeaponLoadoutProcessor.h b/Packets/Model/SaveWeaponLoadoutProcessor.h index dbcac186..c7469325 100644 --- a/Packets/Model/SaveWeaponLoadoutProcessor.h +++ b/Packets/Model/SaveWeaponLoadoutProcessor.h @@ -5,5 +5,5 @@ class SaveWeaponLoadoutProcessor : public WebsocketPacketProcessor { public: explicit SaveWeaponLoadoutProcessor(SpectreRpcType rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/ServerMainThread.h b/Packets/Model/ServerMainThread.h new file mode 100644 index 00000000..8c651f86 --- /dev/null +++ b/Packets/Model/ServerMainThread.h @@ -0,0 +1,6 @@ +#pragma once +#include +#include + +extern std::atomic_bool serverOnline; +int MainThread(int argc, char** argv, std::stop_token st); \ No newline at end of file diff --git a/Packets/Model/SetPlayerPresenceHandler.h b/Packets/Model/SetPlayerPresenceHandler.h new file mode 100644 index 00000000..50724e52 --- /dev/null +++ b/Packets/Model/SetPlayerPresenceHandler.h @@ -0,0 +1,13 @@ +#pragma once +#include "PlayerPresence.pb.h" + +#include + +class SetPlayerPresenceHandler : public WebsocketPacketProcessor { + public: + explicit SetPlayerPresenceHandler(SpectreRpcType rpcType); + + std::optional Process(SpectreWebsocketRequest& packet) override; +}; + +Notification CreatePresenceNotification(PlayerPresence& newPresence, const std::string& playerId); \ No newline at end of file diff --git a/Packets/Model/SetReadyProcessor.h b/Packets/Model/SetReadyProcessor.h index 8e109d5b..978cc24a 100644 --- a/Packets/Model/SetReadyProcessor.h +++ b/Packets/Model/SetReadyProcessor.h @@ -4,5 +4,5 @@ class SetReadyProcessor : public WebsocketPacketProcessor { public: explicit SetReadyProcessor(SpectreRpcType rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/SpectreWebsocket.h b/Packets/Model/SpectreWebsocket.h index 8c33aed7..851477c3 100644 --- a/Packets/Model/SpectreWebsocket.h +++ b/Packets/Model/SpectreWebsocket.h @@ -1,42 +1,56 @@ #pragma once +#include #include -#include -#include -#include -#include +#include +#include +#include #include #include -using tcp = boost::asio::ip::tcp; -namespace http = boost::beast::http; -using ws = boost::beast::websocket::stream; using json = nlohmann::ordered_json; namespace pbuf = google::protobuf; +using namespace drogon; class SpectreWebsocket { - private: - ws& socket; // NOLINT - int curSequenceNumber; + std::atomic_int curSequenceNumber; std::string playerId; + std::deque notificationsToDeliver; + std::mutex notificationQueueLock; + std::condition_variable notificationQueueCondition; + std::jthread notificationWorkerThread; + std::weak_ptr con; + void NotificationThread(const std::stop_token& st); public: - SpectreWebsocket(ws& sock, const http::request& req); - /* - Warning: Do not send packets through the socket directly, it bypasses abstraction and will cause bad things to happen - */ - [[nodiscard]] const ws& GetRawSocket() const; + explicit SpectreWebsocket(const drogon::HttpRequestPtr& req, const drogon::WebSocketConnectionPtr& con); - void SendPacket(const std::shared_ptr& res); + std::string FormulateFinalResponse(const std::shared_ptr& res); - void SendPacket(const std::string& resPayload, int requestId, const std::string& resType); + std::string FormulateFinalResponse(const std::string& resPayload, int requestId, const std::string& resType); - void SendPacket(const pbuf::Message& payload, const std::string& resType, int requestId); + std::string FormulateFinalResponse(const pbuf::Message& payload, const std::string& resType, int requestId); - void SendNotification(const std::shared_ptr& notif, const SpectreRpcType& notificationType); + std::string FormulateFinalNotification(Notification& notification); - void SendNotification(const std::string& notifPayload, const SpectreRpcType& notificationType); + const std::string& GetPlayerId(); - void SendNotification(const pbuf::Message& notif, const SpectreRpcType& notificationType); + void ScheduleNotification(const Notification& notif); +}; - const std::string& GetPlayerId(); -}; \ No newline at end of file +class SpectreWebsocketController : public WebSocketController { + private: + static std::unordered_map connectionsByPlayerId; + static std::mutex connectionsMapMutex; + + public: + void handleNewConnection(const drogon::HttpRequestPtr& req, const WebSocketConnectionPtr& con) override; + void handleNewMessage(const drogon::WebSocketConnectionPtr& con, std::string&& message, const WebSocketMessageType& messageType) override; + void handleConnectionClosed(const WebSocketConnectionPtr& conPtr) override; + static std::optional GetConnectionForPlayer(const std::string& playerId); + static void ScheduleNotificationForPlayer(const std::string& playerId, const Notification& notif); + static void AddConnection(const std::string& playerId, WebSocketConnectionPtr con); + WS_PATH_LIST_BEGIN + WS_PATH_ADD("/"); + WS_PATH_ADD("/v1/rpc"); + WS_PATH_LIST_END +}; diff --git a/Packets/Model/SpectreWebsocketRequest.h b/Packets/Model/SpectreWebsocketRequest.h index a5ae6acf..b0bf3526 100644 --- a/Packets/Model/SpectreWebsocketRequest.h +++ b/Packets/Model/SpectreWebsocketRequest.h @@ -1,6 +1,5 @@ #pragma once #include -#include #include #include #include @@ -11,17 +10,17 @@ using reqbuf = boost::beast::flat_buffer; class SpectreWebsocketRequest { private: - SpectreWebsocket& websocket; - reqbuf requestBuf; + std::string reqBody; SpectreRpcType requestType; - std::shared_ptr reqJson; + std::shared_ptr reqJson; std::string payloadAsStr; - int m_requestId; + int requestId; + std::string playerId; public: - SpectreWebsocketRequest(SpectreWebsocket& sock, reqbuf req); + SpectreWebsocketRequest(std::string& reqBody, std::string& playerId); - std::shared_ptr GetPayload() const; + std::shared_ptr GetPayload() const; template std::unique_ptr GetPayloadAsMessage() { @@ -37,24 +36,13 @@ class SpectreWebsocketRequest { return std::make_unique(message); } - reqbuf* GetRawBuffer() { - return &requestBuf; - } - - [[nodiscard]] SpectreWebsocket& GetSocket() const { - return websocket; - } - - [[nodiscard]] SpectreRpcType GetRequestType() const { - return requestType; - } + [[nodiscard]] SpectreRpcType GetRequestType() const; std::string GetResponseType() const; - [[nodiscard]] int GetRequestId() const { - return m_requestId; - } + [[nodiscard]] int GetRequestId() const; + + const std::string& GetBody() const; - std::shared_ptr GetBaseJsonResponse(); - void SendEmptyResponse(); + const std::string& GetPlayerId() const; }; \ No newline at end of file diff --git a/Packets/Model/StaticHTTPPackets.h b/Packets/Model/StaticHTTPPackets.h new file mode 100644 index 00000000..3476d205 --- /dev/null +++ b/Packets/Model/StaticHTTPPackets.h @@ -0,0 +1,6 @@ +#pragma once +#include + +void RegisterStaticHTTPHandlerFromFile(std::string route, std::string filename); + +void RegisterStaticHTTPHandlers(); \ No newline at end of file diff --git a/Packets/Model/StaticResponseProcessorHTTP.h b/Packets/Model/StaticResponseProcessorHTTP.h index 92092aae..90f8c69a 100644 --- a/Packets/Model/StaticResponseProcessorHTTP.h +++ b/Packets/Model/StaticResponseProcessorHTTP.h @@ -2,15 +2,12 @@ #include -namespace http = boost::beast::http; - class StaticResponseProcessorHTTP : public HTTPPacketProcessor { private: - std::shared_ptr staticRes; + std::string staticRes; public: - StaticResponseProcessorHTTP(const std::string& route, const std::shared_ptr& res) - : HTTPPacketProcessor(route), staticRes(res){}; + StaticResponseProcessorHTTP(HTTPRequestIdentifier id, const nlohmann::json& res); - void Process(const http::request& req, tcp::socket& sock) override; + std::optional Process(const drogon::HttpRequestPtr& req) override; }; \ No newline at end of file diff --git a/Packets/Model/StaticResponseProcessorWS.h b/Packets/Model/StaticResponseProcessorWS.h index 99305f39..716f9304 100644 --- a/Packets/Model/StaticResponseProcessorWS.h +++ b/Packets/Model/StaticResponseProcessorWS.h @@ -5,11 +5,10 @@ class StaticResponseProcessorWS : public WebsocketPacketProcessor { private: - std::shared_ptr staticRes; + std::string staticRes; public: - StaticResponseProcessorWS(const SpectreRpcType rpcType, const std::shared_ptr& res) - : WebsocketPacketProcessor(rpcType), staticRes(res){}; + StaticResponseProcessorWS(const SpectreRpcType rpcType, const nlohmann::json& res); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/StaticWSPackets.h b/Packets/Model/StaticWSPackets.h new file mode 100644 index 00000000..04cfa72c --- /dev/null +++ b/Packets/Model/StaticWSPackets.h @@ -0,0 +1,7 @@ +#pragma once +#include "Regex.h" +#include "SpectreRpcType.h" + +void RegisterStaticHandlerFromFile(std::string filename, SpectreRpcType rpcType); +void RegisterRegexHandlerFromFiles(SpectreRpcType rpcType, std::initializer_list> map); +void RegisterStaticWSHandlers(); \ No newline at end of file diff --git a/Packets/Model/SubmitProviderIdHandler.h b/Packets/Model/SubmitProviderIdHandler.h index ed2212f0..1e7cd79c 100644 --- a/Packets/Model/SubmitProviderIdHandler.h +++ b/Packets/Model/SubmitProviderIdHandler.h @@ -3,7 +3,7 @@ class SubmitProviderIdHandler : public HTTPPacketProcessor { public: - explicit SubmitProviderIdHandler(const std::string& route); + explicit SubmitProviderIdHandler(HTTPRequestIdentifier id); - void Process(const http::request& req, tcp::socket& sock) override; + std::optional Process(const drogon::HttpRequestPtr& req) override; }; \ No newline at end of file diff --git a/Packets/Model/UpdateItemV4Processor.h b/Packets/Model/UpdateItemV4Processor.h index a5d52a59..f686aea3 100644 --- a/Packets/Model/UpdateItemV4Processor.h +++ b/Packets/Model/UpdateItemV4Processor.h @@ -5,5 +5,5 @@ class UpdateItemV4Processor : public WebsocketPacketProcessor { public: explicit UpdateItemV4Processor(SpectreRpcType rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/UpdateItemsV0Processor.h b/Packets/Model/UpdateItemsV0Processor.h index 932e16ae..062c5abd 100644 --- a/Packets/Model/UpdateItemsV0Processor.h +++ b/Packets/Model/UpdateItemsV0Processor.h @@ -5,5 +5,5 @@ class UpdateItemsV0Processor : public WebsocketPacketProcessor { public: explicit UpdateItemsV0Processor(SpectreRpcType rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/UpdatePartyPlayerProcessor.h b/Packets/Model/UpdatePartyPlayerProcessor.h index 34246d8e..a7c58cb0 100644 --- a/Packets/Model/UpdatePartyPlayerProcessor.h +++ b/Packets/Model/UpdatePartyPlayerProcessor.h @@ -4,5 +4,5 @@ class UpdatePartyPlayerProcessor : public WebsocketPacketProcessor { public: explicit UpdatePartyPlayerProcessor(SpectreRpcType rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/UpdatePartyProcessor.h b/Packets/Model/UpdatePartyProcessor.h index 3c0b3196..f4f389ee 100644 --- a/Packets/Model/UpdatePartyProcessor.h +++ b/Packets/Model/UpdatePartyProcessor.h @@ -4,5 +4,5 @@ class UpdatePartyProcessor : public WebsocketPacketProcessor { public: explicit UpdatePartyProcessor(SpectreRpcType rpcType); - void Process(SpectreWebsocketRequest& packet, SpectreWebsocket& sock) override; + std::optional Process(SpectreWebsocketRequest& packet) override; }; \ No newline at end of file diff --git a/Packets/Model/UpdatePresenceForPlayerHandler.h b/Packets/Model/UpdatePresenceForPlayerHandler.h new file mode 100644 index 00000000..cb905711 --- /dev/null +++ b/Packets/Model/UpdatePresenceForPlayerHandler.h @@ -0,0 +1,8 @@ +#pragma once +#include + +class UpdatePresenceForPlayerHandler : public WebsocketPacketProcessor { + public: + explicit UpdatePresenceForPlayerHandler(SpectreRpcType rpcType); + std::optional Process(SpectreWebsocketRequest& packet) override; +}; \ No newline at end of file diff --git a/Packets/Model/WebsocketPayload.h b/Packets/Model/WebsocketPayload.h new file mode 100644 index 00000000..48c4e05b --- /dev/null +++ b/Packets/Model/WebsocketPayload.h @@ -0,0 +1,25 @@ +#pragma once +#include "Notification.h" + +#include +#include +#include + +class WebsocketPayload { + private: + std::string payload; + std::vector postSendNotifications{}; + + public: + WebsocketPayload(const google::protobuf::Message& message); + WebsocketPayload(std::string payload); + WebsocketPayload(const nlohmann::json& json); + WebsocketPayload(const google::protobuf::Message& message, std::initializer_list postSendNotifications); + WebsocketPayload(const nlohmann::json& json, std::initializer_list postSendNotifications); + WebsocketPayload(std::string payload, std::initializer_list postSendNotifications); + WebsocketPayload(const google::protobuf::Message& message, std::vector postSendNotifications); + WebsocketPayload(const nlohmann::json& json, std::vector postSendNotifications); + WebsocketPayload(std::string message, std::vector postSendNotifications); + [[nodiscard]] const std::string& GetPayload() const; + [[nodiscard]] const std::vector& GetPostSendNotifications() const; +}; \ No newline at end of file diff --git a/Packets/Protoc/GetFriendsListAndRegisterOnlineResponse.proto b/Packets/Protoc/GetFriendsListAndRegisterOnlineResponse.proto new file mode 100644 index 00000000..cce56570 --- /dev/null +++ b/Packets/Protoc/GetFriendsListAndRegisterOnlineResponse.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; +import "FriendsList.proto"; + +message GetFriendsListAndRegisterOnlineResponse { + FriendsList friendList = 1; +} \ No newline at end of file diff --git a/Packets/Protoc/PresenceUpdateNotification.proto b/Packets/Protoc/PresenceUpdateNotification.proto new file mode 100644 index 00000000..83b33b34 --- /dev/null +++ b/Packets/Protoc/PresenceUpdateNotification.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; +import "PlayerPresence.proto"; + +message PresenceUpdateNotification { + PlayerFullPresence newPresence = 1; +} + +message PlayerFullPresence { + string playerId = 1; + string gameShardId = 2; + string gameTitleId = 3; + PlayerBasicPresence basicPresence = 4; + string version = 5; + PlayerPresenceExt ext = 6; +} diff --git a/Packets/Protoc/SetPresenceRequest.proto b/Packets/Protoc/SetPresenceRequest.proto new file mode 100644 index 00000000..770131ad --- /dev/null +++ b/Packets/Protoc/SetPresenceRequest.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +import "PlayerPresence.proto"; + +message SetPresenceRequest { + PlayerBasicPresence basicPresence = 1; + string gameShardId = 2; + PlayerPresenceExt requestExt = 3; +} diff --git a/Packets/Protoc/UpdatePlayerPresenceRequest.proto b/Packets/Protoc/UpdatePlayerPresenceRequest.proto new file mode 100644 index 00000000..91cfe969 --- /dev/null +++ b/Packets/Protoc/UpdatePlayerPresenceRequest.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +message ContextualPlayerPresence { + bool isOnline = 1; + string lastUpdated = 2; + int32 mainPresenceId = 3; + string playerId = 4; + string presenceContext = 5; +} + +message UpdatePlayerPresenceRequest { + string gameShardId = 1; + ContextualPlayerPresence playerPresence = 2; +} \ No newline at end of file diff --git a/Persistence/Implementation/BanDatabase.cpp b/Persistence/Implementation/BanDatabase.cpp new file mode 100644 index 00000000..72df8489 --- /dev/null +++ b/Persistence/Implementation/BanDatabase.cpp @@ -0,0 +1,42 @@ +#include "PersistenceUtilities.h" + +#include + + +BanDatabase& BanDatabase::Get() { + static BanDatabase inst = []() { + BanDatabase db{PersistenceUtilities::GetSavePath() / "playerdata.sqlite"}; + return db; + }(); + return inst; +} + +BanDatabase::BanDatabase(const fs::path& path) + : BasicDatabase(path, "bans") { + GetRaw()->exec("CREATE TABLE IF NOT EXISTS bans(" + "player_id TEXT PRIMARY KEY," + "reason TEXT," + "banned_until INTEGER DEFAULT 0)"); + SQLite::Statement probe(*GetRaw(), "SELECT 1 FROM pragma_table_info('bans') WHERE name='banned_until';"); + if (!probe.executeStep()) { + GetRaw()->exec("ALTER TABLE bans ADD COLUMN banned_until INTEGER DEFAULT 0;"); + GetRaw()->exec("UPDATE bans SET banned_until=0 WHERE banned_until IS NULL;"); + } +} + +bool BanDatabase::IsBanned(const std::string& playerId) { + auto* raw = GetRaw(); + SQLite::Statement query(*raw, "SELECT banned_until FROM bans WHERE player_id=?;"); + query.bind(1, playerId); + if (!query.executeStep()) return false; + int64_t untilMs = 0; + if (!query.getColumn(0).isNull()) { + untilMs = query.getColumn(0).getInt64(); + } + + if (untilMs == 0) return true; + const auto nowMs = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + return nowMs < untilMs; +} \ No newline at end of file diff --git a/Persistence/Implementation/BasicDatabase.cpp b/Persistence/Implementation/BasicDatabase.cpp new file mode 100644 index 00000000..e92e93c2 --- /dev/null +++ b/Persistence/Implementation/BasicDatabase.cpp @@ -0,0 +1,19 @@ +#include +#include + +BasicDatabase::BasicDatabase(const fs::path& dbPath, std::string tableName) + : filename(dbPath), dbRaw(dbPath.string(), sql::OPEN_READWRITE | sql::OPEN_CREATE), + tableName(std::move(tableName)) { +} + +sql::Database* BasicDatabase::GetRaw() { + return &dbRaw; +} + +sql::Database& BasicDatabase::GetRawRef() { + return dbRaw; +} + +const std::string& BasicDatabase::GetTableName() { + return tableName; +} \ No newline at end of file diff --git a/Persistence/Implementation/PartyDatabase.cpp b/Persistence/Implementation/PartyDatabase.cpp index ba938e44..fd8312f1 100644 --- a/Persistence/Implementation/PartyDatabase.cpp +++ b/Persistence/Implementation/PartyDatabase.cpp @@ -1,3 +1,5 @@ +#include "PersistenceUtilities.h" + #include #include #include @@ -26,7 +28,7 @@ static void StripEmptyModeBranches(ordered_json& root) { } PartyDatabase::PartyDatabase(const fs::path& path) - : Database(path, "parties", "PartyID", "TEXT") { + : ProtobufDatabase(path, "parties", "PartyID", "TEXT") { sql::Statement colQuery(GetRawRef(), "PRAGMA table_info(" + GetTableName() + ");"); bool hasPartyCode = false; @@ -49,14 +51,16 @@ PartyDatabase::PartyDatabase(const fs::path& path) GetRaw()->exec("ALTER TABLE " + GetTableName() + " ADD COLUMN PartyVersion TEXT;"); } - AddPrototype(FieldKey::PARTY_EXTRA_BROADCAST_INFO); - AddPrototype(FieldKey::PARTY_PRIVATE_EXTRA_BROADCAST_INFO); - AddPrototype(FieldKey::PARTY_MEMBERS); + AddPrototype(FieldKey::PARTY_EXTRA_BROADCAST_INFO, ProtobufDatabaseFieldData(FieldKey::PARTY_EXTRA_BROADCAST_INFO, "extraBroadcastInfo")); + AddPrototype(FieldKey::PARTY_PRIVATE_EXTRA_BROADCAST_INFO, ProtobufDatabaseFieldData(FieldKey::PARTY_PRIVATE_EXTRA_BROADCAST_INFO, "privateExtraBroadcastInfo")); + AddPrototype(FieldKey::PARTY_MEMBERS, ProtobufDatabaseFieldData(FieldKey::PARTY_MEMBERS, "partyMembers")); } -PartyDatabase PartyDatabase::inst("playerdata.sqlite"); - PartyDatabase& PartyDatabase::Get() { + static PartyDatabase inst = []() { + PartyDatabase db{PersistenceUtilities::GetSavePath() / "playerdata.sqlite"}; + return db; + }(); return inst; } diff --git a/Persistence/Implementation/PlayerDatabase.cpp b/Persistence/Implementation/PlayerDatabase.cpp index 7f0767af..b682849b 100644 --- a/Persistence/Implementation/PlayerDatabase.cpp +++ b/Persistence/Implementation/PlayerDatabase.cpp @@ -2,26 +2,34 @@ #include "PersistenceUtilities.h" +#include #include #include #include #include +#include #include #include +#include #include PlayerDatabase::PlayerDatabase(const fs::path& path) - : Database(path, "players", "PlayerID", "TEXT") { - AddPrototype(FieldKey::PLAYER_INVENTORY, ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultInventory.json"); - AddPrototype(FieldKey::PLAYER_OUTFIT_LOADOUT, ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultOutfitLoadout.json"); - AddPrototype(FieldKey::PLAYER_WEAPON_LOADOUT, ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultWeaponLoadout.json"); - AddPrototype(FieldKey::PLAYER_DATA, ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultPlayerData.json"); - AddPrototype(FieldKey::PROFILE_DATA, ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultProfile.json"); - AddPrototype(FieldKey::PLAYER_LEGACY_DATA, ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultLegacyData.json"); + : ProtobufDatabase(path, "players", "PlayerID", "TEXT") { + AddPrototype(FieldKey::PLAYER_INVENTORY, ProtobufDatabaseFieldData::WithDefaultFromPath(FieldKey::PLAYER_INVENTORY, "inventory", ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultInventory.json")); + AddPrototype(FieldKey::PLAYER_OUTFIT_LOADOUT, ProtobufDatabaseFieldData::WithDefaultFromPath(FieldKey::PLAYER_OUTFIT_LOADOUT, "outfitLoadouts", ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultOutfitLoadout.json")); + AddPrototype(FieldKey::PLAYER_WEAPON_LOADOUT, ProtobufDatabaseFieldData::WithDefaultFromPath(FieldKey::PLAYER_WEAPON_LOADOUT, "weaponLoadouts", ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultWeaponLoadout.json")); + AddPrototype(FieldKey::PLAYER_DATA, ProtobufDatabaseFieldData::WithDefaultFromPath(FieldKey::PLAYER_DATA, "playerData", ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultPlayerData.json")); + AddPrototype(FieldKey::PROFILE_DATA, ProtobufDatabaseFieldData::WithDefaultFromPath(FieldKey::PROFILE_DATA, "profileData", ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultProfile.json")); + AddPrototype(FieldKey::PLAYER_LEGACY_DATA, ProtobufDatabaseFieldData::WithDefaultFromPath(FieldKey::PLAYER_LEGACY_DATA, "legacyData", ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultLegacyData.json")); + AddPrototype(FieldKey::PLAYER_PRESENCE, ProtobufDatabaseFieldData::WithDefaultFromPath(FieldKey::PLAYER_PRESENCE, "presence", ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultPlayerPresence.json")); + AddPrototype(FieldKey::FRIENDS_LIST, ProtobufDatabaseFieldData::WithDefaultFromPath(FieldKey::FRIENDS_LIST, "friendsList", ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultFriendsList.json")); + AddPrototype(FieldKey::NOTIFICATION_DATA, ProtobufDatabaseFieldData::WithDefaultFromPath(FieldKey::NOTIFICATION_DATA, "notificationData", ResourcesUtilities::GetResourcesFolder() / "payloads" / "ws" / "game" / "DefaultSavedNotifications.json")); } -PlayerDatabase PlayerDatabase::inst(PersistenceUtilities::GetSavePath() / "playerdata.sqlite"); - PlayerDatabase& PlayerDatabase::Get() { + static PlayerDatabase inst = []() { + PlayerDatabase db{PersistenceUtilities::GetSavePath() / "playerdata.sqlite"}; + return db; + }(); return inst; } \ No newline at end of file diff --git a/Persistence/Implementation/PlayerDatabase_Providers.cpp b/Persistence/Implementation/PlayerDatabase_Providers.cpp deleted file mode 100644 index 6633fa43..00000000 --- a/Persistence/Implementation/PlayerDatabase_Providers.cpp +++ /dev/null @@ -1,73 +0,0 @@ -#include -#include - -static void CheckProviderTable(SQLite::Database& db) { - db.exec( - "CREATE TABLE IF NOT EXISTS providers (" - "provider TEXT NOT NULL," - "provider_id TEXT NOT NULL," - "player_id TEXT NOT NULL," - "PRIMARY KEY(provider, provider_id));"); - db.exec("CREATE INDEX IF NOT EXISTS providers_player_idx ON providers(player_id);"); -} - -std::string PlayerDatabase::LookupPlayerByProvider(const std::string& provider, const std::string& providerId) { - auto* raw = GetRaw(); - CheckProviderTable(*raw); - SQLite::Statement query(*raw, "SELECT player_id FROM providers WHERE provider=? AND provider_id=?;"); - query.bind(1, provider); - query.bind(2, providerId); - if (query.executeStep()) return query.getColumn(0).getString(); - return {}; -} - -std::string PlayerDatabase::GetProviderIdByPlayerId(const std::string& playerId, const std::string& provider) { - auto* raw = GetRaw(); - SQLite::Statement query(*raw, "SELECT provider_id FROM providers WHERE player_id=? AND provider=?;"); - query.bind(1, playerId); - query.bind(2, provider); - if (query.executeStep()) { - return query.getColumn(0).getString(); - } - return ""; -} - -void PlayerDatabase::UpsertProviderMap(const std::string& provider, const std::string& providerId, const std::string& playerId) { - auto* raw = GetRaw(); - CheckProviderTable(*raw); - SQLite::Statement query(*raw, "INSERT OR REPLACE INTO providers(provider, provider_id, player_id) VALUES(?,?,?);"); - query.bind(1, provider); - query.bind(2, providerId); - query.bind(3, playerId); - query.exec(); -} - -static void CheckBansSchema(SQLite::Database& db) { - db.exec("CREATE TABLE IF NOT EXISTS bans(" - "player_id TEXT PRIMARY KEY," - "reason TEXT," - "banned_until INTEGER DEFAULT 0)"); - SQLite::Statement probe(db, "SELECT 1 FROM pragma_table_info('bans') WHERE name='banned_until';"); - if (!probe.executeStep()) { - db.exec("ALTER TABLE bans ADD COLUMN banned_until INTEGER DEFAULT 0;"); - db.exec("UPDATE bans SET banned_until=0 WHERE banned_until IS NULL;"); - } -} - -bool PlayerDatabase::IsBanned(const std::string& playerId) { - auto* raw = GetRaw(); - CheckBansSchema(*raw); - SQLite::Statement query(*raw, "SELECT banned_until FROM bans WHERE player_id=?;"); - query.bind(1, playerId); - if (!query.executeStep()) return false; - int64_t untilMs = 0; - if (!query.getColumn(0).isNull()) { - untilMs = query.getColumn(0).getInt64(); - } - - if (untilMs == 0) return true; - const auto nowMs = std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - return nowMs < untilMs; -} \ No newline at end of file diff --git a/Persistence/Implementation/Database.cpp b/Persistence/Implementation/ProtobufDatabase.cpp similarity index 55% rename from Persistence/Implementation/Database.cpp rename to Persistence/Implementation/ProtobufDatabase.cpp index dfce53bd..ef306307 100644 --- a/Persistence/Implementation/Database.cpp +++ b/Persistence/Implementation/ProtobufDatabase.cpp @@ -1,26 +1,14 @@ -#include "Database.h" +#include "ProtobufDatabase.h" #include #include -std::unordered_map Database::classNames; -std::unordered_map> Database::defaultFieldValues; - -Database::Database(const fs::path& dbPath, std::string tableName, std::string keyFieldName, const std::string& /*keyFieldType*/) - : filename(dbPath), dbRaw(dbPath.string(), sql::OPEN_READWRITE | sql::OPEN_CREATE), - tableName(std::move(tableName)), keyFieldName(std::move(keyFieldName)) { - dbRaw.exec("CREATE TABLE IF NOT EXISTS " + GetTableName() + " (" + GetKeyFieldName() + " " + GetKeyFieldType() + " PRIMARY KEY);"); -} - -sql::Database* Database::GetRaw() { - return &dbRaw; +ProtobufDatabase::ProtobufDatabase(const fs::path& dbPath, const std::string& tableName, std::string keyFieldName, const std::string& /*keyFieldType*/) + : BasicDatabase(dbPath, tableName), keyFieldName(std::move(keyFieldName)) { + GetRaw()->exec("CREATE TABLE IF NOT EXISTS " + GetTableName() + " (" + GetKeyFieldName() + " " + GetKeyFieldType() + " PRIMARY KEY);"); } -sql::Database& Database::GetRawRef() { - return dbRaw; -} - -sql::Statement Database::FormatStatement(std::string command, FieldKey key) { +sql::Statement ProtobufDatabase::FormatStatement(std::string command, FieldKey key) { size_t tablePos = command.find("{table}"); while (tablePos != std::string::npos) { command.replace(tablePos, sizeof("{table}") - 1, GetTableName()); @@ -31,7 +19,7 @@ sql::Statement Database::FormatStatement(std::string command, FieldKey key) { command.replace(colPos, sizeof("{col}") - 1, GetFieldName(key)); colPos = command.find("{col}"); } - return sql::Statement(dbRaw, command); + return sql::Statement(*GetRaw(), command); } /** Makes the following assumptions: @@ -39,7 +27,7 @@ sql::Statement Database::FormatStatement(std::string command, FieldKey key) { * - statement has 1 questionmark, and it's index(1-indexed) is passed for dataBindIndex Eg valid: INSERT INTO players (PlayerName) VALUES (?) WHERE HI=? B=2 */ -void Database::SetField(sql::Statement& statement, FieldKey key, const pbuf::Message* object, const uint32_t dataBindIndex) { +void ProtobufDatabase::SetField(sql::Statement& statement, FieldKey key, const pbuf::Message* object, const uint32_t dataBindIndex) { if (dataBindIndex == 0) { spdlog::error("passed 0 for data bind index which is not valid"); throw; @@ -51,12 +39,12 @@ void Database::SetField(sql::Statement& statement, FieldKey key, const pbuf::Mes statement.bind(dataBindIndex, bytes.data(), bytes.size()); statement.exec(); } catch (const std::exception& e) { - spdlog::error("Failed to set field with FieldKey {}: {}", classNames.at(key), e.what()); + spdlog::error("Failed to set field with FieldKey {}: {}", getFieldData().at(key).GetFieldName(), e.what()); throw; } } -void Database::SetField(FieldKey key, const pbuf::Message* object, const std::string& dbKeyId) { +void ProtobufDatabase::SetField(FieldKey key, const pbuf::Message* object, const std::string& dbKeyId) { sql::Statement setStatement = FormatStatement( "INSERT INTO {table} (" + GetKeyFieldName() + ", {col}) VALUES(?,?) ON CONFLICT(" + GetKeyFieldName() + ") DO UPDATE SET {col} = excluded.{col};", key); @@ -64,7 +52,7 @@ void Database::SetField(FieldKey key, const pbuf::Message* object, const std::st SetField(setStatement, key, object, 2); } -bool Database::IsFieldPopulated(FieldKey key, const std::string& dbKey) { +bool ProtobufDatabase::IsFieldPopulated(FieldKey key, const std::string& dbKey) { sql::Statement query = FormatStatement( "SELECT {col} FROM {table} WHERE " + GetKeyFieldName() + " = ? COLLATE NOCASE", key); @@ -72,18 +60,14 @@ bool Database::IsFieldPopulated(FieldKey key, const std::string& dbKey) { return query.executeStep(); } -const std::string& Database::GetFieldName(FieldKey key) { - return classNames.at(key); -} - -const std::string& Database::GetTableName() { - return tableName; +const std::string& ProtobufDatabase::GetFieldName(FieldKey key) { + return getFieldData().at(key).GetFieldName(); } -const std::string& Database::GetKeyFieldName() { +const std::string& ProtobufDatabase::GetKeyFieldName() { return keyFieldName; } -const std::string& Database::GetKeyFieldType() { +const std::string& ProtobufDatabase::GetKeyFieldType() { return keyFieldType; } \ No newline at end of file diff --git a/Persistence/Implementation/ProtobufDatabaseFieldData.cpp b/Persistence/Implementation/ProtobufDatabaseFieldData.cpp new file mode 100644 index 00000000..e019d785 --- /dev/null +++ b/Persistence/Implementation/ProtobufDatabaseFieldData.cpp @@ -0,0 +1,36 @@ +#include "google/protobuf/util/json_util.h" +#include "spdlog/spdlog.h" + +#include +#include +#include + +ProtobufDatabaseFieldData::ProtobufDatabaseFieldData(FieldKey fieldKey, std::string className, + std::unique_ptr defaultFieldValue, + std::function updateHandler) + : fieldKey(fieldKey), fieldName(std::move(className)), defaultFieldValue(std::move(defaultFieldValue)), updateHandler(std::move(updateHandler)) { +} + +google::protobuf::util::JsonParseOptions ProtobufDatabaseFieldData::parseOpts = { + .allow_legacy_nonconformant_behavior = false, + .ignore_unknown_fields = false, + .case_insensitive_enum_parsing = true}; + +bool ProtobufDatabaseFieldData::PreUpdateField(google::protobuf::Message* newData) { + if (updateHandler != nullptr) { + return updateHandler(newData); + } + return true; +} + +const std::string& ProtobufDatabaseFieldData::GetFieldName() const { + return fieldName; +} + +const google::protobuf::Message* ProtobufDatabaseFieldData::GetDefaultValue() const { + return defaultFieldValue.get(); +} + +const FieldKey& ProtobufDatabaseFieldData::GetFieldKey() const { + return fieldKey; +} \ No newline at end of file diff --git a/Persistence/Implementation/ProviderLinkDatabase.cpp b/Persistence/Implementation/ProviderLinkDatabase.cpp new file mode 100644 index 00000000..27d4ad5a --- /dev/null +++ b/Persistence/Implementation/ProviderLinkDatabase.cpp @@ -0,0 +1,51 @@ +#include "PersistenceUtilities.h" + +#include + +ProviderLinkDatabase& ProviderLinkDatabase::Get() { + static ProviderLinkDatabase inst = []() { + ProviderLinkDatabase db{PersistenceUtilities::GetSavePath() / "playerdata.sqlite"}; + return db; + }(); + return inst; +} + +ProviderLinkDatabase::ProviderLinkDatabase(const fs::path& dbPath) + : BasicDatabase(dbPath, "providers") { + GetRaw()->exec( + "CREATE TABLE IF NOT EXISTS providers (" + "provider TEXT NOT NULL," + "provider_id TEXT NOT NULL," + "player_id TEXT NOT NULL," + "PRIMARY KEY(provider, provider_id));"); + GetRaw()->exec("CREATE INDEX IF NOT EXISTS providers_player_idx ON providers(player_id);"); +} + +std::string ProviderLinkDatabase::LookupPlayerByProvider(AuthProvider provider, const std::string& providerId) { + auto* raw = GetRaw(); + SQLite::Statement query(*raw, "SELECT player_id FROM providers WHERE provider=? AND provider_id=?;"); + query.bind(1, static_cast(provider)); + query.bind(2, providerId); + if (query.executeStep()) return query.getColumn(0).getString(); + return {}; +} + +std::string ProviderLinkDatabase::GetProviderIdByPlayerId(const std::string& playerId, AuthProvider provider) { + auto* raw = GetRaw(); + SQLite::Statement query(*raw, "SELECT provider_id FROM providers WHERE player_id=? AND provider=?;"); + query.bind(1, playerId); + query.bind(2, static_cast(provider)); + if (query.executeStep()) { + return query.getColumn(0).getString(); + } + return ""; +} + +void ProviderLinkDatabase::UpsertProviderMap(AuthProvider provider, const std::string& providerId, const std::string& playerId) { + auto* raw = GetRaw(); + SQLite::Statement query(*raw, "INSERT OR REPLACE INTO providers(provider, provider_id, player_id) VALUES(?,?,?);"); + query.bind(1, static_cast(provider)); + query.bind(2, providerId); + query.bind(3, playerId); + query.exec(); +} \ No newline at end of file diff --git a/Persistence/Model/AuthProvider.h b/Persistence/Model/AuthProvider.h new file mode 100644 index 00000000..b6dd05d6 --- /dev/null +++ b/Persistence/Model/AuthProvider.h @@ -0,0 +1,5 @@ +#pragma once + +enum class AuthProvider { + STEAM +}; \ No newline at end of file diff --git a/Persistence/Model/BanDatabase.h b/Persistence/Model/BanDatabase.h new file mode 100644 index 00000000..cac04d2f --- /dev/null +++ b/Persistence/Model/BanDatabase.h @@ -0,0 +1,9 @@ +#pragma once +#include "ProtobufDatabase.h" + +class BanDatabase : public BasicDatabase { + public: + static BanDatabase& Get(); + BanDatabase(const fs::path& path); + bool IsBanned(const std::string& playerId); +}; \ No newline at end of file diff --git a/Persistence/Model/BasicDatabase.h b/Persistence/Model/BasicDatabase.h new file mode 100644 index 00000000..9a0feceb --- /dev/null +++ b/Persistence/Model/BasicDatabase.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +namespace fs = std::filesystem; +namespace sql = SQLite; + +class BasicDatabase { + private: + fs::path filename; + sql::Database dbRaw; + std::string tableName; + + public: + BasicDatabase(const fs::path& dbPath, std::string tableName); + sql::Database* GetRaw(); + sql::Database& GetRawRef(); + const std::string& GetTableName(); +}; \ No newline at end of file diff --git a/Persistence/Model/FieldKey.h b/Persistence/Model/FieldKey.h index a6fd9b5e..2662733b 100644 --- a/Persistence/Model/FieldKey.h +++ b/Persistence/Model/FieldKey.h @@ -10,5 +10,8 @@ enum class FieldKey : uint32_t { // NOLINT PLAYER_LEGACY_DATA, PARTY_EXTRA_BROADCAST_INFO, PARTY_MEMBERS, - PARTY_PRIVATE_EXTRA_BROADCAST_INFO + PARTY_PRIVATE_EXTRA_BROADCAST_INFO, + PLAYER_PRESENCE, + FRIENDS_LIST, + NOTIFICATION_DATA }; \ No newline at end of file diff --git a/Persistence/Model/PartyDatabase.h b/Persistence/Model/PartyDatabase.h index cd8e237c..2e488f80 100644 --- a/Persistence/Model/PartyDatabase.h +++ b/Persistence/Model/PartyDatabase.h @@ -1,12 +1,9 @@ #pragma once -#include "Database.h" +#include "ProtobufDatabase.h" #include -class PartyDatabase : public Database { - private: - static PartyDatabase inst; - +class PartyDatabase : public ProtobufDatabase { public: static PartyDatabase& Get(); explicit PartyDatabase(const fs::path& path); diff --git a/Persistence/Model/PlayerDatabase.h b/Persistence/Model/PlayerDatabase.h index f96cf5f6..bb0b0183 100644 --- a/Persistence/Model/PlayerDatabase.h +++ b/Persistence/Model/PlayerDatabase.h @@ -1,15 +1,8 @@ #pragma once -#include "Database.h" - -class PlayerDatabase : public Database { - private: - static PlayerDatabase inst; +#include "ProtobufDatabase.h" +class PlayerDatabase : public ProtobufDatabase { public: static PlayerDatabase& Get(); explicit PlayerDatabase(const fs::path& path); - std::string LookupPlayerByProvider(const std::string& provider, const std::string& providerId); - std::string GetProviderIdByPlayerId(const std::string& playerId, const std::string& provider); - void UpsertProviderMap(const std::string& provider, const std::string& providerId, const std::string& playerId); - bool IsBanned(const std::string& playerId); }; \ No newline at end of file diff --git a/Persistence/Model/Database.h b/Persistence/Model/ProtobufDatabase.h similarity index 76% rename from Persistence/Model/Database.h rename to Persistence/Model/ProtobufDatabase.h index 9452371a..dc922e74 100644 --- a/Persistence/Model/Database.h +++ b/Persistence/Model/ProtobufDatabase.h @@ -1,5 +1,8 @@ #pragma once +#include "BasicDatabase.h" +#include "CaseHelper.h" #include "FieldKey.h" +#include "ProtobufDatabaseFieldData.h" #include #include @@ -32,25 +35,24 @@ namespace sql = SQLite; std::unique_ptr nameFetched = playerData.GetField(fetchStatement, FieldKey::PLAYER_INGAME_NAME); spdlog::info("name: {}", nameFetched->name()); */ -class Database { +class ProtobufDatabase : public BasicDatabase { private: - fs::path filename; - sql::Database dbRaw; - std::string tableName; std::string keyFieldName; std::string keyFieldType; pbuf::util::JsonParseOptions parseOpts; - static std::unordered_map classNames; - static std::unordered_map> defaultFieldValues; + static std::unordered_map& getFieldData() { + static std::unordered_map instance; + return instance; + } + template std::unique_ptr DefaultOrNullptr(FieldKey key) { static_assert(std::is_base_of_v, "Type provided to DefaultOrNullptr must inherit from protobuf::Message"); - auto defaultValue = defaultFieldValues.find(key); - if (defaultValue == defaultFieldValues.end()) { + if (getFieldData().at(key).GetDefaultValue() == nullptr) { return nullptr; } spdlog::info("Returning default value for FieldKey: {}", static_cast(key)); - const T* typed = dynamic_cast(defaultValue->second.get()); + const T* typed = dynamic_cast(getFieldData().at(key).GetDefaultValue()); if (!typed) { // Handle type mismatch spdlog::error("type mismatch in DefaultOrNullptr"); @@ -63,10 +65,7 @@ class Database { } public: - Database(const fs::path& dbPath, std::string tableName, std::string keyFieldName, const std::string& keyFieldType); - - sql::Database* GetRaw(); - sql::Database& GetRawRef(); + ProtobufDatabase(const fs::path& dbPath, const std::string& tableName, std::string keyFieldName, const std::string& keyFieldType); template std::vector> GetFields(sql::Statement& query, FieldKey key) { @@ -74,7 +73,7 @@ class Database { std::vector> output; while (query.executeStep()) { if (query.getColumnCount() != 1) { - spdlog::warn("Multiple columns returned by query passed into GetFields w FieldKey {}, ignoring all columns except first one", classNames.at(key)); + spdlog::warn("Multiple columns returned by query passed into GetFields w FieldKey {}, ignoring all columns except first one", getFieldData().at(key).GetFieldName()); } const char* blob = static_cast(query.getColumn(0).getBlob()); int sz = query.getColumn(0).getBytes(); @@ -105,7 +104,8 @@ class Database { return std::move(DefaultOrNullptr(key)); } if (query.getColumnCount() != 1) { - spdlog::warn("Multiple columns returned by query passed into GetField w FieldKey {}, ignoring all columns except first one", classNames.at(key)); + spdlog::warn("Multiple columns returned by query passed into GetField w FieldKey {}, ignoring all columns except first one", + getFieldData().at(key).GetFieldName()); } const char* blob = static_cast(query.getColumn(0).getBlob()); int sz = query.getColumn(0).getBytes(); @@ -141,37 +141,24 @@ class Database { sql::Statement FormatStatement(std::string command, FieldKey key); - template - void AddPrototype(FieldKey key) { - static_assert(std::is_base_of_v, "Type provided to AddPrototype must inherit from protobuf::Message"); - classNames.insert({key, std::string(T::descriptor()->name())}); - sql::Statement colQuery(dbRaw, "PRAGMA table_info(" + GetTableName() + ");"); + void AddPrototype(FieldKey key, ProtobufDatabaseFieldData dat) { + sql::Statement colQuery(*GetRaw(), "PRAGMA table_info(" + GetTableName() + ");"); bool colExists = false; while (colQuery.executeStep()) { std::string colName = colQuery.getColumn(1).getText(); - if (colName == T::descriptor()->name()) { + if (CaseInsensitiveEquals(colName, dat.GetFieldName())) { colExists = true; break; } } if (!colExists) { - dbRaw.exec("ALTER TABLE " + GetTableName() + " ADD COLUMN " + std::string(T::descriptor()->name()) + " BLOB;"); - } - } - - template - void AddPrototype(const FieldKey key, const fs::path& defaultFieldValuePath) { - AddPrototype(key); - const std::ifstream defaultFile(defaultFieldValuePath); - std::stringstream buf; - buf << defaultFile.rdbuf(); - const std::string data = buf.str(); - T defaultFieldData; - if (auto status = pbuf::util::JsonStringToMessage(data, &defaultFieldData, parseOpts); !status.ok()) { - spdlog::error("failed to parse default message: {}", status.message()); - throw; + try { + GetRaw()->exec("ALTER TABLE " + GetTableName() + " ADD COLUMN " + dat.GetFieldName() + " BLOB;"); + } catch (std::exception& e) { + spdlog::error("{}", e.what()); + } } - defaultFieldValues[key] = std::make_unique(defaultFieldData); + getFieldData().insert_or_assign(key, std::move(dat)); } bool IsFieldPopulated(FieldKey key, const std::string& dbKey); @@ -181,7 +168,6 @@ class Database { static const std::string& GetFieldName(FieldKey key); - const std::string& GetTableName(); const std::string& GetKeyFieldName(); const std::string& GetKeyFieldType(); }; diff --git a/Persistence/Model/ProtobufDatabaseFieldData.h b/Persistence/Model/ProtobufDatabaseFieldData.h new file mode 100644 index 00000000..1b9ad657 --- /dev/null +++ b/Persistence/Model/ProtobufDatabaseFieldData.h @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +class ProtobufDatabaseFieldData { + private: + FieldKey fieldKey; + std::string fieldName; + std::unique_ptr defaultFieldValue; + std::function updateHandler; + static google::protobuf::util::JsonParseOptions parseOpts; + + public: + ProtobufDatabaseFieldData(FieldKey fieldKey, std::string className, + std::unique_ptr defaultFieldValue = nullptr, + std::function updateHandler = nullptr); + + template + static ProtobufDatabaseFieldData WithDefaultFromPath(FieldKey fieldKey, std::string className, const std::filesystem::path& defaultFieldValuePath, + std::function updateHandler = nullptr) { + const std::ifstream defaultFile(defaultFieldValuePath); + std::stringstream buf; + buf << defaultFile.rdbuf(); + const std::string data = buf.str(); + std::unique_ptr defaultFieldValue = std::make_unique(); + if (auto status = google::protobuf::util::JsonStringToMessage(data, defaultFieldValue.get(), parseOpts); !status.ok()) { + spdlog::error("failed to parse default message: {}", status.message()); + throw; + } + return ProtobufDatabaseFieldData(fieldKey, className, std::move(defaultFieldValue), updateHandler); + } + + bool PreUpdateField(google::protobuf::Message* newData); + [[nodiscard]] const FieldKey& GetFieldKey() const; + [[nodiscard]] const std::string& GetFieldName() const; + [[nodiscard]] const google::protobuf::Message* GetDefaultValue() const; +}; \ No newline at end of file diff --git a/Persistence/Model/ProviderLinkDatabase.h b/Persistence/Model/ProviderLinkDatabase.h new file mode 100644 index 00000000..37a00419 --- /dev/null +++ b/Persistence/Model/ProviderLinkDatabase.h @@ -0,0 +1,14 @@ +#pragma once +#include "ProtobufDatabase.h" + +#include +#include + +class ProviderLinkDatabase : public BasicDatabase { + public: + static ProviderLinkDatabase& Get(); + ProviderLinkDatabase(const fs::path& dbPath); + std::string LookupPlayerByProvider(AuthProvider provider, const std::string& providerId); + std::string GetProviderIdByPlayerId(const std::string& playerId, AuthProvider provider); + void UpsertProviderMap(AuthProvider provider, const std::string& providerId, const std::string& playerId); +}; \ No newline at end of file diff --git a/Persistence/Protoc/FriendsList.proto b/Persistence/Protoc/FriendsList.proto new file mode 100644 index 00000000..c11c06ff --- /dev/null +++ b/Persistence/Protoc/FriendsList.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +message FriendsList { + bool acceptNewFriendInvites = 1; + repeated string blocked = 2; + repeated string friends = 3; + repeated string receivedInvites = 4; + repeated string sentInvites = 5; + string version = 6; +} \ No newline at end of file diff --git a/Persistence/Protoc/PlayerPresence.proto b/Persistence/Protoc/PlayerPresence.proto new file mode 100644 index 00000000..96544765 --- /dev/null +++ b/Persistence/Protoc/PlayerPresence.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +message PlayerPresence { + PlayerBasicPresence basicPresence = 1; + PlayerPresenceExt ext = 2; + string version = 3; +} + +enum PlayerBasicPresence { + Online = 0; + Offline = 1; +} + +message PlayerPresenceExt { + string lastUpdated = 1; + int32 mainPresenceId = 2; + string presenceContext = 3; +} diff --git a/Persistence/Protoc/SavedNotificationData.proto b/Persistence/Protoc/SavedNotificationData.proto new file mode 100644 index 00000000..bdfe86f4 --- /dev/null +++ b/Persistence/Protoc/SavedNotificationData.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +message SavedNotificationData { + repeated SavedNotification notificationsToDeliver = 1; +} + +message SavedNotification { + string rpcType = 1; + string notificationId = 2; + string notificationData = 3; +} \ No newline at end of file diff --git a/README.md b/README.md index b7e32a00..58d034b1 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ PRs welcome. If you contribute: * * if your absolute path is `E:\dev\spectre\srv\pragmabackend` then your toolchain file path is `E:\dev\spectre\srv\pragmabackend\vcpkg\scripts\buildsystems\vcpkg.cmake` **failed to open InventoryStore file** -* you didnt follow the instructions properly so it couldnt build fully. +* you didn't follow the instructions properly so it couldn't build fully. **Port already in use** @@ -125,3 +125,6 @@ PRs welcome. If you contribute: **failed to read PlayerConfigData in SavePlayerDataProcessor** * Press WIN+R -> type in `%localappdata%` -> press enter -> delete the `Spectre` folder. + +### Using a published release +You may download a public build of the backend from the (latest release)[https://github.com/SpectreRevival/pragmabackend/releases/latest]. This will contain a build for windows, linux and a docker container image. For windows or linux, simply extract the .zip and run the executable named pragmabackend inside. For the docker container, run `docker image load -i pragmabackend-docker.tar` and then `docker run -d -p 80:80 -p 8081:8081 -p 8082:8082 pragmabackend:latest` diff --git a/main.cpp b/main.cpp index 4041a52a..23bc9062 100644 --- a/main.cpp +++ b/main.cpp @@ -1,267 +1,6 @@ -// -// Created by astro on 27/09/2025. -// +#include -#include "StaticHTTPPackets.cpp" // NOLINT -#include "StaticWSPackets.cpp" // NOLINT -#include "SubmitProviderIdHandler.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static uint16_t gamePort = 8081; -static uint16_t socialPort = 8082; -static uint16_t wsPort = 80; - -namespace asio = boost::asio; -namespace beast = boost::beast; -namespace http = beast::http; -namespace websocket = beast::websocket; -using tcp = asio::ip::tcp; -namespace fs = std::filesystem; - -static std::shared_ptr logger; - -static void SetupLogger() { - auto consoleSink = std::make_shared(); - auto fileSink = std::make_shared((PersistenceUtilities::GetSavePath() / "logs" / "app.log").string(), true); - - // Optional: Customize sink formats - consoleSink->set_pattern("[%T] [%^%l%$] %v"); - fileSink->set_pattern("[%Y-%m-%d %T] [%l] %v"); - - // Combine sinks into one logger - std::vector sinks{consoleSink, fileSink}; - logger = std::make_shared("pragma", sinks.begin(), sinks.end()); - - // Register and use the logger - spdlog::register_logger(logger); - spdlog::set_default_logger(logger); -} - -static std::string StripQueryParams(const std::string& url) { - size_t pos = url.find('?'); - if (pos != std::string::npos) { - if (url.at(0) == '/' && url.at(1) == '/') { - return url.substr(1, pos); - } - return url.substr(0, pos); - } - if (url.at(0) == '/' && url.at(1) == '/') { - return url.substr(1); - } - return url; -} - -static void Session(tcp::socket sock) { - try { - beast::flat_buffer buffer; // http parser scratch space - http::request req; - - // blocking read - http::read(sock, buffer, req); - - // detect websocket upgrade and switch protocols if requested - if (websocket::is_upgrade(req)) { - logger->info("trying to accept ws handshake"); - websocket::stream ws(std::move(sock)); - ws.accept(req); // complete ws handshake - SpectreWebsocket sock(ws, req); - logger->info("upgraded connection with {}:{} to websocket", - ws.next_layer().remote_endpoint().address().to_string(), ws.next_layer().remote_endpoint().port()); - - // basic echo loop so clients have something to talk to - for (;;) { - beast::flat_buffer wsbuf; - ws.read(wsbuf); // this blocks until a message arrives or the peer closes - SpectreWebsocketRequest req(sock, wsbuf); - auto route = WebsocketPacketProcessor::GetProcessorForRpc(req.GetRequestType()); - if (route == nullptr) { - logger->warn("no packet processor found for WS requestType: " + req.GetRequestType().GetName()); - continue; - } - route->Process(req, sock); - } - } - auto target = StripQueryParams(std::string(req.target())); // remove ?query so routing is stable - HTTPPacketProcessor* processor = HTTPPacketProcessor::GetProcessorForRoute(target); - if (processor == nullptr) { - logger->warn("missing a handler for http route " + target); - // send a 404 if no processor found - http::response res; - res.version(req.version()); - res.keep_alive(req.keep_alive()); - res.result(http::status::not_found); - res.set(http::field::content_type, "application/json; charset=UTF-8"); - res.body() = "{}"; - res.prepare_payload(); - http::write(sock, res); - return; - } - processor->Process(req, sock); - } catch (std::exception& e) { - // any parse error, socket close, or ws close lands here - logger->error("session error: "); - logger->error(e.what()); - } -} - -static void ConnectionAcceptor(unsigned short port) { - try { - asio::io_context ioc; // we use sync ops but asio still wants an io_context around - - // tls setup. load cert / key - - tcp::acceptor acc(ioc, tcp::endpoint(asio::ip::make_address("0.0.0.0"), port)); - - // accept loop forever. each client gets one detached thread - // if we want to shut down clean, don't detach, keep thread handles - for (;;) { - tcp::socket sock(ioc); - acc.accept(sock); // blocks until a client connects - // each session owns its TLS handshake and stream - std::thread([s = std::move(sock)]() mutable { - Session(std::move(s)); - }).detach(); - } - } catch (std::exception& e) { - logger->error("fatal exception(rip acceptor thread): "); - logger->error(e.what()); - } -} - -bool bStop = false; - -void HandleInterrupt(int /*sigint*/) { - bStop = true; -} - -// the main accept loop -// binds to 127.0.0.1:443, accepts a connection, spins a thread, repeat int main(int argc, char** argv) { - (void)signal(2, HandleInterrupt); - if (argc == 4) { - gamePort = std::stoi(std::string(argv[1])); - socialPort = std::stoi(std::string(argv[2])); - wsPort = std::stoi(std::string(argv[3])); - } - try { - SetupLogger(); - logger->info("starting server..."); - logger->info("Registering handlers..."); - RegisterStaticHTTPHandlers(); - RegisterStaticWSHandlers(); - - // feel like this needs cleaning up T_T - - new SubmitProviderIdHandler("/v1/submitproviderid"); - new AuthenticateHandler("/v1/account/authenticateorcreatev2"); - new HeartbeatProcessor(SpectreRpcType("PlayerSessionRpc.HeartbeatV1Request")); - new FieldFetchProcessor( - SpectreRpcType("InventoryRpc.GetInventoryV2Request"), - FieldKey::PLAYER_INVENTORY); - logger->info("Finished registering handlers"); - new UpdateItemsV0Processor(SpectreRpcType("InventoryRpc.UpdateItemsV0Request")); - new UpdateItemV4Processor(SpectreRpcType("InventoryRpc.UpdateItemV4Request")); - new FieldFetchProcessor( - SpectreRpcType("MtnLoadoutServiceRpc.FetchPlayerOutfitLoadoutsV1Request"), - FieldKey::PLAYER_OUTFIT_LOADOUT); - new FetchPlayerLoadoutsProcessor( - SpectreRpcType("MtnLoadoutServiceRpc.FetchPlayerWeaponLoadoutsV1Request")); - new SaveWeaponLoadoutProcessor( - SpectreRpcType("MtnLoadoutServiceRpc.SaveWeaponLoadoutV1Request")); - new SaveOutfitLoadoutProcessor( - SpectreRpcType("MtnLoadoutServiceRpc.SaveOutfitLoadoutV1Request")); - new GetPlayerDataProcessor( - SpectreRpcType("MtnPlayerDataServiceRpc.GetAllPlayerDataClientV1Request")); - new GetBulkProfileDataProcessor( - SpectreRpcType("MtnPlayerDataServiceRpc.GetBulkProfileDataClientV1Request")); - new SavePlayerDataProcessor( - SpectreRpcType("MtnPlayerDataServiceRpc.SavePlayerDataForClientV1Request")); - new FieldFetchProcessor( - SpectreRpcType("MtnPlayerDataServiceRpc.GetPlayerLegacyDataV1Request"), - FieldKey::PLAYER_LEGACY_DATA); - new GetLoginDataProcessor( - SpectreRpcType("GameDataRpc.GetLoginDataV3Request")); - new CreatePartyProcessor( - SpectreRpcType("PartyRpc.CreateV1Request")); - new SetReadyProcessor( - SpectreRpcType("PartyRpc.SetReadyStateV1Request")); - new UpdatePartyProcessor( - SpectreRpcType("PartyRpc.UpdatePartyV1Request")); - new UpdatePartyPlayerProcessor( - SpectreRpcType("PartyRpc.UpdatePartyPlayerV1Request")); - new EnterMatchmakingProcessor( - SpectreRpcType("PartyRpc.EnterMatchmakingV1Request")); - new IsInPartyHandler( - SpectreRpcType("MultiplayerRpc.InitializePartyV1Request")); - new IsInPartyHandler( - SpectreRpcType("MultiplayerRpc.SyncPartyV1Request")); - std::thread gameThread = std::thread([] { - ConnectionAcceptor(gamePort); // game - }); - std::thread socialThread = std::thread([] { - ConnectionAcceptor(socialPort); // social - }); - std::thread wsThread = std::thread([] { - ConnectionAcceptor(wsPort); // websockets - }); - logger->info("acceptor threads started"); - std::ofstream serverLockFile("./server.lock", std::ios::trunc | std::ios::out); - while (!bStop) { - std::this_thread::sleep_for(std::chrono::milliseconds(500)); - } - serverLockFile.close(); - fs::remove("./server.lock"); - } catch (const std::exception& e) { - logger->error("unhandled exception caught in main: {}", e.what()); - } - return 0; + std::stop_token st; + return MainThread(argc, argv, st); } \ No newline at end of file diff --git a/resources/payloads/ws/game/DefaultFriendsList.json b/resources/payloads/ws/game/DefaultFriendsList.json new file mode 100644 index 00000000..171f0120 --- /dev/null +++ b/resources/payloads/ws/game/DefaultFriendsList.json @@ -0,0 +1,8 @@ +{ + "acceptNewFriendInvites": true, + "blocked": [], + "friends": [], + "receivedInvites": [], + "sentInvites": [], + "version": "0" +} \ No newline at end of file diff --git a/resources/payloads/ws/game/DefaultInventory.json b/resources/payloads/ws/game/DefaultInventory.json index 5ccd1d99..f80160a3 100644 --- a/resources/payloads/ws/game/DefaultInventory.json +++ b/resources/payloads/ws/game/DefaultInventory.json @@ -5965,7 +5965,7 @@ "Option4", "Option5", "Option6" - ], + ] }, { "channelId": "Color", diff --git a/resources/payloads/ws/game/DefaultPlayerPresence.json b/resources/payloads/ws/game/DefaultPlayerPresence.json new file mode 100644 index 00000000..3b094e4b --- /dev/null +++ b/resources/payloads/ws/game/DefaultPlayerPresence.json @@ -0,0 +1,5 @@ +{ + "basicPresence": "Offline", + "ext": {}, + "version": "0" +} \ No newline at end of file diff --git a/resources/payloads/ws/game/DefaultSavedNotifications.json b/resources/payloads/ws/game/DefaultSavedNotifications.json new file mode 100644 index 00000000..a29b58d8 --- /dev/null +++ b/resources/payloads/ws/game/DefaultSavedNotifications.json @@ -0,0 +1,3 @@ +{ + "notificationsToDeliver": [] +} \ No newline at end of file diff --git a/resources/testrequests/httpSkipTests.txt b/resources/testrequests/httpSkipTests.txt new file mode 100644 index 00000000..27557992 --- /dev/null +++ b/resources/testrequests/httpSkipTests.txt @@ -0,0 +1 @@ +/track \ No newline at end of file diff --git a/resources/testrequests/sequenced/party/1.json b/resources/testrequests/sequenced/party/1.json index f0900b72..d32555e5 100644 --- a/resources/testrequests/sequenced/party/1.json +++ b/resources/testrequests/sequenced/party/1.json @@ -1,275 +1,7 @@ { - "requestBody": { - "createRequestExt": { - "acceptableRegions": [], - "crossPlayPreference": { - "platform": "CROSS_PLAY_PLATFORM_PC" - }, - "lobbyMode": "standard_casual", - "profile": "", - "region": "", - "standard": { - "mode": "Standard" - }, - "tag": "", - "useTeamMmr": false, - "version": "173322" - }, - "gameClientVersion": "", - "gameServerZoneToPing": {}, - "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9", - "playerJoinRequestExt": { - "clientData": "{\r\n\t\"clientPublicKey\": \"\"\r\n}", - "crossPlayPreference": { - "platform": "ANY_CROSS_PLAY_PLATFORM" - }, - "preferredRegions": [], - "preferredTeam": "SPECTATOR", - "region": "", - "regionPings": { - "uscentral-1": 17, - "uscentral-2": 33, - "useast-1": 50, - "uswest-1": 33, - "uswest-2": 33 - }, - "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561199041068696\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", - "version": "173322" - }, - "preferredGameServerZones": [] - }, - "responsePayload": { - "party": { - "extBroadcastParty": { - "chatId": "", - "crossPlayPreference": { - "platform": "CROSS_PLAY_PLATFORM_PC" - }, - "hasAcceptableRegion": true, - "lobbyMode": "standard_casual", - "pool": "", - "profile": "", - "region": "", - "removePlayers": [], - "standard": { - "mode": "Standard" - }, - "tag": "", - "tokens": [], - "useTeamMmr": false, - "version": "173322" - }, - "extPrivatePlayer": {}, - "inviteCode": "*", - "partyId": "*", - "partyMembers": [ - { - "displayName": { - "discriminator": "*", - "displayName": "Ohm" - }, - "ext": { - "limitedInstancedInventory": [ - ], - "playerData": { - "attackerOutfitLoadout": { - "faceAccessoryData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "", - "itemInstanceId": "", - "itemType": "" - }, - "faceStyleData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "", - "itemInstanceId": "", - "itemType": "" - }, - "hairData": { - "alterationData": [ - { - "alterationId": "Option7", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0016", - "itemInstanceId": "*", - "itemType": "" - }, - "headData": { - "alterationData": [ - { - "alterationId": "Option5", - "channelId": "SkinTone" - }, - { - "alterationId": "Option3", - "channelId": "LipColor" - }, - { - "alterationId": "Option6", - "channelId": "EyeColor" - }, - { - "alterationId": "Option4", - "channelId": "EyebrowColor" - }, - { - "alterationId": "Option5", - "channelId": "Voice" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_003", - "itemInstanceId": "*", - "itemType": "" - }, - "loadoutId": "*", - "outfitData": { - "alterationData": [ - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_F_Default", - "itemInstanceId": "*", - "itemType": "" - }, - "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9" - }, - "attackerWeaponLoadout": { - "loadoutId": "*", - "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9" - }, - "banner": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreBannerItemDef:BannerID_Default", - "itemInstanceId": "*", - "itemType": "" - }, - "defenderOutfitLoadout": { - "faceAccessoryData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "", - "itemInstanceId": "", - "itemType": "" - }, - "faceStyleData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "", - "itemInstanceId": "", - "itemType": "" - }, - "hairData": { - "alterationData": [ - { - "alterationId": "Option5", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", - "itemInstanceId": "*", - "itemType": "" - }, - "headData": { - "alterationData": [ - { - "alterationId": "Option8", - "channelId": "SkinTone" - }, - { - "alterationId": "Option2", - "channelId": "LipColor" - }, - { - "alterationId": "Option6", - "channelId": "EyeColor" - }, - { - "alterationId": "Option4", - "channelId": "EyebrowColor" - }, - { - "alterationId": "Option3", - "channelId": "Voice" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", - "itemInstanceId": "*", - "itemType": "" - }, - "loadoutId": "*", - "outfitData": { - "alterationData": [ - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Default", - "itemInstanceId": "40268935-F961-4783-934F-C46B3E7DC1DB", - "itemType": "" - }, - "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9" - }, - "defenderWeaponLoadout": { - "loadoutId": "*", - "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9" - }, - "matchmakingData": { - "casualMatchesPlayedCount": 0.0, - "casualMatchesWonCount": 0.0, - "currentSoloRank": 0.0, - "highestTeamRank": 0.0, - "matchmakingFlags": 1.0, - "primaryGeographicRegion": "", - "priorityMatchmakingUntil": "0", - "restrictMatchmakingUntil": "0", - "secondaryGeographicRegion": "", - "soloRankPoints": 0.0 - } - }, - "preferredTeam": "TEAM0", - "rankedModeUnlocked": false, - "region": "", - "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561199041068696\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", - "version": "173322" - }, - "isLeader": true, - "isReady": false, - "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9", - "socialId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9" - } - ], - "preferredGameServerZones": [ - "uscentral-1" - ], - "version": "1" - } - }, - "rpcType": "PartyRpc.CreateV1Request", - "ignoreReplace": true, - "testAge": 1740507431.194 + "method": "POST", + "path": "/v1/account/authenticateorcreatev2", + "request": "{\"providerId\":\"STEAM\",\"providerToken\":\"14000000101B0A7F70C1FBBEE7E69F6C01001001B908BE67180000000100000002000000C1F9C3FB3D7764BEA0A8010001000000B20000003200000004000000E7E69F6C010010013E4E280061342A2D061C6E6400000000AD58BA672D08D66701007A730E00000000008561B7CAE2C32A312B14297D110EC706041E747242EC356EC06CC7CBED9E88B06ACE7C131B109603310CD4EBBD780C0CBD71DDFD43C7FDDB210DD69E69A76D7BE28F51C11FDEF5E04ACA018AB9333065E4340076F77F12BD906C57366CF6FD93931D52A9F0657279759865B5C60E99538904EBD23A3804FE02D594647943FE49\",\"gameShardId\":\"00000000-0000-0000-0000-000000000001\",\"loginQueuePassToken\":\"eyJraWQiOiJkM0p0T3E2ankzX0hxdXdUc3J6dDgxd2gzQkxpQS00Zi1xTThtai0wLVlRPSIsImFsZyI6IlJTMjU2IiwidHlwIjoiSldUIn0.eyJpc3MiOiJwcmFnbWEiLCJzdWIiOiIxMTYyOCIsImlhdCI6MTc0MDUwNzQyNywiZXhwIjoxNzQwNTkzODI3LCJqdGkiOiIyYjFiMzc5Yi1jZmQwLTRkMzAtYWE4NS02OWMyZWIyMWE4MWEiLCJ0aWNrZXROdW1iZXIiOiIxMTYyOCIsImlzQWxsb3dlZEluIjoidHJ1ZSJ9.PJp5MNXz2_xvCkq_XjzZeui1MvS9ylDLDgeLkJiv9jp_FVnTI9LISMtajHcef-7JehNs5sQC6P_Gpmb6JuVdD4k7HUX7a9IAgM8HKAagnfgmymn02SSpL7Mfz9wbH8FgOYU2ylKG_ExIW_aSG5HK588_waNeSydygwX2zRoSf8ZYZzbUHmMsZcG2iXpDq_Peejbt6Cgep9lsyNE5L5ZZzil9_KVu3FaEojcrI7tiPpHX7wi2K_J78rxmg2weUreowhv0VJA-YGqtOUlqFl7Ep8VGi-IrJdAf4gLeiVZMQoktc_g5tD9FgXzEAH_aDoBqGgoqnbKLcWLRiT1TAYGgXtCfw15Efh_ta-h4IIOI-DAnhJ1ujapd80Z87Wo6h7SpBaOitaI-bjBPkqDQGe2JooUNCrki848vPrfu0IQW00vawUtLX6LaS_aAEs0L2Vjxyebk1X37E9KwTDoxQGdmurutcnvSmVXOoO4P8F6o4oGx-A9d6HgFJl5rRie2LrWSJHlmcFm5_IKYw7okHwBh63Cx3mhUevji5SkEGj3gbwlBURjeEXpOm0qr-ECeKdmagbi_ipiiQB8m8FNwAbx9Z-Sl3nbJ-kS3QtPZrFHqxf91sgFY16H6sn1ruhna-ZygG5cYKf4JWbEcmLrSmdQ_xIBODjWDcatvNKGrv7Cx_Ng\"}", + "response": "{\"pragmaTokens\":{\"pragmaGameToken\":\"*\",\"pragmaSocialToken\":\"*\"}}", + "testAge": "2025-02-25T13:17:06.6028148-05:00" } \ No newline at end of file diff --git a/resources/testrequests/sequenced/party/2.json b/resources/testrequests/sequenced/party/2.json index bc7b7b1f..f0900b72 100644 --- a/resources/testrequests/sequenced/party/2.json +++ b/resources/testrequests/sequenced/party/2.json @@ -1,19 +1,42 @@ { "requestBody": { - "partyId": "${responses[1]party.partyId}", - "requestExt": { - "acceptableRegions": [ - ], - "lobbyMode": "${responses[1]party.extBroadcastParty.lobbyMode}", - "pool": "${responses[1]party.extBroadcastParty.pool}", - "profile": "${responses[1]party.extBroadcastParty.profile}", - "region": "${responses[1]party.extBroadcastParty.region}", - "removePlayers": "${responses[1]party.extBroadcastParty.removePlayers}", - "standard": "${responses[1]party.extBroadcastParty.standard}", - "tag": "${responses[1]party.extBroadcastParty.tag}", - "useTeamMmr": "${responses[1]party.extBroadcastParty.useTeamMmr}", - "version": "${responses[1]party.extBroadcastParty.version}" - } + "createRequestExt": { + "acceptableRegions": [], + "crossPlayPreference": { + "platform": "CROSS_PLAY_PLATFORM_PC" + }, + "lobbyMode": "standard_casual", + "profile": "", + "region": "", + "standard": { + "mode": "Standard" + }, + "tag": "", + "useTeamMmr": false, + "version": "173322" + }, + "gameClientVersion": "", + "gameServerZoneToPing": {}, + "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9", + "playerJoinRequestExt": { + "clientData": "{\r\n\t\"clientPublicKey\": \"\"\r\n}", + "crossPlayPreference": { + "platform": "ANY_CROSS_PLAY_PLATFORM" + }, + "preferredRegions": [], + "preferredTeam": "SPECTATOR", + "region": "", + "regionPings": { + "uscentral-1": 17, + "uscentral-2": 33, + "useast-1": 50, + "uswest-1": 33, + "uswest-2": 33 + }, + "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561199041068696\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", + "version": "173322" + }, + "preferredGameServerZones": [] }, "responsePayload": { "party": { @@ -24,7 +47,7 @@ }, "hasAcceptableRegion": true, "lobbyMode": "standard_casual", - "pool": "standard_casual", + "pool": "", "profile": "", "region": "", "removePlayers": [], @@ -37,183 +60,16 @@ "version": "173322" }, "extPrivatePlayer": {}, - "inviteCode": "CDPLJB", - "partyId": "b0cfc843-4cbc-4e5f-9e59-0934ebad4f1f", + "inviteCode": "*", + "partyId": "*", "partyMembers": [ { "displayName": { - "discriminator": "3361", - "displayName": "SantaiGG" + "discriminator": "*", + "displayName": "Ohm" }, "ext": { "limitedInstancedInventory": [ - { - "catalogId": "SpectreBannerItemDef:BannerID_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "4f30362e-0f4a-4389-8c43-4c588924ca09" - }, - { - "catalogId": "CharacterCosmeticItemOutfit:OutfitID_F_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "ac5035e3-6572-484b-a5d8-1ffcc1f367f1" - }, - { - "catalogId": "CharacterCosmeticItemHead:HeadID_Face_003", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "SkinTone", - "ownedAlterations": [ - "Option5" - ] - }, - { - "channelId": "LipColor", - "ownedAlterations": [ - "Option3" - ] - }, - { - "channelId": "EyeColor", - "ownedAlterations": [ - "Option6" - ] - }, - { - "channelId": "EyebrowColor", - "ownedAlterations": [ - "Option4" - ] - }, - { - "channelId": "Voice", - "ownedAlterations": [ - "Option5" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "56490170-5353-4a9d-8f5f-14b0bbb91b63" - }, - { - "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0016", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Color", - "ownedAlterations": [ - "Option7" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "5f021dba-65c9-48f9-ae02-3b6eebb1a430" - }, - { - "catalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "40268935-f961-4783-934f-c46b3e7dc1db" - }, - { - "catalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "SkinTone", - "ownedAlterations": [ - "Option8" - ] - }, - { - "channelId": "LipColor", - "ownedAlterations": [ - "Option2" - ] - }, - { - "channelId": "EyeColor", - "ownedAlterations": [ - "Option6" - ] - }, - { - "channelId": "EyebrowColor", - "ownedAlterations": [ - "Option4" - ] - }, - { - "channelId": "Voice", - "ownedAlterations": [ - "Option3" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "711af3ec-f1e0-4c0b-ae88-4bd2c51eb23f" - }, - { - "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Color", - "ownedAlterations": [ - "Option5" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "a98f9664-2387-4931-b6a2-8f0e7983b856" - } ], "playerData": { "attackerOutfitLoadout": { @@ -243,7 +99,7 @@ "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0016", - "itemInstanceId": "5F021DBA-65C9-48F9-AE02-3B6EEBB1A430", + "itemInstanceId": "*", "itemType": "" }, "headData": { @@ -272,10 +128,10 @@ "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_003", - "itemInstanceId": "56490170-5353-4A9D-8F5F-14B0BBB91B63", + "itemInstanceId": "*", "itemType": "" }, - "loadoutId": "53925c2d-5b68-48f8-88de-2d6999f9f3c6", + "loadoutId": "*", "outfitData": { "alterationData": [ { @@ -286,21 +142,21 @@ "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_F_Default", - "itemInstanceId": "AC5035E3-6572-484B-A5D8-1FFCC1F367F1", + "itemInstanceId": "*", "itemType": "" }, - "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" + "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9" }, "attackerWeaponLoadout": { - "loadoutId": "5152439b-2aaf-484c-9de0-0fdea92d99c9", - "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" + "loadoutId": "*", + "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9" }, "banner": { "alterationData": [], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", "itemCatalogId": "SpectreBannerItemDef:BannerID_Default", - "itemInstanceId": "4F30362E-0F4A-4389-8C43-4C588924CA09", + "itemInstanceId": "*", "itemType": "" }, "defenderOutfitLoadout": { @@ -330,7 +186,7 @@ "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", - "itemInstanceId": "A98F9664-2387-4931-B6A2-8F0E7983B856", + "itemInstanceId": "*", "itemType": "" }, "headData": { @@ -359,10 +215,10 @@ "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", - "itemInstanceId": "711AF3EC-F1E0-4C0B-AE88-4BD2C51EB23F", + "itemInstanceId": "*", "itemType": "" }, - "loadoutId": "ebb85cdd-827a-4884-9eb5-0b584d43be19", + "loadoutId": "*", "outfitData": { "alterationData": [ { @@ -376,11 +232,11 @@ "itemInstanceId": "40268935-F961-4783-934F-C46B3E7DC1DB", "itemType": "" }, - "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" + "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9" }, "defenderWeaponLoadout": { - "loadoutId": "ff09ddea-2594-4bea-abd1-d6f255ce3f57", - "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" + "loadoutId": "*", + "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9" }, "matchmakingData": { "casualMatchesPlayedCount": 0.0, @@ -398,25 +254,22 @@ "preferredTeam": "TEAM0", "rankedModeUnlocked": false, "region": "", - "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561199782684391\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", + "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561199041068696\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", "version": "173322" }, "isLeader": true, - "isReady": true, - "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab", - "socialId": "2c4eef11-d53e-48c2-a80f-a3f8234e61f7" + "isReady": false, + "playerId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9", + "socialId": "6dc9d069-97fe-5440-bdbb-eebeec1047e9" } ], "preferredGameServerZones": [ - "uscentral-1", - "uscentral-2", - "uswest-1", - "uswest-2", - "useast-1" + "uscentral-1" ], - "version": "3" + "version": "1" } }, - "rpcType": "PartyRpc.UpdatePartyV1Request", - "testAge": 1740507431.363 -} + "rpcType": "PartyRpc.CreateV1Request", + "ignoreReplace": true, + "testAge": 1740507431.194 +} \ No newline at end of file diff --git a/resources/testrequests/sequenced/party/3.json b/resources/testrequests/sequenced/party/3.json index 52536319..bc7b7b1f 100644 --- a/resources/testrequests/sequenced/party/3.json +++ b/resources/testrequests/sequenced/party/3.json @@ -2,13 +2,17 @@ "requestBody": { "partyId": "${responses[1]party.partyId}", "requestExt": { - "preferredRegions": [], - "preferredTeam": "SPECTATOR", - "refreshData": true, - "region": "", - "regionPings": {}, - "sharedClientData": "", - "version": "" + "acceptableRegions": [ + ], + "lobbyMode": "${responses[1]party.extBroadcastParty.lobbyMode}", + "pool": "${responses[1]party.extBroadcastParty.pool}", + "profile": "${responses[1]party.extBroadcastParty.profile}", + "region": "${responses[1]party.extBroadcastParty.region}", + "removePlayers": "${responses[1]party.extBroadcastParty.removePlayers}", + "standard": "${responses[1]party.extBroadcastParty.standard}", + "tag": "${responses[1]party.extBroadcastParty.tag}", + "useTeamMmr": "${responses[1]party.extBroadcastParty.useTeamMmr}", + "version": "${responses[1]party.extBroadcastParty.version}" } }, "responsePayload": { @@ -18,41 +22,33 @@ "crossPlayPreference": { "platform": "CROSS_PLAY_PLATFORM_PC" }, - "custom": { - "map": "Greenbelt_P", - "mode": "Custom", - "overtimeType": "disabled", - "teamAssignments": [ - { - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", - "team": 0.0 - } - ] - }, "hasAcceptableRegion": true, - "lobbyMode": "custom", - "pool": "custom", + "lobbyMode": "standard_casual", + "pool": "standard_casual", "profile": "", "region": "", "removePlayers": [], + "standard": { + "mode": "Standard" + }, "tag": "", "tokens": [], "useTeamMmr": false, - "version": "171268" + "version": "173322" }, "extPrivatePlayer": {}, - "inviteCode": "CDCXZ9", - "partyId": "4e398837-d121-4f37-b5fd-50d291ba45ab", + "inviteCode": "CDPLJB", + "partyId": "b0cfc843-4cbc-4e5f-9e59-0934ebad4f1f", "partyMembers": [ { "displayName": { - "discriminator": "0817", - "displayName": "LIMIT" + "discriminator": "3361", + "displayName": "SantaiGG" }, "ext": { "limitedInstancedInventory": [ { - "catalogId": "SpectreBannerItemDef:BannerID_Track_Kit01_District_01", + "catalogId": "SpectreBannerItemDef:BannerID_Default", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [] @@ -60,10 +56,10 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "ff01ef00-d0f2-4583-b4a4-cc56a606b404" + "instanceId": "4f30362e-0f4a-4389-8c43-4c588924ca09" }, { - "catalogId": "CharacterCosmeticItemOutfit:OutfitID_CyberLord", + "catalogId": "CharacterCosmeticItemOutfit:OutfitID_F_Default", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ @@ -78,23 +74,23 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "919e2723-a15b-40fa-a58f-c73eed03feb7" + "instanceId": "ac5035e3-6572-484b-a5d8-1ffcc1f367f1" }, { - "catalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", + "catalogId": "CharacterCosmeticItemHead:HeadID_Face_003", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ { "channelId": "SkinTone", "ownedAlterations": [ - "Option9" + "Option5" ] }, { "channelId": "LipColor", "ownedAlterations": [ - "Option2" + "Option3" ] }, { @@ -106,31 +102,13 @@ { "channelId": "EyebrowColor", "ownedAlterations": [ - "Option2" + "Option4" ] }, { "channelId": "Voice", "ownedAlterations": [ - "Option3" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "5779cd78-07e5-4216-8670-c4e41f506cdd" - }, - { - "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Color", - "ownedAlterations": [ - "Option4" + "Option5" ] } ] @@ -138,17 +116,17 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "d2cedf72-1091-4824-a936-5d97f83a0f8e" + "instanceId": "56490170-5353-4a9d-8f5f-14b0bbb91b63" }, { - "catalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_001", + "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0016", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ { "channelId": "Color", "ownedAlterations": [ - "Option1" + "Option7" ] } ] @@ -156,10 +134,10 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "eec7f364-69cd-47cd-92d0-e5062769d28c" + "instanceId": "5f021dba-65c9-48f9-ae02-3b6eebb1a430" }, { - "catalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Prince", + "catalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Default", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ @@ -174,10 +152,10 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "4d34a455-f92c-414c-94b0-2e5c65bc338b" + "instanceId": "40268935-f961-4783-934f-c46b3e7dc1db" }, { - "catalogId": "CharacterCosmeticItemHead:HeadID_Face_003_B", + "catalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ @@ -202,7 +180,7 @@ { "channelId": "EyebrowColor", "ownedAlterations": [ - "Option2" + "Option4" ] }, { @@ -216,17 +194,17 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "a60cf16b-306d-4874-bd0c-810d664688ac" + "instanceId": "711af3ec-f1e0-4c0b-ae88-4bd2c51eb23f" }, { - "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_CyberLord_01", + "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ { "channelId": "Color", "ownedAlterations": [ - "Option2" + "Option5" ] } ] @@ -234,1010 +212,125 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "4a47cf85-da23-4d36-82cb-2ae96757490f" - }, - { - "catalogId": "CharacterCosmeticItemSimple:HAccessoryID_M_Prince", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [] - }, - "reserved": 0.0, - "viewed": false + "instanceId": "a98f9664-2387-4931-b6a2-8f0e7983b856" + } + ], + "playerData": { + "attackerOutfitLoadout": { + "faceAccessoryData": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "", + "itemInstanceId": "", + "itemType": "" }, - "instanceId": "325ca087-46aa-4127-93b1-40ca17605d32" - }, - { - "catalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_002", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false + "faceStyleData": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "", + "itemInstanceId": "", + "itemType": "" }, - "instanceId": "45fe4b4a-acb5-42e4-adc6-f07663c907bf" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false + "hairData": { + "alterationData": [ + { + "alterationId": "Option7", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0016", + "itemInstanceId": "5F021DBA-65C9-48F9-AE02-3B6EEBB1A430", + "itemType": "" }, - "instanceId": "46a9620f-3129-49ed-9c9a-10427ec0043a" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - }, - { - "channelId": "FiringUpgrades", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "VFX", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "KillCeremony", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false + "headData": { + "alterationData": [ + { + "alterationId": "Option5", + "channelId": "SkinTone" + }, + { + "alterationId": "Option3", + "channelId": "LipColor" + }, + { + "alterationId": "Option6", + "channelId": "EyeColor" + }, + { + "alterationId": "Option4", + "channelId": "EyebrowColor" + }, + { + "alterationId": "Option5", + "channelId": "Voice" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_003", + "itemInstanceId": "56490170-5353-4A9D-8F5F-14B0BBB91B63", + "itemType": "" }, - "instanceId": "1b98ef70-cec6-455f-9e13-a4232176aa79" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false + "loadoutId": "53925c2d-5b68-48f8-88de-2d6999f9f3c6", + "outfitData": { + "alterationData": [ + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_F_Default", + "itemInstanceId": "AC5035E3-6572-484B-A5D8-1FFCC1F367F1", + "itemType": "" }, - "instanceId": "d79dbaeb-cc09-487b-be65-93b0bc5eb567" + "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "a83f4964-0532-4c9d-8219-47e9d557c9dc" + "attackerWeaponLoadout": { + "loadoutId": "5152439b-2aaf-484c-9de0-0fdea92d99c9", + "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "d14f3756-6b79-4c13-bb86-8ec7be42aed2" + "banner": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreBannerItemDef:BannerID_Default", + "itemInstanceId": "4F30362E-0F4A-4389-8C43-4C588924CA09", + "itemType": "" }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - }, - { - "channelId": "FiringUpgrades", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "VFX", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "KillCeremony", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "a134a5c9-1d26-4b57-bba3-5f49b541a28d" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "039108c5-c339-4dc4-84fc-c35d58f15a25" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "cecddd31-4eba-42bc-8910-e6971c010eab" - }, - { - "catalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_02", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "385aaac6-c736-4a2d-9ce0-e16e0a1cc736" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "9210c48c-1bf8-4234-b3b4-73f2675e92d6" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "3b231ab1-765d-4b93-9d70-0ac8650c0cf3" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "24bc0e97-808c-4361-9399-507cc6e608fe" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "85e6b34c-34e0-4e39-b6fa-57445146f6a8" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "06db77d5-849a-4509-895c-b24f28b800c4" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "9be5418b-b6d7-41e3-9b33-e28bb1545b96" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - }, - { - "channelId": "FiringUpgrades", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "VFX", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "KillCeremony", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "d10e5a1f-a703-4c0a-a415-4cbef6ed94ba" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "bf99937b-00b8-4178-9e0a-02b3a7ce3050" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "cc81fd53-27e9-424c-9bc3-ec74244e98b2" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "7f9b8d30-c0f2-4c24-985e-8bcb74e6c4fb" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - }, - { - "channelId": "FiringUpgrades", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "VFX", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "KillCeremony", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "c5f0d728-563d-4d10-be1c-aee45e98c7bd" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - }, - { - "channelId": "VFX", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "KillCeremony", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "Trail", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "930216bb-2e63-488f-9faf-25b7eaa334ca" - }, - { - "catalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_06", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "b291c752-10a2-47d6-84e2-d33736d0c550" - } - ], - "playerData": { - "attackerOutfitLoadout": { - "faceAccessoryData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "", - "itemInstanceId": "", - "itemType": "" - }, - "faceStyleData": { - "alterationData": [ - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_001", - "itemInstanceId": "EEC7F364-69CD-47CD-92D0-E5062769D28C", - "itemType": "" - }, - "hairData": { - "alterationData": [ - { - "alterationId": "Option4", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", - "itemInstanceId": "D2CEDF72-1091-4824-A936-5D97F83A0F8E", - "itemType": "" - }, - "headData": { - "alterationData": [ - { - "alterationId": "Option9", - "channelId": "SkinTone" - }, - { - "alterationId": "Option2", - "channelId": "LipColor" - }, - { - "alterationId": "Option6", - "channelId": "EyeColor" - }, - { - "alterationId": "Option2", - "channelId": "EyebrowColor" - }, - { - "alterationId": "Option3", - "channelId": "Voice" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", - "itemInstanceId": "5779CD78-07E5-4216-8670-C4E41F506CDD", - "itemType": "" - }, - "loadoutId": "0ade2a13-7977-43bf-ab50-9c2122987544", - "outfitData": { - "alterationData": [ - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_CyberLord", - "itemInstanceId": "919E2723-A15B-40FA-A58F-C73EED03FEB7", - "itemType": "" - }, - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d" - }, - "attackerWeaponLoadout": { - "autoPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", - "itemInstanceId": "1B98EF70-CEC6-455F-9E13-A4232176AA79", - "itemType": "" - }, - "autoShotgunData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", - "itemInstanceId": "24BC0E97-808C-4361-9399-507CC6E608FE", - "itemType": "" - }, - "boltactionSniperData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", - "itemInstanceId": "D10E5A1F-A703-4C0A-A415-4CBEF6ED94BA", - "itemType": "" - }, - "burstArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", - "itemInstanceId": "039108C5-C339-4DC4-84FC-C35D58F15A25", - "itemType": "" - }, - "heavyArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_06", - "attachmentItemInstanceId": "B291C752-10A2-47D6-84E2-D33736D0C550", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", - "itemInstanceId": "CECDDD31-4EBA-42BC-8910-E6971C010EAB", - "itemType": "" - }, - "heavyShotgunData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", - "itemInstanceId": "85E6B34C-34E0-4E39-B6FA-57445146F6A8", - "itemType": "" - }, - "highcalMgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", - "itemInstanceId": "06DB77D5-849A-4509-895C-B24F28B800C4", - "itemType": "" - }, - "highcalPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", - "itemInstanceId": "A83F4964-0532-4C9D-8219-47E9D557C9DC", - "itemType": "" - }, - "loadoutId": "00f12c51-62e8-4320-b2f2-a07e87d52662", - "meleeData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "On", - "channelId": "Trail" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", - "itemInstanceId": "930216BB-2E63-488F-9FAF-25B7EAA334CA", - "itemType": "" - }, - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", - "rapidfireMgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", - "itemInstanceId": "9BE5418B-B6D7-41E3-9B33-E28BB1545B96", - "itemType": "" - }, - "rapidfireSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", - "itemInstanceId": "CC81FD53-27E9-424C-9BC3-EC74244E98B2", - "itemType": "" - }, - "semiautoArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", - "itemInstanceId": "D14F3756-6B79-4C13-BB86-8EC7BE42AED2", - "itemType": "" - }, - "semiautoPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", - "itemInstanceId": "46A9620F-3129-49ED-9C9A-10427EC0043A", - "itemType": "" - }, - "semiautoSniperData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", - "itemInstanceId": "BF99937B-00B8-4178-9E0A-02B3A7CE3050", - "itemType": "" - }, - "standardArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", - "itemInstanceId": "A134A5C9-1D26-4B57-BBA3-5F49B541A28D", - "itemType": "" - }, - "suppressedArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", - "itemInstanceId": "9210C48C-1BF8-4234-B3B4-73F2675E92D6", - "itemType": "" - }, - "suppressedPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", - "itemInstanceId": "D79DBAEB-CC09-487B-BE65-93B0BC5EB567", - "itemType": "" - }, - "suppressedSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", - "itemInstanceId": "C5F0D728-563D-4D10-BE1C-AEE45E98C7BD", - "itemType": "" - }, - "tacticalArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", - "itemInstanceId": "3B231AB1-765D-4B93-9D70-0AC8650C0CF3", - "itemType": "" - }, - "tacticalSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", - "itemInstanceId": "7F9B8D30-C0F2-4C24-985E-8BCB74E6C4FB", - "itemType": "" - } - }, - "banner": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreBannerItemDef:BannerID_Track_Kit01_District_01", - "itemInstanceId": "FF01EF00-D0F2-4583-B4A4-CC56A606B404", - "itemType": "" - }, - "defenderOutfitLoadout": { - "faceAccessoryData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HAccessoryID_M_Prince", - "itemInstanceId": "325CA087-46AA-4127-93B1-40CA17605D32", - "itemType": "" + "defenderOutfitLoadout": { + "faceAccessoryData": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "", + "itemInstanceId": "", + "itemType": "" }, "faceStyleData": { - "alterationData": [ - { - "alterationId": "Option1", - "channelId": "Color" - } - ], + "alterationData": [], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_002", - "itemInstanceId": "45FE4B4A-ACB5-42E4-ADC6-F07663C907BF", + "itemCatalogId": "", + "itemInstanceId": "", "itemType": "" }, "hairData": { "alterationData": [ { - "alterationId": "Option2", + "alterationId": "Option5", "channelId": "Color" } ], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_CyberLord_01", - "itemInstanceId": "4A47CF85-DA23-4D36-82CB-2AE96757490F", + "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", + "itemInstanceId": "A98F9664-2387-4931-B6A2-8F0E7983B856", "itemType": "" }, "headData": { @@ -1255,7 +348,7 @@ "channelId": "EyeColor" }, { - "alterationId": "Option2", + "alterationId": "Option4", "channelId": "EyebrowColor" }, { @@ -1265,11 +358,11 @@ ], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_003_B", - "itemInstanceId": "A60CF16B-306D-4874-BD0C-810D664688AC", + "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", + "itemInstanceId": "711AF3EC-F1E0-4C0B-AE88-4BD2C51EB23F", "itemType": "" }, - "loadoutId": "6299fca0-fb5b-4d69-8932-9495ee0521ed", + "loadoutId": "ebb85cdd-827a-4884-9eb5-0b584d43be19", "outfitData": { "alterationData": [ { @@ -1279,355 +372,19 @@ ], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Prince", - "itemInstanceId": "4D34A455-F92C-414C-94B0-2E5C65BC338B", + "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Default", + "itemInstanceId": "40268935-F961-4783-934F-C46B3E7DC1DB", "itemType": "" }, - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d" + "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" }, "defenderWeaponLoadout": { - "autoPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", - "itemInstanceId": "1B98EF70-CEC6-455F-9E13-A4232176AA79", - "itemType": "" - }, - "autoShotgunData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", - "itemInstanceId": "24BC0E97-808C-4361-9399-507CC6E608FE", - "itemType": "" - }, - "boltactionSniperData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", - "itemInstanceId": "D10E5A1F-A703-4C0A-A415-4CBEF6ED94BA", - "itemType": "" - }, - "burstArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", - "itemInstanceId": "039108C5-C339-4DC4-84FC-C35D58F15A25", - "itemType": "" - }, - "heavyArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_02", - "attachmentItemInstanceId": "385AAAC6-C736-4A2D-9CE0-E16E0A1CC736", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", - "itemInstanceId": "CECDDD31-4EBA-42BC-8910-E6971C010EAB", - "itemType": "" - }, - "heavyShotgunData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", - "itemInstanceId": "85E6B34C-34E0-4E39-B6FA-57445146F6A8", - "itemType": "" - }, - "highcalMgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", - "itemInstanceId": "06DB77D5-849A-4509-895C-B24F28B800C4", - "itemType": "" - }, - "highcalPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", - "itemInstanceId": "A83F4964-0532-4C9D-8219-47E9D557C9DC", - "itemType": "" - }, - "loadoutId": "17a07016-4264-4511-9f12-8b2665082ecb", - "meleeData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "On", - "channelId": "Trail" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", - "itemInstanceId": "930216BB-2E63-488F-9FAF-25B7EAA334CA", - "itemType": "" - }, - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", - "rapidfireMgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", - "itemInstanceId": "9BE5418B-B6D7-41E3-9B33-E28BB1545B96", - "itemType": "" - }, - "rapidfireSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", - "itemInstanceId": "CC81FD53-27E9-424C-9BC3-EC74244E98B2", - "itemType": "" - }, - "semiautoArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", - "itemInstanceId": "D14F3756-6B79-4C13-BB86-8EC7BE42AED2", - "itemType": "" - }, - "semiautoPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", - "itemInstanceId": "46A9620F-3129-49ED-9C9A-10427EC0043A", - "itemType": "" - }, - "semiautoSniperData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", - "itemInstanceId": "BF99937B-00B8-4178-9E0A-02B3A7CE3050", - "itemType": "" - }, - "standardArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", - "itemInstanceId": "A134A5C9-1D26-4B57-BBA3-5F49B541A28D", - "itemType": "" - }, - "suppressedArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", - "itemInstanceId": "9210C48C-1BF8-4234-B3B4-73F2675E92D6", - "itemType": "" - }, - "suppressedPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", - "itemInstanceId": "D79DBAEB-CC09-487B-BE65-93B0BC5EB567", - "itemType": "" - }, - "suppressedSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", - "itemInstanceId": "C5F0D728-563D-4D10-BE1C-AEE45E98C7BD", - "itemType": "" - }, - "tacticalArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", - "itemInstanceId": "3B231AB1-765D-4B93-9D70-0AC8650C0CF3", - "itemType": "" - }, - "tacticalSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", - "itemInstanceId": "7F9B8D30-C0F2-4C24-985E-8BCB74E6C4FB", - "itemType": "" - } + "loadoutId": "ff09ddea-2594-4bea-abd1-d6f255ce3f57", + "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" }, "matchmakingData": { - "casualMatchesPlayedCount": 16.0, - "casualMatchesWonCount": 7.0, + "casualMatchesPlayedCount": 0.0, + "casualMatchesWonCount": 0.0, "currentSoloRank": 0.0, "highestTeamRank": 0.0, "matchmakingFlags": 1.0, @@ -1639,27 +396,27 @@ } }, "preferredTeam": "TEAM0", - "rankedModeUnlocked": true, + "rankedModeUnlocked": false, "region": "", - "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561197961806213\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", - "version": "171268" + "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561199782684391\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", + "version": "173322" }, "isLeader": true, "isReady": true, - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", - "socialId": "3213444f-7b0f-4168-8220-2013159acc50" + "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab", + "socialId": "2c4eef11-d53e-48c2-a80f-a3f8234e61f7" } ], "preferredGameServerZones": [ "uscentral-1", - "uswest-2", "uscentral-2", "uswest-1", + "uswest-2", "useast-1" ], - "version": "10" + "version": "3" } }, - "rpcType": "PartyRpc.UpdatePartyPlayerV1Request", - "testAge": 1740506574.492 -} \ No newline at end of file + "rpcType": "PartyRpc.UpdatePartyV1Request", + "testAge": 1740507431.363 +} diff --git a/resources/testrequests/sequenced/party/4.json b/resources/testrequests/sequenced/party/4.json index 9c7cade7..52536319 100644 --- a/resources/testrequests/sequenced/party/4.json +++ b/resources/testrequests/sequenced/party/4.json @@ -1,7 +1,15 @@ { "requestBody": { "partyId": "${responses[1]party.partyId}", - "ready": true + "requestExt": { + "preferredRegions": [], + "preferredTeam": "SPECTATOR", + "refreshData": true, + "region": "", + "regionPings": {}, + "sharedClientData": "", + "version": "" + } }, "responsePayload": { "party": { @@ -10,33 +18,41 @@ "crossPlayPreference": { "platform": "CROSS_PLAY_PLATFORM_PC" }, + "custom": { + "map": "Greenbelt_P", + "mode": "Custom", + "overtimeType": "disabled", + "teamAssignments": [ + { + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", + "team": 0.0 + } + ] + }, "hasAcceptableRegion": true, - "lobbyMode": "standard_casual", - "pool": "", + "lobbyMode": "custom", + "pool": "custom", "profile": "", "region": "", "removePlayers": [], - "standard": { - "mode": "Standard" - }, "tag": "", "tokens": [], "useTeamMmr": false, "version": "171268" }, "extPrivatePlayer": {}, - "inviteCode": "CDPLJB", - "partyId": "*", + "inviteCode": "CDCXZ9", + "partyId": "4e398837-d121-4f37-b5fd-50d291ba45ab", "partyMembers": [ { "displayName": { - "discriminator": "3361", - "displayName": "SantaiGG" + "discriminator": "0817", + "displayName": "LIMIT" }, "ext": { "limitedInstancedInventory": [ { - "catalogId": "SpectreBannerItemDef:BannerID_Default", + "catalogId": "SpectreBannerItemDef:BannerID_Track_Kit01_District_01", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [] @@ -44,10 +60,10 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "4f30362e-0f4a-4389-8c43-4c588924ca09" + "instanceId": "ff01ef00-d0f2-4583-b4a4-cc56a606b404" }, { - "catalogId": "CharacterCosmeticItemOutfit:OutfitID_F_Default", + "catalogId": "CharacterCosmeticItemOutfit:OutfitID_CyberLord", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ @@ -62,23 +78,23 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "ac5035e3-6572-484b-a5d8-1ffcc1f367f1" + "instanceId": "919e2723-a15b-40fa-a58f-c73eed03feb7" }, { - "catalogId": "CharacterCosmeticItemHead:HeadID_Face_003", + "catalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ { "channelId": "SkinTone", "ownedAlterations": [ - "Option5" + "Option9" ] }, { "channelId": "LipColor", "ownedAlterations": [ - "Option3" + "Option2" ] }, { @@ -90,13 +106,13 @@ { "channelId": "EyebrowColor", "ownedAlterations": [ - "Option4" + "Option2" ] }, { "channelId": "Voice", "ownedAlterations": [ - "Option5" + "Option3" ] } ] @@ -104,17 +120,17 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "56490170-5353-4a9d-8f5f-14b0bbb91b63" + "instanceId": "5779cd78-07e5-4216-8670-c4e41f506cdd" }, { - "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0016", + "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ { "channelId": "Color", "ownedAlterations": [ - "Option7" + "Option4" ] } ] @@ -122,10 +138,10 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "5f021dba-65c9-48f9-ae02-3b6eebb1a430" + "instanceId": "d2cedf72-1091-4824-a936-5d97f83a0f8e" }, { - "catalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Default", + "catalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_001", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ @@ -140,10 +156,28 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "40268935-f961-4783-934f-c46b3e7dc1db" + "instanceId": "eec7f364-69cd-47cd-92d0-e5062769d28c" }, { - "catalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", + "catalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Prince", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "4d34a455-f92c-414c-94b0-2e5c65bc338b" + }, + { + "catalogId": "CharacterCosmeticItemHead:HeadID_Face_003_B", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ @@ -168,7 +202,7 @@ { "channelId": "EyebrowColor", "ownedAlterations": [ - "Option4" + "Option2" ] }, { @@ -182,17 +216,17 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "711af3ec-f1e0-4c0b-ae88-4bd2c51eb23f" + "instanceId": "a60cf16b-306d-4874-bd0c-810d664688ac" }, { - "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", + "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_CyberLord_01", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ { "channelId": "Color", "ownedAlterations": [ - "Option5" + "Option2" ] } ] @@ -200,125 +234,1010 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "a98f9664-2387-4931-b6a2-8f0e7983b856" - } - ], - "playerData": { - "attackerOutfitLoadout": { - "faceAccessoryData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "", - "itemInstanceId": "", - "itemType": "" - }, - "faceStyleData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "", - "itemInstanceId": "", - "itemType": "" + "instanceId": "4a47cf85-da23-4d36-82cb-2ae96757490f" + }, + { + "catalogId": "CharacterCosmeticItemSimple:HAccessoryID_M_Prince", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [] + }, + "reserved": 0.0, + "viewed": false }, - "hairData": { - "alterationData": [ - { - "alterationId": "Option7", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0016", - "itemInstanceId": "5F021DBA-65C9-48F9-AE02-3B6EEBB1A430", - "itemType": "" + "instanceId": "325ca087-46aa-4127-93b1-40ca17605d32" + }, + { + "catalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_002", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false }, - "headData": { - "alterationData": [ - { - "alterationId": "Option5", - "channelId": "SkinTone" - }, - { - "alterationId": "Option3", - "channelId": "LipColor" - }, - { - "alterationId": "Option6", - "channelId": "EyeColor" - }, - { - "alterationId": "Option4", - "channelId": "EyebrowColor" - }, - { - "alterationId": "Option5", - "channelId": "Voice" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_003", - "itemInstanceId": "56490170-5353-4A9D-8F5F-14B0BBB91B63", - "itemType": "" + "instanceId": "45fe4b4a-acb5-42e4-adc6-f07663c907bf" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false }, - "loadoutId": "53925c2d-5b68-48f8-88de-2d6999f9f3c6", - "outfitData": { - "alterationData": [ - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_F_Default", - "itemInstanceId": "AC5035E3-6572-484B-A5D8-1FFCC1F367F1", - "itemType": "" + "instanceId": "46a9620f-3129-49ed-9c9a-10427ec0043a" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + }, + { + "channelId": "FiringUpgrades", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "VFX", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "KillCeremony", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false }, - "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" + "instanceId": "1b98ef70-cec6-455f-9e13-a4232176aa79" }, - "attackerWeaponLoadout": { - "loadoutId": "5152439b-2aaf-484c-9de0-0fdea92d99c9", - "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "d79dbaeb-cc09-487b-be65-93b0bc5eb567" }, - "banner": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreBannerItemDef:BannerID_Default", - "itemInstanceId": "4F30362E-0F4A-4389-8C43-4C588924CA09", - "itemType": "" + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "a83f4964-0532-4c9d-8219-47e9d557c9dc" }, - "defenderOutfitLoadout": { - "faceAccessoryData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "", - "itemInstanceId": "", - "itemType": "" + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false }, - "faceStyleData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "", - "itemInstanceId": "", - "itemType": "" + "instanceId": "d14f3756-6b79-4c13-bb86-8ec7be42aed2" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + }, + { + "channelId": "FiringUpgrades", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "VFX", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "KillCeremony", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false }, - "hairData": { - "alterationData": [ - { - "alterationId": "Option5", - "channelId": "Color" - } + "instanceId": "a134a5c9-1d26-4b57-bba3-5f49b541a28d" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "039108c5-c339-4dc4-84fc-c35d58f15a25" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "cecddd31-4eba-42bc-8910-e6971c010eab" + }, + { + "catalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_02", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "385aaac6-c736-4a2d-9ce0-e16e0a1cc736" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "9210c48c-1bf8-4234-b3b4-73f2675e92d6" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "3b231ab1-765d-4b93-9d70-0ac8650c0cf3" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "24bc0e97-808c-4361-9399-507cc6e608fe" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "85e6b34c-34e0-4e39-b6fa-57445146f6a8" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "06db77d5-849a-4509-895c-b24f28b800c4" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "9be5418b-b6d7-41e3-9b33-e28bb1545b96" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + }, + { + "channelId": "FiringUpgrades", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "VFX", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "KillCeremony", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "d10e5a1f-a703-4c0a-a415-4cbef6ed94ba" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "bf99937b-00b8-4178-9e0a-02b3a7ce3050" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "cc81fd53-27e9-424c-9bc3-ec74244e98b2" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "7f9b8d30-c0f2-4c24-985e-8bcb74e6c4fb" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + }, + { + "channelId": "FiringUpgrades", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "VFX", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "KillCeremony", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "c5f0d728-563d-4d10-be1c-aee45e98c7bd" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + }, + { + "channelId": "VFX", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "KillCeremony", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "Trail", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "930216bb-2e63-488f-9faf-25b7eaa334ca" + }, + { + "catalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_06", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "b291c752-10a2-47d6-84e2-d33736d0c550" + } + ], + "playerData": { + "attackerOutfitLoadout": { + "faceAccessoryData": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "", + "itemInstanceId": "", + "itemType": "" + }, + "faceStyleData": { + "alterationData": [ + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_001", + "itemInstanceId": "EEC7F364-69CD-47CD-92D0-E5062769D28C", + "itemType": "" + }, + "hairData": { + "alterationData": [ + { + "alterationId": "Option4", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", + "itemInstanceId": "D2CEDF72-1091-4824-A936-5D97F83A0F8E", + "itemType": "" + }, + "headData": { + "alterationData": [ + { + "alterationId": "Option9", + "channelId": "SkinTone" + }, + { + "alterationId": "Option2", + "channelId": "LipColor" + }, + { + "alterationId": "Option6", + "channelId": "EyeColor" + }, + { + "alterationId": "Option2", + "channelId": "EyebrowColor" + }, + { + "alterationId": "Option3", + "channelId": "Voice" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", + "itemInstanceId": "5779CD78-07E5-4216-8670-C4E41F506CDD", + "itemType": "" + }, + "loadoutId": "0ade2a13-7977-43bf-ab50-9c2122987544", + "outfitData": { + "alterationData": [ + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_CyberLord", + "itemInstanceId": "919E2723-A15B-40FA-A58F-C73EED03FEB7", + "itemType": "" + }, + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d" + }, + "attackerWeaponLoadout": { + "autoPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", + "itemInstanceId": "1B98EF70-CEC6-455F-9E13-A4232176AA79", + "itemType": "" + }, + "autoShotgunData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", + "itemInstanceId": "24BC0E97-808C-4361-9399-507CC6E608FE", + "itemType": "" + }, + "boltactionSniperData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", + "itemInstanceId": "D10E5A1F-A703-4C0A-A415-4CBEF6ED94BA", + "itemType": "" + }, + "burstArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", + "itemInstanceId": "039108C5-C339-4DC4-84FC-C35D58F15A25", + "itemType": "" + }, + "heavyArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_06", + "attachmentItemInstanceId": "B291C752-10A2-47D6-84E2-D33736D0C550", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", + "itemInstanceId": "CECDDD31-4EBA-42BC-8910-E6971C010EAB", + "itemType": "" + }, + "heavyShotgunData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", + "itemInstanceId": "85E6B34C-34E0-4E39-B6FA-57445146F6A8", + "itemType": "" + }, + "highcalMgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", + "itemInstanceId": "06DB77D5-849A-4509-895C-B24F28B800C4", + "itemType": "" + }, + "highcalPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", + "itemInstanceId": "A83F4964-0532-4C9D-8219-47E9D557C9DC", + "itemType": "" + }, + "loadoutId": "00f12c51-62e8-4320-b2f2-a07e87d52662", + "meleeData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "On", + "channelId": "Trail" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", + "itemInstanceId": "930216BB-2E63-488F-9FAF-25B7EAA334CA", + "itemType": "" + }, + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", + "rapidfireMgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", + "itemInstanceId": "9BE5418B-B6D7-41E3-9B33-E28BB1545B96", + "itemType": "" + }, + "rapidfireSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", + "itemInstanceId": "CC81FD53-27E9-424C-9BC3-EC74244E98B2", + "itemType": "" + }, + "semiautoArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", + "itemInstanceId": "D14F3756-6B79-4C13-BB86-8EC7BE42AED2", + "itemType": "" + }, + "semiautoPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", + "itemInstanceId": "46A9620F-3129-49ED-9C9A-10427EC0043A", + "itemType": "" + }, + "semiautoSniperData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", + "itemInstanceId": "BF99937B-00B8-4178-9E0A-02B3A7CE3050", + "itemType": "" + }, + "standardArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", + "itemInstanceId": "A134A5C9-1D26-4B57-BBA3-5F49B541A28D", + "itemType": "" + }, + "suppressedArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", + "itemInstanceId": "9210C48C-1BF8-4234-B3B4-73F2675E92D6", + "itemType": "" + }, + "suppressedPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", + "itemInstanceId": "D79DBAEB-CC09-487B-BE65-93B0BC5EB567", + "itemType": "" + }, + "suppressedSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", + "itemInstanceId": "C5F0D728-563D-4D10-BE1C-AEE45E98C7BD", + "itemType": "" + }, + "tacticalArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } ], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", - "itemInstanceId": "A98F9664-2387-4931-B6A2-8F0E7983B856", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", + "itemInstanceId": "3B231AB1-765D-4B93-9D70-0AC8650C0CF3", + "itemType": "" + }, + "tacticalSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", + "itemInstanceId": "7F9B8D30-C0F2-4C24-985E-8BCB74E6C4FB", + "itemType": "" + } + }, + "banner": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreBannerItemDef:BannerID_Track_Kit01_District_01", + "itemInstanceId": "FF01EF00-D0F2-4583-B4A4-CC56A606B404", + "itemType": "" + }, + "defenderOutfitLoadout": { + "faceAccessoryData": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HAccessoryID_M_Prince", + "itemInstanceId": "325CA087-46AA-4127-93B1-40CA17605D32", + "itemType": "" + }, + "faceStyleData": { + "alterationData": [ + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_002", + "itemInstanceId": "45FE4B4A-ACB5-42E4-ADC6-F07663C907BF", + "itemType": "" + }, + "hairData": { + "alterationData": [ + { + "alterationId": "Option2", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_CyberLord_01", + "itemInstanceId": "4A47CF85-DA23-4D36-82CB-2AE96757490F", "itemType": "" }, "headData": { @@ -336,7 +1255,7 @@ "channelId": "EyeColor" }, { - "alterationId": "Option4", + "alterationId": "Option2", "channelId": "EyebrowColor" }, { @@ -346,11 +1265,11 @@ ], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", - "itemInstanceId": "711AF3EC-F1E0-4C0B-AE88-4BD2C51EB23F", + "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_003_B", + "itemInstanceId": "A60CF16B-306D-4874-BD0C-810D664688AC", "itemType": "" }, - "loadoutId": "ebb85cdd-827a-4884-9eb5-0b584d43be19", + "loadoutId": "6299fca0-fb5b-4d69-8932-9495ee0521ed", "outfitData": { "alterationData": [ { @@ -360,19 +1279,355 @@ ], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Default", - "itemInstanceId": "40268935-F961-4783-934F-C46B3E7DC1DB", + "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Prince", + "itemInstanceId": "4D34A455-F92C-414C-94B0-2E5C65BC338B", "itemType": "" }, - "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d" }, "defenderWeaponLoadout": { - "loadoutId": "ff09ddea-2594-4bea-abd1-d6f255ce3f57", - "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" + "autoPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", + "itemInstanceId": "1B98EF70-CEC6-455F-9E13-A4232176AA79", + "itemType": "" + }, + "autoShotgunData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", + "itemInstanceId": "24BC0E97-808C-4361-9399-507CC6E608FE", + "itemType": "" + }, + "boltactionSniperData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", + "itemInstanceId": "D10E5A1F-A703-4C0A-A415-4CBEF6ED94BA", + "itemType": "" + }, + "burstArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", + "itemInstanceId": "039108C5-C339-4DC4-84FC-C35D58F15A25", + "itemType": "" + }, + "heavyArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_02", + "attachmentItemInstanceId": "385AAAC6-C736-4A2D-9CE0-E16E0A1CC736", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", + "itemInstanceId": "CECDDD31-4EBA-42BC-8910-E6971C010EAB", + "itemType": "" + }, + "heavyShotgunData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", + "itemInstanceId": "85E6B34C-34E0-4E39-B6FA-57445146F6A8", + "itemType": "" + }, + "highcalMgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", + "itemInstanceId": "06DB77D5-849A-4509-895C-B24F28B800C4", + "itemType": "" + }, + "highcalPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", + "itemInstanceId": "A83F4964-0532-4C9D-8219-47E9D557C9DC", + "itemType": "" + }, + "loadoutId": "17a07016-4264-4511-9f12-8b2665082ecb", + "meleeData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "On", + "channelId": "Trail" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", + "itemInstanceId": "930216BB-2E63-488F-9FAF-25B7EAA334CA", + "itemType": "" + }, + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", + "rapidfireMgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", + "itemInstanceId": "9BE5418B-B6D7-41E3-9B33-E28BB1545B96", + "itemType": "" + }, + "rapidfireSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", + "itemInstanceId": "CC81FD53-27E9-424C-9BC3-EC74244E98B2", + "itemType": "" + }, + "semiautoArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", + "itemInstanceId": "D14F3756-6B79-4C13-BB86-8EC7BE42AED2", + "itemType": "" + }, + "semiautoPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", + "itemInstanceId": "46A9620F-3129-49ED-9C9A-10427EC0043A", + "itemType": "" + }, + "semiautoSniperData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", + "itemInstanceId": "BF99937B-00B8-4178-9E0A-02B3A7CE3050", + "itemType": "" + }, + "standardArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", + "itemInstanceId": "A134A5C9-1D26-4B57-BBA3-5F49B541A28D", + "itemType": "" + }, + "suppressedArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", + "itemInstanceId": "9210C48C-1BF8-4234-B3B4-73F2675E92D6", + "itemType": "" + }, + "suppressedPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", + "itemInstanceId": "D79DBAEB-CC09-487B-BE65-93B0BC5EB567", + "itemType": "" + }, + "suppressedSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", + "itemInstanceId": "C5F0D728-563D-4D10-BE1C-AEE45E98C7BD", + "itemType": "" + }, + "tacticalArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", + "itemInstanceId": "3B231AB1-765D-4B93-9D70-0AC8650C0CF3", + "itemType": "" + }, + "tacticalSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", + "itemInstanceId": "7F9B8D30-C0F2-4C24-985E-8BCB74E6C4FB", + "itemType": "" + } }, "matchmakingData": { - "casualMatchesPlayedCount": 0.0, - "casualMatchesWonCount": 0.0, + "casualMatchesPlayedCount": 16.0, + "casualMatchesWonCount": 7.0, "currentSoloRank": 0.0, "highestTeamRank": 0.0, "matchmakingFlags": 1.0, @@ -384,27 +1639,27 @@ } }, "preferredTeam": "TEAM0", - "rankedModeUnlocked": false, + "rankedModeUnlocked": true, "region": "", - "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561199782684391\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", + "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561197961806213\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", "version": "171268" }, "isLeader": true, "isReady": true, - "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab", - "socialId": "2c4eef11-d53e-48c2-a80f-a3f8234e61f7" + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", + "socialId": "3213444f-7b0f-4168-8220-2013159acc50" } ], "preferredGameServerZones": [ "uscentral-1", + "uswest-2", "uscentral-2", "uswest-1", - "uswest-2", "useast-1" ], - "version": "2" + "version": "10" } }, - "rpcType": "PartyRpc.SetReadyStateV1Request", - "testAge": 1740507431.329 + "rpcType": "PartyRpc.UpdatePartyPlayerV1Request", + "testAge": 1740506574.492 } \ No newline at end of file diff --git a/resources/testrequests/sequenced/party/5.json b/resources/testrequests/sequenced/party/5.json index ee97cadb..9c7cade7 100644 --- a/resources/testrequests/sequenced/party/5.json +++ b/resources/testrequests/sequenced/party/5.json @@ -1,6 +1,7 @@ { "requestBody": { - "partyId": "${responses[1]party.partyId}" + "partyId": "${responses[1]party.partyId}", + "ready": true }, "responsePayload": { "party": { @@ -9,41 +10,33 @@ "crossPlayPreference": { "platform": "CROSS_PLAY_PLATFORM_PC" }, - "custom": { - "map": "Greenbelt_P", - "mode": "Custom", - "overtimeType": "disabled", - "teamAssignments": [ - { - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", - "team": 0.0 - } - ] - }, "hasAcceptableRegion": true, - "lobbyMode": "custom", - "pool": "custom", + "lobbyMode": "standard_casual", + "pool": "", "profile": "", "region": "", "removePlayers": [], + "standard": { + "mode": "Standard" + }, "tag": "", "tokens": [], "useTeamMmr": false, "version": "171268" }, "extPrivatePlayer": {}, - "inviteCode": "CDCXZ9", - "partyId": "4e398837-d121-4f37-b5fd-50d291ba45ab", + "inviteCode": "CDPLJB", + "partyId": "*", "partyMembers": [ { "displayName": { - "discriminator": "0817", - "displayName": "LIMIT" + "discriminator": "3361", + "displayName": "SantaiGG" }, "ext": { "limitedInstancedInventory": [ { - "catalogId": "SpectreBannerItemDef:BannerID_Track_Kit01_District_01", + "catalogId": "SpectreBannerItemDef:BannerID_Default", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [] @@ -51,10 +44,10 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "ff01ef00-d0f2-4583-b4a4-cc56a606b404" + "instanceId": "4f30362e-0f4a-4389-8c43-4c588924ca09" }, { - "catalogId": "CharacterCosmeticItemOutfit:OutfitID_CyberLord", + "catalogId": "CharacterCosmeticItemOutfit:OutfitID_F_Default", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ @@ -69,23 +62,23 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "919e2723-a15b-40fa-a58f-c73eed03feb7" + "instanceId": "ac5035e3-6572-484b-a5d8-1ffcc1f367f1" }, { - "catalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", + "catalogId": "CharacterCosmeticItemHead:HeadID_Face_003", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ { "channelId": "SkinTone", "ownedAlterations": [ - "Option9" + "Option5" ] }, { "channelId": "LipColor", "ownedAlterations": [ - "Option2" + "Option3" ] }, { @@ -97,31 +90,13 @@ { "channelId": "EyebrowColor", "ownedAlterations": [ - "Option2" + "Option4" ] }, { "channelId": "Voice", "ownedAlterations": [ - "Option3" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "5779cd78-07e5-4216-8670-c4e41f506cdd" - }, - { - "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Color", - "ownedAlterations": [ - "Option4" + "Option5" ] } ] @@ -129,17 +104,17 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "d2cedf72-1091-4824-a936-5d97f83a0f8e" + "instanceId": "56490170-5353-4a9d-8f5f-14b0bbb91b63" }, { - "catalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_001", + "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0016", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ { "channelId": "Color", "ownedAlterations": [ - "Option1" + "Option7" ] } ] @@ -147,10 +122,10 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "eec7f364-69cd-47cd-92d0-e5062769d28c" + "instanceId": "5f021dba-65c9-48f9-ae02-3b6eebb1a430" }, { - "catalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Prince", + "catalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Default", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ @@ -165,10 +140,10 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "4d34a455-f92c-414c-94b0-2e5c65bc338b" + "instanceId": "40268935-f961-4783-934f-c46b3e7dc1db" }, { - "catalogId": "CharacterCosmeticItemHead:HeadID_Face_003_B", + "catalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ @@ -193,7 +168,7 @@ { "channelId": "EyebrowColor", "ownedAlterations": [ - "Option2" + "Option4" ] }, { @@ -207,17 +182,17 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "a60cf16b-306d-4874-bd0c-810d664688ac" + "instanceId": "711af3ec-f1e0-4c0b-ae88-4bd2c51eb23f" }, { - "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_CyberLord_01", + "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", "ext": { "instancedCustomizationData": { "instancedAlterationChannels": [ { "channelId": "Color", "ownedAlterations": [ - "Option2" + "Option5" ] } ] @@ -225,992 +200,125 @@ "reserved": 0.0, "viewed": false }, - "instanceId": "4a47cf85-da23-4d36-82cb-2ae96757490f" - }, - { - "catalogId": "CharacterCosmeticItemSimple:HAccessoryID_M_Prince", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "325ca087-46aa-4127-93b1-40ca17605d32" - }, - { - "catalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_002", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false + "instanceId": "a98f9664-2387-4931-b6a2-8f0e7983b856" + } + ], + "playerData": { + "attackerOutfitLoadout": { + "faceAccessoryData": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "", + "itemInstanceId": "", + "itemType": "" }, - "instanceId": "45fe4b4a-acb5-42e4-adc6-f07663c907bf" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false + "faceStyleData": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "", + "itemInstanceId": "", + "itemType": "" }, - "instanceId": "46a9620f-3129-49ed-9c9a-10427ec0043a" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - }, - { - "channelId": "FiringUpgrades", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "VFX", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "KillCeremony", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false + "hairData": { + "alterationData": [ + { + "alterationId": "Option7", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0016", + "itemInstanceId": "5F021DBA-65C9-48F9-AE02-3B6EEBB1A430", + "itemType": "" }, - "instanceId": "1b98ef70-cec6-455f-9e13-a4232176aa79" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false + "headData": { + "alterationData": [ + { + "alterationId": "Option5", + "channelId": "SkinTone" + }, + { + "alterationId": "Option3", + "channelId": "LipColor" + }, + { + "alterationId": "Option6", + "channelId": "EyeColor" + }, + { + "alterationId": "Option4", + "channelId": "EyebrowColor" + }, + { + "alterationId": "Option5", + "channelId": "Voice" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_003", + "itemInstanceId": "56490170-5353-4A9D-8F5F-14B0BBB91B63", + "itemType": "" }, - "instanceId": "d79dbaeb-cc09-487b-be65-93b0bc5eb567" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false + "loadoutId": "53925c2d-5b68-48f8-88de-2d6999f9f3c6", + "outfitData": { + "alterationData": [ + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_F_Default", + "itemInstanceId": "AC5035E3-6572-484B-A5D8-1FFCC1F367F1", + "itemType": "" }, - "instanceId": "a83f4964-0532-4c9d-8219-47e9d557c9dc" + "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "d14f3756-6b79-4c13-bb86-8ec7be42aed2" + "attackerWeaponLoadout": { + "loadoutId": "5152439b-2aaf-484c-9de0-0fdea92d99c9", + "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - }, - { - "channelId": "FiringUpgrades", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "VFX", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "KillCeremony", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "a134a5c9-1d26-4b57-bba3-5f49b541a28d" + "banner": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreBannerItemDef:BannerID_Default", + "itemInstanceId": "4F30362E-0F4A-4389-8C43-4C588924CA09", + "itemType": "" }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "039108c5-c339-4dc4-84fc-c35d58f15a25" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "cecddd31-4eba-42bc-8910-e6971c010eab" - }, - { - "catalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_02", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "385aaac6-c736-4a2d-9ce0-e16e0a1cc736" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "9210c48c-1bf8-4234-b3b4-73f2675e92d6" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "3b231ab1-765d-4b93-9d70-0ac8650c0cf3" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "24bc0e97-808c-4361-9399-507cc6e608fe" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "85e6b34c-34e0-4e39-b6fa-57445146f6a8" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "06db77d5-849a-4509-895c-b24f28b800c4" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "9be5418b-b6d7-41e3-9b33-e28bb1545b96" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - }, - { - "channelId": "FiringUpgrades", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "VFX", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "KillCeremony", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "d10e5a1f-a703-4c0a-a415-4cbef6ed94ba" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "bf99937b-00b8-4178-9e0a-02b3a7ce3050" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "cc81fd53-27e9-424c-9bc3-ec74244e98b2" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "7f9b8d30-c0f2-4c24-985e-8bcb74e6c4fb" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - }, - { - "channelId": "FiringUpgrades", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "VFX", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "KillCeremony", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "Color", - "ownedAlterations": [ - "Option1" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "c5f0d728-563d-4d10-be1c-aee45e98c7bd" - }, - { - "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", - "ext": { - "instancedCustomizationData": { - "instancedAlterationChannels": [ - { - "channelId": "Mesh", - "ownedAlterations": [ - "On" - ] - }, - { - "channelId": "VFX", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "KillCeremony", - "ownedAlterations": [ - "Off" - ] - }, - { - "channelId": "Trail", - "ownedAlterations": [ - "On" - ] - } - ] - }, - "reserved": 0.0, - "viewed": false - }, - "instanceId": "930216bb-2e63-488f-9faf-25b7eaa334ca" - } - ], - "playerData": { - "attackerOutfitLoadout": { - "faceAccessoryData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "", - "itemInstanceId": "", - "itemType": "" - }, - "faceStyleData": { - "alterationData": [ - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_001", - "itemInstanceId": "EEC7F364-69CD-47CD-92D0-E5062769D28C", - "itemType": "" - }, - "hairData": { - "alterationData": [ - { - "alterationId": "Option4", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", - "itemInstanceId": "D2CEDF72-1091-4824-A936-5D97F83A0F8E", - "itemType": "" - }, - "headData": { - "alterationData": [ - { - "alterationId": "Option9", - "channelId": "SkinTone" - }, - { - "alterationId": "Option2", - "channelId": "LipColor" - }, - { - "alterationId": "Option6", - "channelId": "EyeColor" - }, - { - "alterationId": "Option2", - "channelId": "EyebrowColor" - }, - { - "alterationId": "Option3", - "channelId": "Voice" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", - "itemInstanceId": "5779CD78-07E5-4216-8670-C4E41F506CDD", - "itemType": "" - }, - "loadoutId": "0ade2a13-7977-43bf-ab50-9c2122987544", - "outfitData": { - "alterationData": [ - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_CyberLord", - "itemInstanceId": "919E2723-A15B-40FA-A58F-C73EED03FEB7", - "itemType": "" - }, - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d" - }, - "attackerWeaponLoadout": { - "autoPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", - "itemInstanceId": "1B98EF70-CEC6-455F-9E13-A4232176AA79", - "itemType": "" - }, - "autoShotgunData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", - "itemInstanceId": "24BC0E97-808C-4361-9399-507CC6E608FE", - "itemType": "" - }, - "boltactionSniperData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", - "itemInstanceId": "D10E5A1F-A703-4C0A-A415-4CBEF6ED94BA", - "itemType": "" - }, - "burstArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", - "itemInstanceId": "039108C5-C339-4DC4-84FC-C35D58F15A25", - "itemType": "" - }, - "heavyArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_02", - "attachmentItemInstanceId": "385AAAC6-C736-4A2D-9CE0-E16E0A1CC736", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", - "itemInstanceId": "CECDDD31-4EBA-42BC-8910-E6971C010EAB", - "itemType": "" - }, - "heavyShotgunData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", - "itemInstanceId": "85E6B34C-34E0-4E39-B6FA-57445146F6A8", - "itemType": "" - }, - "highcalMgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", - "itemInstanceId": "06DB77D5-849A-4509-895C-B24F28B800C4", - "itemType": "" - }, - "highcalPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", - "itemInstanceId": "A83F4964-0532-4C9D-8219-47E9D557C9DC", - "itemType": "" - }, - "loadoutId": "00f12c51-62e8-4320-b2f2-a07e87d52662", - "meleeData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "On", - "channelId": "Trail" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", - "itemInstanceId": "930216BB-2E63-488F-9FAF-25B7EAA334CA", - "itemType": "" - }, - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", - "rapidfireMgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", - "itemInstanceId": "9BE5418B-B6D7-41E3-9B33-E28BB1545B96", - "itemType": "" - }, - "rapidfireSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", - "itemInstanceId": "CC81FD53-27E9-424C-9BC3-EC74244E98B2", - "itemType": "" - }, - "semiautoArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", - "itemInstanceId": "D14F3756-6B79-4C13-BB86-8EC7BE42AED2", - "itemType": "" - }, - "semiautoPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", - "itemInstanceId": "46A9620F-3129-49ED-9C9A-10427EC0043A", - "itemType": "" - }, - "semiautoSniperData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", - "itemInstanceId": "BF99937B-00B8-4178-9E0A-02B3A7CE3050", - "itemType": "" - }, - "standardArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", - "itemInstanceId": "A134A5C9-1D26-4B57-BBA3-5F49B541A28D", - "itemType": "" - }, - "suppressedArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", - "itemInstanceId": "9210C48C-1BF8-4234-B3B4-73F2675E92D6", - "itemType": "" - }, - "suppressedPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", - "itemInstanceId": "D79DBAEB-CC09-487B-BE65-93B0BC5EB567", - "itemType": "" - }, - "suppressedSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", - "itemInstanceId": "C5F0D728-563D-4D10-BE1C-AEE45E98C7BD", - "itemType": "" - }, - "tacticalArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", - "itemInstanceId": "3B231AB1-765D-4B93-9D70-0AC8650C0CF3", - "itemType": "" - }, - "tacticalSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", - "itemInstanceId": "7F9B8D30-C0F2-4C24-985E-8BCB74E6C4FB", - "itemType": "" - } - }, - "banner": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreBannerItemDef:BannerID_Track_Kit01_District_01", - "itemInstanceId": "FF01EF00-D0F2-4583-B4A4-CC56A606B404", - "itemType": "" - }, - "defenderOutfitLoadout": { - "faceAccessoryData": { - "alterationData": [], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HAccessoryID_M_Prince", - "itemInstanceId": "325CA087-46AA-4127-93B1-40CA17605D32", - "itemType": "" + "defenderOutfitLoadout": { + "faceAccessoryData": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "", + "itemInstanceId": "", + "itemType": "" }, "faceStyleData": { - "alterationData": [ - { - "alterationId": "Option1", - "channelId": "Color" - } - ], + "alterationData": [], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_002", - "itemInstanceId": "45FE4B4A-ACB5-42E4-ADC6-F07663C907BF", + "itemCatalogId": "", + "itemInstanceId": "", "itemType": "" }, "hairData": { "alterationData": [ { - "alterationId": "Option2", + "alterationId": "Option5", "channelId": "Color" } ], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_CyberLord_01", - "itemInstanceId": "4A47CF85-DA23-4D36-82CB-2AE96757490F", + "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", + "itemInstanceId": "A98F9664-2387-4931-B6A2-8F0E7983B856", "itemType": "" }, "headData": { @@ -1228,7 +336,7 @@ "channelId": "EyeColor" }, { - "alterationId": "Option2", + "alterationId": "Option4", "channelId": "EyebrowColor" }, { @@ -1238,11 +346,11 @@ ], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_003_B", - "itemInstanceId": "A60CF16B-306D-4874-BD0C-810D664688AC", + "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", + "itemInstanceId": "711AF3EC-F1E0-4C0B-AE88-4BD2C51EB23F", "itemType": "" }, - "loadoutId": "6299fca0-fb5b-4d69-8932-9495ee0521ed", + "loadoutId": "ebb85cdd-827a-4884-9eb5-0b584d43be19", "outfitData": { "alterationData": [ { @@ -1252,355 +360,19 @@ ], "attachmentItemCatalogId": "", "attachmentItemInstanceId": "", - "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Prince", - "itemInstanceId": "4D34A455-F92C-414C-94B0-2E5C65BC338B", + "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Default", + "itemInstanceId": "40268935-F961-4783-934F-C46B3E7DC1DB", "itemType": "" }, - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d" + "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" }, "defenderWeaponLoadout": { - "autoPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", - "itemInstanceId": "1B98EF70-CEC6-455F-9E13-A4232176AA79", - "itemType": "" - }, - "autoShotgunData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", - "itemInstanceId": "24BC0E97-808C-4361-9399-507CC6E608FE", - "itemType": "" - }, - "boltactionSniperData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", - "itemInstanceId": "D10E5A1F-A703-4C0A-A415-4CBEF6ED94BA", - "itemType": "" - }, - "burstArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", - "itemInstanceId": "039108C5-C339-4DC4-84FC-C35D58F15A25", - "itemType": "" - }, - "heavyArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_02", - "attachmentItemInstanceId": "385AAAC6-C736-4A2D-9CE0-E16E0A1CC736", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", - "itemInstanceId": "CECDDD31-4EBA-42BC-8910-E6971C010EAB", - "itemType": "" - }, - "heavyShotgunData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", - "itemInstanceId": "85E6B34C-34E0-4E39-B6FA-57445146F6A8", - "itemType": "" - }, - "highcalMgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", - "itemInstanceId": "06DB77D5-849A-4509-895C-B24F28B800C4", - "itemType": "" - }, - "highcalPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", - "itemInstanceId": "A83F4964-0532-4C9D-8219-47E9D557C9DC", - "itemType": "" - }, - "loadoutId": "17a07016-4264-4511-9f12-8b2665082ecb", - "meleeData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "On", - "channelId": "Trail" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", - "itemInstanceId": "930216BB-2E63-488F-9FAF-25B7EAA334CA", - "itemType": "" - }, - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", - "rapidfireMgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", - "itemInstanceId": "9BE5418B-B6D7-41E3-9B33-E28BB1545B96", - "itemType": "" - }, - "rapidfireSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", - "itemInstanceId": "CC81FD53-27E9-424C-9BC3-EC74244E98B2", - "itemType": "" - }, - "semiautoArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", - "itemInstanceId": "D14F3756-6B79-4C13-BB86-8EC7BE42AED2", - "itemType": "" - }, - "semiautoPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", - "itemInstanceId": "46A9620F-3129-49ED-9C9A-10427EC0043A", - "itemType": "" - }, - "semiautoSniperData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", - "itemInstanceId": "BF99937B-00B8-4178-9E0A-02B3A7CE3050", - "itemType": "" - }, - "standardArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", - "itemInstanceId": "A134A5C9-1D26-4B57-BBA3-5F49B541A28D", - "itemType": "" - }, - "suppressedArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", - "itemInstanceId": "9210C48C-1BF8-4234-B3B4-73F2675E92D6", - "itemType": "" - }, - "suppressedPistolData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", - "itemInstanceId": "D79DBAEB-CC09-487B-BE65-93B0BC5EB567", - "itemType": "" - }, - "suppressedSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - }, - { - "alterationId": "Off", - "channelId": "FiringUpgrades" - }, - { - "alterationId": "Off", - "channelId": "VFX" - }, - { - "alterationId": "Off", - "channelId": "KillCeremony" - }, - { - "alterationId": "Option1", - "channelId": "Color" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", - "itemInstanceId": "C5F0D728-563D-4D10-BE1C-AEE45E98C7BD", - "itemType": "" - }, - "tacticalArData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", - "itemInstanceId": "3B231AB1-765D-4B93-9D70-0AC8650C0CF3", - "itemType": "" - }, - "tacticalSmgData": { - "alterationData": [ - { - "alterationId": "On", - "channelId": "Mesh" - } - ], - "attachmentItemCatalogId": "", - "attachmentItemInstanceId": "", - "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", - "itemInstanceId": "7F9B8D30-C0F2-4C24-985E-8BCB74E6C4FB", - "itemType": "" - } + "loadoutId": "ff09ddea-2594-4bea-abd1-d6f255ce3f57", + "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab" }, "matchmakingData": { - "casualMatchesPlayedCount": 16.0, - "casualMatchesWonCount": 7.0, + "casualMatchesPlayedCount": 0.0, + "casualMatchesWonCount": 0.0, "currentSoloRank": 0.0, "highestTeamRank": 0.0, "matchmakingFlags": 1.0, @@ -1612,27 +384,27 @@ } }, "preferredTeam": "TEAM0", - "rankedModeUnlocked": true, + "rankedModeUnlocked": false, "region": "", - "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561197961806213\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", + "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561199782684391\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", "version": "171268" }, "isLeader": true, "isReady": true, - "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", - "socialId": "3213444f-7b0f-4168-8220-2013159acc50" + "playerId": "250d6bea-4f0d-4ded-96cc-6e40ec80afab", + "socialId": "2c4eef11-d53e-48c2-a80f-a3f8234e61f7" } ], "preferredGameServerZones": [ "uscentral-1", - "uswest-2", "uscentral-2", "uswest-1", + "uswest-2", "useast-1" ], - "version": "5" + "version": "2" } }, - "rpcType": "PartyRpc.EnterMatchmakingV1Request", - "testAge": 1740506368.83 + "rpcType": "PartyRpc.SetReadyStateV1Request", + "testAge": 1740507431.329 } \ No newline at end of file diff --git a/resources/testrequests/sequenced/party/6.json b/resources/testrequests/sequenced/party/6.json new file mode 100644 index 00000000..ee97cadb --- /dev/null +++ b/resources/testrequests/sequenced/party/6.json @@ -0,0 +1,1638 @@ +{ + "requestBody": { + "partyId": "${responses[1]party.partyId}" + }, + "responsePayload": { + "party": { + "extBroadcastParty": { + "chatId": "", + "crossPlayPreference": { + "platform": "CROSS_PLAY_PLATFORM_PC" + }, + "custom": { + "map": "Greenbelt_P", + "mode": "Custom", + "overtimeType": "disabled", + "teamAssignments": [ + { + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", + "team": 0.0 + } + ] + }, + "hasAcceptableRegion": true, + "lobbyMode": "custom", + "pool": "custom", + "profile": "", + "region": "", + "removePlayers": [], + "tag": "", + "tokens": [], + "useTeamMmr": false, + "version": "171268" + }, + "extPrivatePlayer": {}, + "inviteCode": "CDCXZ9", + "partyId": "4e398837-d121-4f37-b5fd-50d291ba45ab", + "partyMembers": [ + { + "displayName": { + "discriminator": "0817", + "displayName": "LIMIT" + }, + "ext": { + "limitedInstancedInventory": [ + { + "catalogId": "SpectreBannerItemDef:BannerID_Track_Kit01_District_01", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "ff01ef00-d0f2-4583-b4a4-cc56a606b404" + }, + { + "catalogId": "CharacterCosmeticItemOutfit:OutfitID_CyberLord", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "919e2723-a15b-40fa-a58f-c73eed03feb7" + }, + { + "catalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "SkinTone", + "ownedAlterations": [ + "Option9" + ] + }, + { + "channelId": "LipColor", + "ownedAlterations": [ + "Option2" + ] + }, + { + "channelId": "EyeColor", + "ownedAlterations": [ + "Option6" + ] + }, + { + "channelId": "EyebrowColor", + "ownedAlterations": [ + "Option2" + ] + }, + { + "channelId": "Voice", + "ownedAlterations": [ + "Option3" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "5779cd78-07e5-4216-8670-c4e41f506cdd" + }, + { + "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Color", + "ownedAlterations": [ + "Option4" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "d2cedf72-1091-4824-a936-5d97f83a0f8e" + }, + { + "catalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_001", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "eec7f364-69cd-47cd-92d0-e5062769d28c" + }, + { + "catalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Prince", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "4d34a455-f92c-414c-94b0-2e5c65bc338b" + }, + { + "catalogId": "CharacterCosmeticItemHead:HeadID_Face_003_B", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "SkinTone", + "ownedAlterations": [ + "Option8" + ] + }, + { + "channelId": "LipColor", + "ownedAlterations": [ + "Option2" + ] + }, + { + "channelId": "EyeColor", + "ownedAlterations": [ + "Option6" + ] + }, + { + "channelId": "EyebrowColor", + "ownedAlterations": [ + "Option2" + ] + }, + { + "channelId": "Voice", + "ownedAlterations": [ + "Option3" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "a60cf16b-306d-4874-bd0c-810d664688ac" + }, + { + "catalogId": "CharacterCosmeticItemSimple:HairID_Hair_CyberLord_01", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Color", + "ownedAlterations": [ + "Option2" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "4a47cf85-da23-4d36-82cb-2ae96757490f" + }, + { + "catalogId": "CharacterCosmeticItemSimple:HAccessoryID_M_Prince", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "325ca087-46aa-4127-93b1-40ca17605d32" + }, + { + "catalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_002", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "45fe4b4a-acb5-42e4-adc6-f07663c907bf" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "46a9620f-3129-49ed-9c9a-10427ec0043a" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + }, + { + "channelId": "FiringUpgrades", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "VFX", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "KillCeremony", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "1b98ef70-cec6-455f-9e13-a4232176aa79" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "d79dbaeb-cc09-487b-be65-93b0bc5eb567" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "a83f4964-0532-4c9d-8219-47e9d557c9dc" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "d14f3756-6b79-4c13-bb86-8ec7be42aed2" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + }, + { + "channelId": "FiringUpgrades", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "VFX", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "KillCeremony", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "a134a5c9-1d26-4b57-bba3-5f49b541a28d" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "039108c5-c339-4dc4-84fc-c35d58f15a25" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "cecddd31-4eba-42bc-8910-e6971c010eab" + }, + { + "catalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_02", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "385aaac6-c736-4a2d-9ce0-e16e0a1cc736" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "9210c48c-1bf8-4234-b3b4-73f2675e92d6" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "3b231ab1-765d-4b93-9d70-0ac8650c0cf3" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "24bc0e97-808c-4361-9399-507cc6e608fe" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "85e6b34c-34e0-4e39-b6fa-57445146f6a8" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "06db77d5-849a-4509-895c-b24f28b800c4" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "9be5418b-b6d7-41e3-9b33-e28bb1545b96" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + }, + { + "channelId": "FiringUpgrades", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "VFX", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "KillCeremony", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "d10e5a1f-a703-4c0a-a415-4cbef6ed94ba" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "bf99937b-00b8-4178-9e0a-02b3a7ce3050" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "cc81fd53-27e9-424c-9bc3-ec74244e98b2" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "7f9b8d30-c0f2-4c24-985e-8bcb74e6c4fb" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + }, + { + "channelId": "FiringUpgrades", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "VFX", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "KillCeremony", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "Color", + "ownedAlterations": [ + "Option1" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "c5f0d728-563d-4d10-be1c-aee45e98c7bd" + }, + { + "catalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", + "ext": { + "instancedCustomizationData": { + "instancedAlterationChannels": [ + { + "channelId": "Mesh", + "ownedAlterations": [ + "On" + ] + }, + { + "channelId": "VFX", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "KillCeremony", + "ownedAlterations": [ + "Off" + ] + }, + { + "channelId": "Trail", + "ownedAlterations": [ + "On" + ] + } + ] + }, + "reserved": 0.0, + "viewed": false + }, + "instanceId": "930216bb-2e63-488f-9faf-25b7eaa334ca" + } + ], + "playerData": { + "attackerOutfitLoadout": { + "faceAccessoryData": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "", + "itemInstanceId": "", + "itemType": "" + }, + "faceStyleData": { + "alterationData": [ + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_001", + "itemInstanceId": "EEC7F364-69CD-47CD-92D0-E5062769D28C", + "itemType": "" + }, + "hairData": { + "alterationData": [ + { + "alterationId": "Option4", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_0020", + "itemInstanceId": "D2CEDF72-1091-4824-A936-5D97F83A0F8E", + "itemType": "" + }, + "headData": { + "alterationData": [ + { + "alterationId": "Option9", + "channelId": "SkinTone" + }, + { + "alterationId": "Option2", + "channelId": "LipColor" + }, + { + "alterationId": "Option6", + "channelId": "EyeColor" + }, + { + "alterationId": "Option2", + "channelId": "EyebrowColor" + }, + { + "alterationId": "Option3", + "channelId": "Voice" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_002_B", + "itemInstanceId": "5779CD78-07E5-4216-8670-C4E41F506CDD", + "itemType": "" + }, + "loadoutId": "0ade2a13-7977-43bf-ab50-9c2122987544", + "outfitData": { + "alterationData": [ + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_CyberLord", + "itemInstanceId": "919E2723-A15B-40FA-A58F-C73EED03FEB7", + "itemType": "" + }, + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d" + }, + "attackerWeaponLoadout": { + "autoPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", + "itemInstanceId": "1B98EF70-CEC6-455F-9E13-A4232176AA79", + "itemType": "" + }, + "autoShotgunData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", + "itemInstanceId": "24BC0E97-808C-4361-9399-507CC6E608FE", + "itemType": "" + }, + "boltactionSniperData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", + "itemInstanceId": "D10E5A1F-A703-4C0A-A415-4CBEF6ED94BA", + "itemType": "" + }, + "burstArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", + "itemInstanceId": "039108C5-C339-4DC4-84FC-C35D58F15A25", + "itemType": "" + }, + "heavyArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_02", + "attachmentItemInstanceId": "385AAAC6-C736-4A2D-9CE0-E16E0A1CC736", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", + "itemInstanceId": "CECDDD31-4EBA-42BC-8910-E6971C010EAB", + "itemType": "" + }, + "heavyShotgunData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", + "itemInstanceId": "85E6B34C-34E0-4E39-B6FA-57445146F6A8", + "itemType": "" + }, + "highcalMgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", + "itemInstanceId": "06DB77D5-849A-4509-895C-B24F28B800C4", + "itemType": "" + }, + "highcalPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", + "itemInstanceId": "A83F4964-0532-4C9D-8219-47E9D557C9DC", + "itemType": "" + }, + "loadoutId": "00f12c51-62e8-4320-b2f2-a07e87d52662", + "meleeData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "On", + "channelId": "Trail" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", + "itemInstanceId": "930216BB-2E63-488F-9FAF-25B7EAA334CA", + "itemType": "" + }, + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", + "rapidfireMgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", + "itemInstanceId": "9BE5418B-B6D7-41E3-9B33-E28BB1545B96", + "itemType": "" + }, + "rapidfireSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", + "itemInstanceId": "CC81FD53-27E9-424C-9BC3-EC74244E98B2", + "itemType": "" + }, + "semiautoArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", + "itemInstanceId": "D14F3756-6B79-4C13-BB86-8EC7BE42AED2", + "itemType": "" + }, + "semiautoPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", + "itemInstanceId": "46A9620F-3129-49ED-9C9A-10427EC0043A", + "itemType": "" + }, + "semiautoSniperData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", + "itemInstanceId": "BF99937B-00B8-4178-9E0A-02B3A7CE3050", + "itemType": "" + }, + "standardArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", + "itemInstanceId": "A134A5C9-1D26-4B57-BBA3-5F49B541A28D", + "itemType": "" + }, + "suppressedArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", + "itemInstanceId": "9210C48C-1BF8-4234-B3B4-73F2675E92D6", + "itemType": "" + }, + "suppressedPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", + "itemInstanceId": "D79DBAEB-CC09-487B-BE65-93B0BC5EB567", + "itemType": "" + }, + "suppressedSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", + "itemInstanceId": "C5F0D728-563D-4D10-BE1C-AEE45E98C7BD", + "itemType": "" + }, + "tacticalArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", + "itemInstanceId": "3B231AB1-765D-4B93-9D70-0AC8650C0CF3", + "itemType": "" + }, + "tacticalSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", + "itemInstanceId": "7F9B8D30-C0F2-4C24-985E-8BCB74E6C4FB", + "itemType": "" + } + }, + "banner": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreBannerItemDef:BannerID_Track_Kit01_District_01", + "itemInstanceId": "FF01EF00-D0F2-4583-B4A4-CC56A606B404", + "itemType": "" + }, + "defenderOutfitLoadout": { + "faceAccessoryData": { + "alterationData": [], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HAccessoryID_M_Prince", + "itemInstanceId": "325CA087-46AA-4127-93B1-40CA17605D32", + "itemType": "" + }, + "faceStyleData": { + "alterationData": [ + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HStylingID_DefaultStyling_002", + "itemInstanceId": "45FE4B4A-ACB5-42E4-ADC6-F07663C907BF", + "itemType": "" + }, + "hairData": { + "alterationData": [ + { + "alterationId": "Option2", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemSimple:HairID_Hair_CyberLord_01", + "itemInstanceId": "4A47CF85-DA23-4D36-82CB-2AE96757490F", + "itemType": "" + }, + "headData": { + "alterationData": [ + { + "alterationId": "Option8", + "channelId": "SkinTone" + }, + { + "alterationId": "Option2", + "channelId": "LipColor" + }, + { + "alterationId": "Option6", + "channelId": "EyeColor" + }, + { + "alterationId": "Option2", + "channelId": "EyebrowColor" + }, + { + "alterationId": "Option3", + "channelId": "Voice" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemHead:HeadID_Face_003_B", + "itemInstanceId": "A60CF16B-306D-4874-BD0C-810D664688AC", + "itemType": "" + }, + "loadoutId": "6299fca0-fb5b-4d69-8932-9495ee0521ed", + "outfitData": { + "alterationData": [ + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "CharacterCosmeticItemOutfit:OutfitID_M_Prince", + "itemInstanceId": "4D34A455-F92C-414C-94B0-2E5C65BC338B", + "itemType": "" + }, + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d" + }, + "defenderWeaponLoadout": { + "autoPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Auto_Angel", + "itemInstanceId": "1B98EF70-CEC6-455F-9E13-A4232176AA79", + "itemType": "" + }, + "autoShotgunData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Auto_Default", + "itemInstanceId": "24BC0E97-808C-4361-9399-507CC6E608FE", + "itemType": "" + }, + "boltactionSniperData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_BoltAction_Angel", + "itemInstanceId": "D10E5A1F-A703-4C0A-A415-4CBEF6ED94BA", + "itemType": "" + }, + "burstArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Burst_Default", + "itemInstanceId": "039108C5-C339-4DC4-84FC-C35D58F15A25", + "itemType": "" + }, + "heavyArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "SpectreWeaponCharmItemDef:CharmID_Misc_Community_02", + "attachmentItemInstanceId": "385AAAC6-C736-4A2D-9CE0-E16E0A1CC736", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Heavy_Communa", + "itemInstanceId": "CECDDD31-4EBA-42BC-8910-E6971C010EAB", + "itemType": "" + }, + "heavyShotgunData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Shotgun_Heavy_Default", + "itemInstanceId": "85E6B34C-34E0-4E39-B6FA-57445146F6A8", + "itemType": "" + }, + "highcalMgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_HighCal_Default", + "itemInstanceId": "06DB77D5-849A-4509-895C-B24F28B800C4", + "itemType": "" + }, + "highcalPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_HighCal_Default", + "itemInstanceId": "A83F4964-0532-4C9D-8219-47E9D557C9DC", + "itemType": "" + }, + "loadoutId": "17a07016-4264-4511-9f12-8b2665082ecb", + "meleeData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "On", + "channelId": "Trail" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Knife_Cyberlord", + "itemInstanceId": "930216BB-2E63-488F-9FAF-25B7EAA334CA", + "itemType": "" + }, + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", + "rapidfireMgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_MG_RapidFire_Default", + "itemInstanceId": "9BE5418B-B6D7-41E3-9B33-E28BB1545B96", + "itemType": "" + }, + "rapidfireSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_RapidFire_Default", + "itemInstanceId": "CC81FD53-27E9-424C-9BC3-EC74244E98B2", + "itemType": "" + }, + "semiautoArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_SemiAuto_Default", + "itemInstanceId": "D14F3756-6B79-4C13-BB86-8EC7BE42AED2", + "itemType": "" + }, + "semiautoPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_SemiAuto_Default", + "itemInstanceId": "46A9620F-3129-49ED-9C9A-10427EC0043A", + "itemType": "" + }, + "semiautoSniperData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Sniper_SemiAuto_Default", + "itemInstanceId": "BF99937B-00B8-4178-9E0A-02B3A7CE3050", + "itemType": "" + }, + "standardArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Standard_Angel", + "itemInstanceId": "A134A5C9-1D26-4B57-BBA3-5F49B541A28D", + "itemType": "" + }, + "suppressedArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Suppressed_Default", + "itemInstanceId": "9210C48C-1BF8-4234-B3B4-73F2675E92D6", + "itemType": "" + }, + "suppressedPistolData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_Pistol_Suppressed_Default", + "itemInstanceId": "D79DBAEB-CC09-487B-BE65-93B0BC5EB567", + "itemType": "" + }, + "suppressedSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + }, + { + "alterationId": "Off", + "channelId": "FiringUpgrades" + }, + { + "alterationId": "Off", + "channelId": "VFX" + }, + { + "alterationId": "Off", + "channelId": "KillCeremony" + }, + { + "alterationId": "Option1", + "channelId": "Color" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Suppressed_Angel", + "itemInstanceId": "C5F0D728-563D-4D10-BE1C-AEE45E98C7BD", + "itemType": "" + }, + "tacticalArData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_AR_Tactical_Default", + "itemInstanceId": "3B231AB1-765D-4B93-9D70-0AC8650C0CF3", + "itemType": "" + }, + "tacticalSmgData": { + "alterationData": [ + { + "alterationId": "On", + "channelId": "Mesh" + } + ], + "attachmentItemCatalogId": "", + "attachmentItemInstanceId": "", + "itemCatalogId": "SpectreWeaponCosmeticItemDef:WeaponID_SMG_Tactical_Default", + "itemInstanceId": "7F9B8D30-C0F2-4C24-985E-8BCB74E6C4FB", + "itemType": "" + } + }, + "matchmakingData": { + "casualMatchesPlayedCount": 16.0, + "casualMatchesWonCount": 7.0, + "currentSoloRank": 0.0, + "highestTeamRank": 0.0, + "matchmakingFlags": 1.0, + "primaryGeographicRegion": "", + "priorityMatchmakingUntil": "0", + "restrictMatchmakingUntil": "0", + "secondaryGeographicRegion": "", + "soloRankPoints": 0.0 + } + }, + "preferredTeam": "TEAM0", + "rankedModeUnlocked": true, + "region": "", + "sharedClientData": "{\r\n\t\"accountIdProvider\": \"STEAM\",\r\n\t\"platformName\": \"STEAM\",\r\n\t\"crossPlayPlatformKind\": \"CROSS_PLAY_PLATFORM_PC\",\r\n\t\"currentProviderAccountId\": \"76561197961806213\",\r\n\t\"crewId\": \"\",\r\n\t\"gamesRemainingUntilCrewJoin\": 0,\r\n\t\"battlePassLevel\": 0\r\n}", + "version": "171268" + }, + "isLeader": true, + "isReady": true, + "playerId": "2b940443-58da-45b7-b704-94459a2f9a4d", + "socialId": "3213444f-7b0f-4168-8220-2013159acc50" + } + ], + "preferredGameServerZones": [ + "uscentral-1", + "uswest-2", + "uscentral-2", + "uswest-1", + "useast-1" + ], + "version": "5" + } + }, + "rpcType": "PartyRpc.EnterMatchmakingV1Request", + "testAge": 1740506368.83 +} \ No newline at end of file diff --git a/resources/testrequests/sequenced/party/0.json b/resources/testrequests/sequenced/party/inactive0.json similarity index 100% rename from resources/testrequests/sequenced/party/0.json rename to resources/testrequests/sequenced/party/inactive0.json diff --git a/resources/testrequests/wsSkipTests.txt b/resources/testrequests/wsSkipTests.txt new file mode 100644 index 00000000..e9a1c6ae --- /dev/null +++ b/resources/testrequests/wsSkipTests.txt @@ -0,0 +1,18 @@ +AccountRpc.GetDisplayNameForPragmaPlayerIdV1Request +MtnCrewServiceRpc.GetCrewLegacyDataV1Request +MtnCrewServiceRpc.GetCrewDashboardDataV1Request +MtnInboxServiceRpc.GetMessagesClientV1Request +MtnCrewServiceRpc.GetDivisionRulesV1Request +MtnCrewServiceRpc.GetCrewChatRoomV1Request +MtnChatServiceRpc.LoginToChatV2Request +MtnCrewServiceRpc.GetCrewEndOfMatchDataV2Request +GameInstanceRpc.GetHostConnectionDetailsV1Request +MtnCrewServiceRpc.GetCrewV1Request +MtnCrewServiceRpc.GetDivisionByCrewIdV1Request +MtnCrewServiceRpc.GetCrewLeaderboardDataV1Request +MtnCrewServiceRpc.GetCrewByPlayerIdV1Request +MtnCrewServiceRpc.CanJoinCrewV1Request +MtnAnalyticsNodeServiceRpc.RecordClientAnalyticsEventV1Request +MtnCrewServiceRpc.GetCrewRosterV2Request +MtnInboxServiceRpc.MarkMessageAsReadClientV1Request +MtnPlayerDataServiceRpc.GetMatchmakingDataClientV1Request \ No newline at end of file diff --git a/tests/Model/BackendEnvironment.h b/tests/Model/BackendEnvironment.h index b4870e09..8effac10 100644 --- a/tests/Model/BackendEnvironment.h +++ b/tests/Model/BackendEnvironment.h @@ -1,11 +1,12 @@ #pragma once #include #include +#define WIN32_LEAN_AND_MEAN // won't compile without this :skull: +#include class BackendEnvironment : public ::testing::Environment { - private: - std::unique_ptr server; - +private: + DWORD backendPID{}; public: void SetUp() override; diff --git a/tests/Model/HTTPRequestTest.h b/tests/Model/HTTPRequestTest.h index 250fba70..3d9238af 100644 --- a/tests/Model/HTTPRequestTest.h +++ b/tests/Model/HTTPRequestTest.h @@ -5,6 +5,8 @@ namespace fs = std::filesystem; using json = nlohmann::ordered_json; +const std::vector& GetSkipRoutes(); + class HTTPRequestTest : public RequestTest { }; diff --git a/tests/Model/WebsocketRequestTest.h b/tests/Model/WebsocketRequestTest.h index 6c93d43c..ce0507b7 100644 --- a/tests/Model/WebsocketRequestTest.h +++ b/tests/Model/WebsocketRequestTest.h @@ -1,10 +1,13 @@ #pragma once #include +#include #include namespace fs = std::filesystem; using json = nlohmann::ordered_json; +std::vector& GetSkipRpcTypes(); + class WebsocketRequestTest : public RequestTest { }; diff --git a/tests/implementation/BackendEnvironment.cpp b/tests/implementation/BackendEnvironment.cpp index e214e839..3866e6e2 100644 --- a/tests/implementation/BackendEnvironment.cpp +++ b/tests/implementation/BackendEnvironment.cpp @@ -4,9 +4,36 @@ #include #include #include +#define WIN32_LEAN_AND_MEAN // why is this a thing. it literally won't compile w/o it +#include namespace fs = std::filesystem; +static DWORD RunBackendWindows() { + STARTUPINFOA si = {sizeof(si)}; + PROCESS_INFORMATION pi = {}; + std::filesystem::path exePath = ResourcesUtilities::GetCurrentExecutablePath().parent_path().parent_path() / + "pragmabackend.exe"; + BOOL ok = CreateProcessA(exePath.string().c_str(), + (LPSTR)"8081 8082 8083", + nullptr, + nullptr, + TRUE, + CREATE_NEW_PROCESS_GROUP, + nullptr, + nullptr, + &si, + &pi); + FreeConsole(); + FreeConsole(); + for (int i = 0; i < 50; i++) { + if (AttachConsole(pi.dwProcessId) != 0) break; + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + SetConsoleCtrlHandler(nullptr, TRUE); + return pi.dwProcessId; +} + void BackendEnvironment::SetUp() { #if defined(_WIN32) std::filesystem::path exePath = ResourcesUtilities::GetCurrentExecutablePath().parent_path().parent_path() / @@ -18,15 +45,7 @@ void BackendEnvironment::SetUp() { std::system("pkill -9 pragmabackend"); // NOLINT #endif std::filesystem::remove(PersistenceUtilities::GetSavePath() / "playerdata.sqlite"); - server = std::make_unique( - std::string(std::filesystem::absolute(exePath).string() + " 8081 8082 8083"), - ResourcesUtilities::GetCurrentExecutablePath().parent_path().parent_path().string(), - [](const char* bytes, size_t n) { - std::cout << std::string(bytes, n); - }, - [](const char* bytes, size_t n) { - std::cerr << std::string(bytes, n); - }); + backendPID = RunBackendWindows(); while (true) { if (fs::exists(ResourcesUtilities::GetCurrentExecutablePath().parent_path().parent_path() / "server.lock")) { break; @@ -35,11 +54,12 @@ void BackendEnvironment::SetUp() { } void BackendEnvironment::TearDown() { - if (server) { - server->kill(); - std::this_thread::sleep_for(std::chrono::seconds(1)); // wait for windows to release file handle - server.reset(); - } + GenerateConsoleCtrlEvent(CTRL_C_EVENT, NULL); + SetConsoleCtrlHandler(nullptr, FALSE); + HANDLE h = OpenProcess(SYNCHRONIZE, FALSE, backendPID); + WaitForSingleObject(h, INFINITE); + CloseHandle(h); + std::this_thread::sleep_for(std::chrono::seconds(1)); // wait for windows to release file handle std::filesystem::remove(PersistenceUtilities::GetSavePath() / "playerdata.sqlite"); std::filesystem::remove(ResourcesUtilities::GetCurrentExecutablePath().parent_path().parent_path() / "server.lock"); } \ No newline at end of file diff --git a/tests/implementation/HTTPRequestTest.cpp b/tests/implementation/HTTPRequestTest.cpp index 092a526a..c3945ed4 100644 --- a/tests/implementation/HTTPRequestTest.cpp +++ b/tests/implementation/HTTPRequestTest.cpp @@ -1,3 +1,5 @@ +#include "ResourcesUtilities.h" + #include #include #include @@ -13,7 +15,23 @@ void RunHTTPTest(const fs::path& testPath, json& outResponse) { return RunHTTPTest(testJson, outResponse); } +const std::vector& GetSkipRoutes() { + static std::vector skipRoutes = []() { + std::vector skipRoutes; + std::ifstream skipRoutesFile(ResourcesUtilities::GetResourcesFolder() / "testrequests" / "httpSkipTests.txt"); + std::string line; + while (std::getline(skipRoutesFile, line)) { + skipRoutes.push_back(line); + } + return skipRoutes; + }(); + return skipRoutes; +} + void RunHTTPTest(json testJson, json& outResponse) { + if (std::ranges::find(GetSkipRoutes(), testJson.at("path").get()) != GetSkipRoutes().end()) { + GTEST_SKIP() << "Route " << testJson.at("path") << "is in list of skip routes"; + } std::cout << "Test info: " << '\n'; std::cout << "Path: " << testJson["path"].dump() << '\n'; std::cout << "method: " << testJson["method"].dump() << '\n'; @@ -29,7 +47,7 @@ void RunHTTPTest(json testJson, json& outResponse) { GTEST_FATAL_FAILURE_("Unrecognized http verb"); } if (res.body().empty()) { - GTEST_SKIP() << "Got an empty response"; + GTEST_FAIL() << "Got an empty response"; } outResponse = json::parse(res.body()); json expectedResponse = json::parse(testJson["response"].get()); diff --git a/tests/implementation/SequencedRequestTest.cpp b/tests/implementation/SequencedRequestTest.cpp index f5d39604..c2485a1e 100644 --- a/tests/implementation/SequencedRequestTest.cpp +++ b/tests/implementation/SequencedRequestTest.cpp @@ -1,3 +1,5 @@ +#include "SpectreRpcType.h" + #include #include #include @@ -73,12 +75,18 @@ TEST_P(SequencedRequestTest, ResponseValidation) { std::string testJsonStr = ss.str(); json testJson = json::parse(testJsonStr); if (testJson.contains("rpcType")) { + if (std::ranges::find(GetSkipRpcTypes(), SpectreRpcType(testJson.at("rpcType").get())) != GetSkipRpcTypes().end()) { + GTEST_SKIP() << "Skipping sequenced test due to rpc type " << testJson.at("rpcType") << " that is on the skip list"; + } // ws request ApplyTestRequestInserts(testJson["requestBody"], responses); json out; RunWebsocketTest(testJson, out); responses.push_back(out["response"]["payload"]); } else { + if (std::ranges::find(GetSkipRoutes(), testJson.at("path").get()) != GetSkipRoutes().end()) { + GTEST_SKIP() << "Skipping sequenced test due to route " << testJson.at("path") << " that is on the skip list"; + } // http request json out; RunHTTPTest(testJson, out); diff --git a/tests/implementation/TestWebsocketClient.cpp b/tests/implementation/TestWebsocketClient.cpp index 96fe9811..3e97d6b2 100644 --- a/tests/implementation/TestWebsocketClient.cpp +++ b/tests/implementation/TestWebsocketClient.cpp @@ -73,3 +73,4 @@ TestWebsocketClient::~TestWebsocketClient() { ioThread.join(); // wait for background thread } } + \ No newline at end of file diff --git a/tests/implementation/WebsocketRequestTest.cpp b/tests/implementation/WebsocketRequestTest.cpp index eb00ad1e..202fb780 100644 --- a/tests/implementation/WebsocketRequestTest.cpp +++ b/tests/implementation/WebsocketRequestTest.cpp @@ -1,9 +1,12 @@ +#include "ResourcesUtilities.h" #include "boost/asio/buffers_iterator.hpp" #include #include #include +#include #include +#include void RunWebsocketTest(const fs::path& testJsonPath, json& outResponse) { std::ifstream testFile(testJsonPath); @@ -14,10 +17,26 @@ void RunWebsocketTest(const fs::path& testJsonPath, json& outResponse) { RunWebsocketTest(testJson, outResponse); } +std::vector& GetSkipRpcTypes() { + static std::vector skipRpcTypes = []() { + std::vector rpcTypes; + std::ifstream testSkipFile(ResourcesUtilities::GetResourcesFolder() / "testrequests" / "wsSkipTests.txt"); + std::string line; + while (std::getline(testSkipFile, line)) { + rpcTypes.emplace_back(line); + } + return rpcTypes; + }(); + return skipRpcTypes; +} + void RunWebsocketTest(json testJson, json& outResponse) // NOLINT { TestWebsocketClient wsClient(8082); SpectreRpcType reqType = SpectreRpcType(testJson.at("rpcType").get()); + if (std::ranges::find(GetSkipRpcTypes(), reqType) != GetSkipRpcTypes().end()) { + GTEST_SKIP() << "Skipping since in the ignore list"; + } std::cout << "Test info: " << '\n'; std::cout << "RPC type: " << reqType.GetName() << '\n'; std::cout << "Request payload: " << testJson.at("requestBody").dump() << '\n'; @@ -25,9 +44,7 @@ void RunWebsocketTest(json testJson, json& outResponse) // NOLINT json& reqBody = testJson.at("requestBody"); boost::beast::flat_buffer res = wsClient.SendPacket(reqBody, reqType); std::string resStr(boost::asio::buffers_begin(res.data()), boost::asio::buffers_end(res.data())); - if (resStr.empty()) { - GTEST_SKIP() << "Skipping since no response given"; - } + ASSERT_FALSE(resStr.empty()) << "Expected a response to be given"; ASSERT_NO_THROW(outResponse = json::parse(resStr)); ASSERT_TRUE(outResponse.contains("sequenceNumber")); ASSERT_TRUE(outResponse.at("sequenceNumber") == 0); diff --git a/vcpkg b/vcpkg index ed8445dd..b08425aa 160000 --- a/vcpkg +++ b/vcpkg @@ -1 +1 @@ -Subproject commit ed8445dd2a9ba44e0e3ace4e952b2e33130082d9 +Subproject commit b08425aa65e91ff245dcbaa7ddbbea0d2b58cb00 diff --git a/vcpkg.json b/vcpkg.json index fc76fd22..a08ee12d 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -46,6 +46,7 @@ }, "sqlitecpp", "stduuid", - "tiny-process-library" + "tiny-process-library", + "drogon" ] }