diff --git a/dCommon/dEnums/MessageType/World.h b/dCommon/dEnums/MessageType/World.h index acca62466..333d9d63b 100644 --- a/dCommon/dEnums/MessageType/World.h +++ b/dCommon/dEnums/MessageType/World.h @@ -38,12 +38,13 @@ namespace MessageType { FAKE_PRG_CSR_MESSAGE, REQUEST_FREE_TRIAL_REFRESH, GM_SET_FREE_TRIAL_STATUS, - UI_HELP_TOP_5 = 91 + UI_HELP_TOP_5 = 91, + UGC_DOWNLOAD_FAILED = 120 }; } template <> struct magic_enum::customize::enum_range { static constexpr int min = 0; - static constexpr int max = 91; + static constexpr int max = 120; }; diff --git a/dNet/WorldPackets.cpp b/dNet/WorldPackets.cpp index 7aa101714..379959066 100644 --- a/dNet/WorldPackets.cpp +++ b/dNet/WorldPackets.cpp @@ -351,6 +351,21 @@ namespace WorldPackets { return true; } + void UgcDownloadFailed::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(resType); + bitStream.Write(blueprintId); + bitStream.Write(statusCode); + bitStream.Write(charId); + } + + bool UgcDownloadFailed::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(resType)); + VALIDATE_READ(bitStream.Read(blueprintId)); + VALIDATE_READ(bitStream.Read(statusCode)); + VALIDATE_READ(bitStream.Read(charId)); + return true; + } + void UIHelpTop5::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(language); } diff --git a/dNet/WorldPackets.h b/dNet/WorldPackets.h index 866025d54..b1addcab2 100644 --- a/dNet/WorldPackets.h +++ b/dNet/WorldPackets.h @@ -213,6 +213,22 @@ namespace WorldPackets { bool Deserialize(RakNet::BitStream& bitStream) override; }; + // Client -> server. Sent by the client's resource manager (LWOResMgr2Interface::FinishResourceRequest 0x0105e5c0) + // for every blueprint file (a player's model, or a car or rocket's build) whose request did not end with HTTP + // status 200. Status 0: the file was not downloaded (live: 1520 of 1525 packets, all four file types, on load); + // otherwise the HTTP status (live: 5 were 404). Live answered nothing. When the downloads keep failing, the client + // itself logs out with NET_DISCONNECT_FAILED_DOWNLOAD_UGC (MainThread_LogoutDueToConnectionFailures 0x0102b9c0). + struct UgcDownloadFailed : public WorldLUBitStream { + uint32_t resType{}; // eUgcResourceType, as a u32 + LWOOBJID blueprintId{}; + uint32_t statusCode{}; + LWOOBJID charId{}; + + UgcDownloadFailed() : WorldLUBitStream(MessageType::World::UGC_DOWNLOAD_FAILED) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + }; + // Client -> server. The help screen wants its top 5 issues. struct UIHelpTop5 : public WorldLUBitStream { int32_t language{}; // 0: en_US, 1: pl_US, 2: de_DE, 3: en_GB diff --git a/dWorldServer/WorldServer.cpp b/dWorldServer/WorldServer.cpp index ba264dc6d..af7f2a84a 100644 --- a/dWorldServer/WorldServer.cpp +++ b/dWorldServer/WorldServer.cpp @@ -1689,6 +1689,18 @@ namespace { } }; + struct UgcDownloadFailedPacket final : public WorldPackets::UgcDownloadFailed { + void Handle() override { + // Live sent nothing back; the client already fell back to its own copy or gave up. Log real HTTP + // failures (a missing or unreadable file on the UGC server). + if (statusCode == 0) { + LOG_DEBUG("Client %s did not download blueprint %llu file type %u (character %llu)", sysAddr.ToString(), blueprintId, resType, charId); + return; + } + LOG("Client %s failed to download blueprint %llu file type %u: HTTP status %u (character %llu)", sysAddr.ToString(), blueprintId, resType, statusCode, charId); + } + }; + template std::unique_ptr Create() { return std::make_unique(); } @@ -1709,6 +1721,7 @@ namespace { { MessageType::World::HANDLE_FUNNESS, Create }, { MessageType::World::UI_HELP_TOP_5, Create }, { MessageType::World::REQUEST_UGC_MANIFEST_INFO, Create }, + { MessageType::World::UGC_DOWNLOAD_FAILED, Create }, }; } diff --git a/tests/dCommonTests/dEnumsTests/MagicEnumTests.cpp b/tests/dCommonTests/dEnumsTests/MagicEnumTests.cpp index fcd517f1f..45925cb7e 100644 --- a/tests/dCommonTests/dEnumsTests/MagicEnumTests.cpp +++ b/tests/dCommonTests/dEnumsTests/MagicEnumTests.cpp @@ -55,6 +55,7 @@ TEST(MagicEnumTest, eWorldMessageTypeTest) { ENUM_EQ(MessageType::World, 32, REQUEST_FREE_TRIAL_REFRESH); ENUM_EQ(MessageType::World, 33, GM_SET_FREE_TRIAL_STATUS); ENUM_EQ(MessageType::World, 91, UI_HELP_TOP_5); + ENUM_EQ(MessageType::World, 120, UGC_DOWNLOAD_FAILED); ENUM_NE(MessageType::World, 37); ENUM_NE(MessageType::World, 123); diff --git a/tests/dGameTests/dNetTests/WorldPacketsTests.cpp b/tests/dGameTests/dNetTests/WorldPacketsTests.cpp index 3c650505b..5eebad713 100644 --- a/tests/dGameTests/dNetTests/WorldPacketsTests.cpp +++ b/tests/dGameTests/dNetTests/WorldPacketsTests.cpp @@ -718,6 +718,26 @@ TEST_F(WorldPacketsTests, SmallRequestsMatchLegacy) { EXPECT_PACKET_EQ(FromBitStream(mail.data), FromBitStream(mailRead.data)); } +// A packet from a 2011/2012 live capture (IDs replaced): UgcDownloadFailed (world 120) for a DDS (3) with status 0. +// The client sends 31 bytes after the 0x53: resType u32, blueprint, status u32, character. +TEST_F(WorldPacketsTests, UgcDownloadFailedMatchesLiveCapture) { + auto live = FromHex("53 04 00 78 00 00 00 00 03 00 00 00 07 06 05 04 03 02 01 10 00 00 00 00 01 00 00 00 00 00 00 10"); + RakNet::BitStream bitStream(live.bytes.data(), live.bytes.size(), false); + WorldPackets::UgcDownloadFailed failed; + ASSERT_TRUE(failed.ReadHeader(bitStream)); + ASSERT_TRUE(failed.Deserialize(bitStream)); + EXPECT_EQ(bitStream.GetNumberOfUnreadBits(), 0u); + EXPECT_EQ(failed.resType, 3u); + EXPECT_EQ(failed.blueprintId, 0x1001020304050607LL); + EXPECT_EQ(failed.statusCode, 0u); + EXPECT_EQ(failed.charId, 0x1000000000000001LL); + RakNet::BitStream again; + failed.WritePacket(again); + EXPECT_PACKET_EQ(live, FromBitStream(again)); + EXPECT_EQ(again.GetNumberOfBytesUsed() - 1, 31u); + ExpectTruncatedFails(failed); +} + // The layouts the 1.10.64 client uses without 3D services (UGCUSE3DSERVICES=7:0): it sends 16 bytes after the 0x53 // (SendRequestUGCManifestInfoPacket) and only reads an answer that is exactly 37 bytes after it // (PacketHandler_MSG_CLIENT_UGC_MANIFEST_RESPONSE: 21 bytes of manifest info after the blueprint and type).