From e9ff5f402dd814608488b5601f8dc4c9120aa022 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Tue, 29 Sep 2026 03:19:59 -0500 Subject: [PATCH] feat(world): read UgcDownloadFailed and log real download failures World packet 120 (UgcDownloadFailed: resType u32, blueprint ID, status u32, character ID; lu_packets, 31 bytes after the 0x53 as the client sends it) was logged as an unknown world packet. The client sends it from LWOResMgr2Interface::FinishResourceRequest (0x0105e5c0) for every blueprint file (player model, car or rocket build) whose request did not end with HTTP 200, including files it did not download at all (status 0). Live: 1525 packets, 1520 with status 0 (all four file types, around load and FetchModelMetadataRequest), 5 with 404. Live sent nothing back and the sessions carried on. The logout the client does on failed UGC downloads (NET_DISCONNECT_FAILED_DOWNLOAD_UGC, MainThread_LogoutDueToConnectionFailures 0x0102b9c0) is its own decision after repeated connection failures to the UGC HTTP server; there is no server message that prevents or triggers it. So the server only records it: a log line for an HTTP failure (for finding missing files on the UGC server), a debug line for status 0. MessageType::World gains UGC_DOWNLOAD_FAILED = 120 (appended; the magic enum range goes to 120). Check in game: nothing changes for the player. With a property that has models, load it: the world server log has no "Unknown world packet 120" lines; if a model file is missing on the UGC server there is one "failed to download blueprint" line naming it. Co-Authored-By: Claude Opus 5.5 --- dCommon/dEnums/MessageType/World.h | 5 +++-- dNet/WorldPackets.cpp | 15 ++++++++++++++ dNet/WorldPackets.h | 16 +++++++++++++++ dWorldServer/WorldServer.cpp | 13 ++++++++++++ .../dEnumsTests/MagicEnumTests.cpp | 1 + .../dNetTests/WorldPacketsTests.cpp | 20 +++++++++++++++++++ 6 files changed, 68 insertions(+), 2 deletions(-) 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).