From 9458230f0bff7c4fe6e71bdbe463d2357a2706d1 Mon Sep 17 00:00:00 2001 From: Aaron Kimbrell Date: Sat, 26 Sep 2026 19:12:17 -0500 Subject: [PATCH] refactor: property game messages as structs Converts the property messages to NetGameMsgs in PropertyMessages.{h,cpp}. Sent: OpenPropertyVendor, OpenPropertyManagement, DownloadPropertyData (replaces PropertyDataMessage, now with the client's PropertyData field names), PropertyRentalResponse, PropertyEntranceBegin, PropertySelectQuery (replaces PropertySelectQueryProperty with the client's PropertyInfo), GetModelsOnProperty, PlaceModelResponse and HandleUGCEquipPre/PostDeleteBasedOnEditMode. Received, registered in GameMessageHandler's map: SetPropertyAccess, UpdatePropertyOrModelForFilterCheck, QueryPropertyData, PropertyEditorBegin/End, PropertyContentsFromClient, ZonePropertyModelEquipped/Rotated, PlacePropertyModel, UpdateModelFromClient, DeleteModelFromClient, ControlBehaviors, PropertyEntranceSync, EnterProperty1, UpdatePropertyPerformanceCost, ReportOffensiveModel/Property and GetHotPropertyData. The news screen's NewsSendHotPropertiesInfoToClient moves here with the top properties lookup; the dashboard's player reports now take the decoded text. Handlers keep their logic and hand the work to PropertyManagementComponent, PropertyVendorComponent, PropertyEntranceComponent and MultiZoneEntranceComponent as before. Messages whose payload DLU ignored (PropertyEditorBegin, PropertyContentsFromClient, ZonePropertyModel*) now read it with the client's layout. The never-called SendZonePropertyModelEquipped (which wrote no default flags) is gone. No wire change and no change in recipients. Verified byte for byte against a frozen verbatim copy of the old functions, PropertyDataMessage and PropertySelectQueryProperty over an input grid (to one client and broadcast), received messages compared with the old handlers' read sequences with every truncated payload rejected, hand computed golden bytes, round trips and a mutation check. Layouts confirmed against the 1.10.64 client in Ghidra. Known wire bug kept as is: PlaceModelResponse writes response where the client reads a rotation quaternion (PlaceModelResponse::Deserialize 0x00dc0170). Co-Authored-By: Claude Opus 5.5 --- .../MultiZoneEntranceComponent.cpp | 5 +- .../dComponents/PropertyEntranceComponent.cpp | 110 +-- dGame/dComponents/PropertyEntranceComponent.h | 3 +- .../PropertyManagementComponent.cpp | 157 +++- dGame/dComponents/PropertyVendorComponent.cpp | 14 +- dGame/dGameMessages/CMakeLists.txt | 5 +- dGame/dGameMessages/GameMessageHandler.cpp | 95 +-- dGame/dGameMessages/GameMessages.cpp | 604 -------------- dGame/dGameMessages/GameMessages.h | 120 --- dGame/dGameMessages/PropertyDataMessage.cpp | 112 --- dGame/dGameMessages/PropertyDataMessage.h | 49 -- dGame/dGameMessages/PropertyMessages.cpp | 741 ++++++++++++++++++ dGame/dGameMessages/PropertyMessages.h | 398 ++++++++++ .../PropertySelectQueryProperty.cpp | 38 - .../PropertySelectQueryProperty.h | 29 - dGame/dUtilities/PlayerReports.cpp | 30 +- dGame/dUtilities/PlayerReports.h | 9 +- .../dGameMessagesTests/CMakeLists.txt | 1 + .../HotPropertyMessageTests.cpp | 2 +- .../Legacy/PropertyMessagesLegacy.h | 589 ++++++++++++++ .../PropertyMessagesTests.cpp | 609 ++++++++++++++ 21 files changed, 2580 insertions(+), 1140 deletions(-) delete mode 100644 dGame/dGameMessages/PropertyDataMessage.cpp delete mode 100644 dGame/dGameMessages/PropertyDataMessage.h create mode 100644 dGame/dGameMessages/PropertyMessages.cpp create mode 100644 dGame/dGameMessages/PropertyMessages.h delete mode 100644 dGame/dGameMessages/PropertySelectQueryProperty.cpp delete mode 100644 dGame/dGameMessages/PropertySelectQueryProperty.h create mode 100644 tests/dGameTests/dGameMessagesTests/Legacy/PropertyMessagesLegacy.h create mode 100644 tests/dGameTests/dGameMessagesTests/PropertyMessagesTests.cpp diff --git a/dGame/dComponents/MultiZoneEntranceComponent.cpp b/dGame/dComponents/MultiZoneEntranceComponent.cpp index 674af0991..0e2681c76 100644 --- a/dGame/dComponents/MultiZoneEntranceComponent.cpp +++ b/dGame/dComponents/MultiZoneEntranceComponent.cpp @@ -1,4 +1,5 @@ #include "MultiZoneEntranceComponent.h" +#include "PropertyMessages.h" #include "RocketLaunchpadControlComponent.h" #include "InventoryComponent.h" #include "CharacterComponent.h" @@ -24,7 +25,9 @@ void MultiZoneEntranceComponent::OnUse(Entity* originator) { if (!rocket) return; // the LUP world menu is just the property menu, the client knows how to handle it - GameMessages::SendPropertyEntranceBegin(m_Parent->GetObjectID(), originator->GetSystemAddress()); + GameMessages::PropertyEntranceBegin msg; + msg.target = m_Parent->GetObjectID(); + msg.Send(originator->GetSystemAddress()); } void MultiZoneEntranceComponent::OnSelectWorld(Entity* originator, uint32_t index) { diff --git a/dGame/dComponents/PropertyEntranceComponent.cpp b/dGame/dComponents/PropertyEntranceComponent.cpp index f282a73df..b4a02ac4a 100644 --- a/dGame/dComponents/PropertyEntranceComponent.cpp +++ b/dGame/dComponents/PropertyEntranceComponent.cpp @@ -6,7 +6,7 @@ #include "Database.h" #include "GameMessages.h" #include "PropertyManagementComponent.h" -#include "PropertySelectQueryProperty.h" +#include "PropertyMessages.h" #include "RocketLaunchpadControlComponent.h" #include "CharacterComponent.h" #include "UserManager.h" @@ -34,7 +34,9 @@ void PropertyEntranceComponent::OnUse(Entity* entity) { auto* rocket = entity->GetComponent()->RocketEquip(entity); if (!rocket) return; - GameMessages::SendPropertyEntranceBegin(m_Parent->GetObjectID(), entity->GetSystemAddress()); + GameMessages::PropertyEntranceBegin msg; + msg.target = m_Parent->GetObjectID(); + msg.Send(entity->GetSystemAddress()); AMFArrayValue args; @@ -62,7 +64,7 @@ void PropertyEntranceComponent::OnEnterProperty(Entity* entity, uint32_t index, if (index >= query.size()) return; - cloneId = query[index].CloneId; + cloneId = query[index].cloneId; } auto* launcher = m_Parent->GetComponent(); @@ -90,34 +92,34 @@ void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool incl // If the player has a property this query will have a single result. auto& playerEntry = entries.emplace_back(); if (playerProperty.has_value()) { - playerEntry.OwnerName = character->GetName(); - playerEntry.IsBestFriend = true; - playerEntry.IsFriend = true; - playerEntry.IsAlt = true; - playerEntry.IsOwned = true; - playerEntry.CloneId = playerProperty->cloneId; - playerEntry.Name = playerProperty->name; - playerEntry.Description = playerProperty->description; - playerEntry.AccessType = playerProperty->privacyOption; - playerEntry.IsModeratorApproved = playerProperty->modApproved; - playerEntry.DateLastPublished = playerProperty->lastUpdatedTime; - playerEntry.Reputation = playerProperty->reputation; - playerEntry.PerformanceCost = playerProperty->performanceCost; + playerEntry.ownerName = GeneralUtils::UTF8ToUTF16(character->GetName()); + playerEntry.isBff = true; + playerEntry.isFriend = true; + playerEntry.isAlt = true; + playerEntry.isOwned = true; + playerEntry.cloneId = playerProperty->cloneId; + playerEntry.name = GeneralUtils::UTF8ToUTF16(playerProperty->name); + playerEntry.description = GeneralUtils::UTF8ToUTF16(playerProperty->description); + playerEntry.accessType = playerProperty->privacyOption; + playerEntry.isModApproved = playerProperty->modApproved; + playerEntry.dateLastPublished = playerProperty->lastUpdatedTime; + playerEntry.reputation = playerProperty->reputation; + playerEntry.performanceCost = playerProperty->performanceCost; auto& entry = playerEntry; } else { - playerEntry.OwnerName = character->GetName(); - playerEntry.IsBestFriend = true; - playerEntry.IsFriend = true; - playerEntry.IsAlt = false; - playerEntry.IsOwned = false; - playerEntry.CloneId = character->GetPropertyCloneID(); - playerEntry.Name = ""; - playerEntry.Description = ""; - playerEntry.AccessType = 0; - playerEntry.IsModeratorApproved = false; - playerEntry.DateLastPublished = 0; - playerEntry.Reputation = 0; - playerEntry.PerformanceCost = 0.0f; + playerEntry.ownerName = GeneralUtils::UTF8ToUTF16(character->GetName()); + playerEntry.isBff = true; + playerEntry.isFriend = true; + playerEntry.isAlt = false; + playerEntry.isOwned = false; + playerEntry.cloneId = character->GetPropertyCloneID(); + playerEntry.name = u""; + playerEntry.description = u""; + playerEntry.accessType = 0; + playerEntry.isModApproved = false; + playerEntry.dateLastPublished = 0; + playerEntry.reputation = 0; + playerEntry.performanceCost = 0.0f; } IProperty::PropertyLookup propertyLookup; @@ -140,39 +142,47 @@ void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool incl } auto& entry = entries.emplace_back(); - entry.IsOwned = entry.CloneId == otherCharacter->cloneId; - entry.OwnerName = otherCharacter->name; - entry.CloneId = propertyEntry.cloneId; - entry.Name = propertyEntry.name; - entry.Description = propertyEntry.description; - entry.AccessType = propertyEntry.privacyOption; - entry.IsModeratorApproved = propertyEntry.modApproved; - entry.DateLastPublished = propertyEntry.lastUpdatedTime; - entry.Reputation = propertyEntry.reputation; - entry.PerformanceCost = propertyEntry.performanceCost; - entry.IsBestFriend = false; - entry.IsFriend = false; + entry.isOwned = entry.cloneId == otherCharacter->cloneId; + entry.ownerName = GeneralUtils::UTF8ToUTF16(otherCharacter->name); + entry.cloneId = propertyEntry.cloneId; + entry.name = GeneralUtils::UTF8ToUTF16(propertyEntry.name); + entry.description = GeneralUtils::UTF8ToUTF16(propertyEntry.description); + entry.accessType = propertyEntry.privacyOption; + entry.isModApproved = propertyEntry.modApproved; + entry.dateLastPublished = propertyEntry.lastUpdatedTime; + entry.reputation = propertyEntry.reputation; + entry.performanceCost = propertyEntry.performanceCost; + entry.isBff = false; + entry.isFriend = false; // Query to get friend and best friend fields const auto friendCheck = Database::Get()->GetBestFriendStatus(character->GetID(), owner); // If we got a result than the two players are friends. if (friendCheck.has_value()) { - entry.IsFriend = true; - entry.IsBestFriend = friendCheck->bestFriendStatus == 3; + entry.isFriend = true; + entry.isBff = friendCheck->bestFriendStatus == 3; } - if (!entry.IsModeratorApproved && entity->GetGMLevel() >= eGameMasterLevel::LEAD_MODERATOR) { - entry.Name = "[AWAITING APPROVAL]"; - entry.Description = "[AWAITING APPROVAL]"; - entry.IsModeratorApproved = true; + if (!entry.isModApproved && entity->GetGMLevel() >= eGameMasterLevel::LEAD_MODERATOR) { + entry.name = u"[AWAITING APPROVAL]"; + entry.description = u"[AWAITING APPROVAL]"; + entry.isModApproved = true; } // Query to determine whether this property is an alt character of the entity. for (const auto charid : Database::Get()->GetAccountCharacterIds(user->GetAccountID())) { - entry.IsAlt = charid == owner; - if (entry.IsAlt) break; + entry.isAlt = charid == owner; + if (entry.isAlt) break; } } // Query here is to figure out whether or not to display the button to go to the next page or not. - GameMessages::SendPropertySelectQuery(m_Parent->GetObjectID(), startIndex, lookupResult.totalEntriesMatchingQuery - (startIndex + numResults) > 0, character->GetPropertyCloneID(), false, true, entries, sysAddr); + GameMessages::PropertySelectQuery query; + query.target = m_Parent->GetObjectID(); + query.navOffset = startIndex; + query.thereAreMore = lookupResult.totalEntriesMatchingQuery - (startIndex + numResults) > 0; + query.cloneId = character->GetPropertyCloneID(); + query.hasFeaturedProperty = false; + query.wasFriends = true; + query.properties = entries; + query.Send(sysAddr); } diff --git a/dGame/dComponents/PropertyEntranceComponent.h b/dGame/dComponents/PropertyEntranceComponent.h index 1d51d97e5..14c60aeb9 100644 --- a/dGame/dComponents/PropertyEntranceComponent.h +++ b/dGame/dComponents/PropertyEntranceComponent.h @@ -6,6 +6,7 @@ #include "Entity.h" #include "EntityManager.h" #include "GameMessages.h" +#include "PropertyMessages.h" #include "eReplicaComponentType.h" /** @@ -63,7 +64,7 @@ private: /** * Cache of property information that was queried for property launched, indexed by property ID */ - std::map> propertyQueries; + std::map> propertyQueries; /** * The custom name for this property diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index f745b48a8..528d979da 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -5,7 +5,9 @@ #include "MissionComponent.h" #include "EntityManager.h" -#include "PropertyDataMessage.h" +#include "PropertyMessages.h" +#include "CDClientManager.h" +#include "CDPropertyTemplateTable.h" #include "UserManager.h" #include "GameMessages.h" #include "Character.h" @@ -406,11 +408,32 @@ void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const N models.insert_or_assign(model->GetObjectID(), spawnerId); - GameMessages::SendPlaceModelResponse(entity->GetObjectID(), entity->GetSystemAddress(), position, m_Parent->GetObjectID(), 14, originalRotation); + { + GameMessages::PlaceModelResponse msg; + msg.target = entity->GetObjectID(); + msg.position = position; + msg.propertyPlaqueID = m_Parent->GetObjectID(); + msg.response = 14; + msg.rotation = originalRotation; + msg.Send(entity->GetSystemAddress()); + } - GameMessages::SendUGCEquipPreCreateBasedOnEditMode(entity->GetObjectID(), entity->GetSystemAddress(), 0, spawnerId); + { + GameMessages::HandleUGCEquipPreCreateBasedOnEditMode msg; + msg.target = entity->GetObjectID(); + msg.modelCount = 0; + msg.modelID = spawnerId; + msg.Send(entity->GetSystemAddress()); + } - GameMessages::SendGetModelsOnProperty(entity->GetObjectID(), GetModels(), UNASSIGNED_SYSTEM_ADDRESS); + { + const auto& propertyModels = GetModels(); + GameMessages::GetModelsOnProperty msg; + msg.target = entity->GetObjectID(); + msg.models = { propertyModels.begin(), propertyModels.end() }; + LOG("Sending property models to (%llu) (%d)", msg.target, true); + msg.Send(UNASSIGNED_SYSTEM_ADDRESS); + } Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPlaced(entity); @@ -445,10 +468,24 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet if (model->GetLOT() == 14 && deleteReason == 0) { LOG("User is trying to pick up a BBB model, but this is not implemented, so we return to prevent the user from losing the model"); - GameMessages::SendUGCEquipPostDeleteBasedOnEditMode(entity->GetObjectID(), entity->GetSystemAddress(), LWOOBJID_EMPTY, 0); + { + GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode msg; + msg.target = entity->GetObjectID(); + msg.invItem = LWOOBJID_EMPTY; + msg.itemsTotal = 0; + msg.Send(entity->GetSystemAddress()); + } // Need this to pop the user out of their current state - GameMessages::SendPlaceModelResponse(entity->GetObjectID(), entity->GetSystemAddress(), entity->GetPosition(), m_Parent->GetObjectID(), 14, entity->GetRotation()); + { + GameMessages::PlaceModelResponse msg; + msg.target = entity->GetObjectID(); + msg.position = entity->GetPosition(); + msg.propertyPlaqueID = m_Parent->GetObjectID(); + msg.response = 14; + msg.rotation = entity->GetRotation(); + msg.Send(entity->GetSystemAddress()); + } return; } @@ -501,12 +538,33 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet } if (deleteReason == 0 || deleteReason == 2) { - GameMessages::SendUGCEquipPostDeleteBasedOnEditMode(entity->GetObjectID(), entity->GetSystemAddress(), item->GetId(), item->GetCount()); + { + GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode msg; + msg.target = entity->GetObjectID(); + msg.invItem = item->GetId(); + msg.itemsTotal = item->GetCount(); + msg.Send(entity->GetSystemAddress()); + } } - GameMessages::SendGetModelsOnProperty(entity->GetObjectID(), GetModels(), UNASSIGNED_SYSTEM_ADDRESS); + { + const auto& propertyModels = GetModels(); + GameMessages::GetModelsOnProperty msg; + msg.target = entity->GetObjectID(); + msg.models = { propertyModels.begin(), propertyModels.end() }; + LOG("Sending property models to (%llu) (%d)", msg.target, true); + msg.Send(UNASSIGNED_SYSTEM_ADDRESS); + } - GameMessages::SendPlaceModelResponse(entity->GetObjectID(), entity->GetSystemAddress(), NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 16, QuatUtils::IDENTITY); + { + GameMessages::PlaceModelResponse msg; + msg.target = entity->GetObjectID(); + msg.position = NiPoint3Constant::ZERO; + msg.propertyPlaqueID = LWOOBJID_EMPTY; + msg.response = 16; + msg.rotation = QuatUtils::IDENTITY; + msg.Send(entity->GetSystemAddress()); + } if (spawner != nullptr) { Game::zoneManager->RemoveSpawner(spawner->m_Info.spawnerID); @@ -532,7 +590,13 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet { item->Equip(); - GameMessages::SendUGCEquipPostDeleteBasedOnEditMode(entity->GetObjectID(), entity->GetSystemAddress(), item->GetId(), item->GetCount()); + { + GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode msg; + msg.target = entity->GetObjectID(); + msg.invItem = item->GetId(); + msg.itemsTotal = item->GetCount(); + msg.Send(entity->GetSystemAddress()); + } Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelPickedUp(entity); break; @@ -557,9 +621,24 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet } } - GameMessages::SendGetModelsOnProperty(entity->GetObjectID(), GetModels(), UNASSIGNED_SYSTEM_ADDRESS); + { + const auto& propertyModels = GetModels(); + GameMessages::GetModelsOnProperty msg; + msg.target = entity->GetObjectID(); + msg.models = { propertyModels.begin(), propertyModels.end() }; + LOG("Sending property models to (%llu) (%d)", msg.target, true); + msg.Send(UNASSIGNED_SYSTEM_ADDRESS); + } - GameMessages::SendPlaceModelResponse(entity->GetObjectID(), entity->GetSystemAddress(), NiPoint3Constant::ZERO, LWOOBJID_EMPTY, 16, QuatUtils::IDENTITY); + { + GameMessages::PlaceModelResponse msg; + msg.target = entity->GetObjectID(); + msg.position = NiPoint3Constant::ZERO; + msg.propertyPlaqueID = LWOOBJID_EMPTY; + msg.response = 16; + msg.rotation = QuatUtils::IDENTITY; + msg.Send(entity->GetSystemAddress()); + } if (spawner != nullptr) { Game::zoneManager->RemoveSpawner(spawner->m_Info.spawnerID); @@ -744,7 +823,7 @@ void PropertyManagementComponent::OnQueryPropertyData(Entity* originator, const const auto cloneId = worldId.GetCloneID(); LOG("Getting property info for %d", zoneId); - GameMessages::PropertyDataMessage message = GameMessages::PropertyDataMessage(zoneId); + const auto propertyTemplate = CDClientManager::GetTable()->GetByMapID(zoneId); const auto isClaimed = GetOwnerId() != LWOOBJID_EMPTY; @@ -778,26 +857,52 @@ void PropertyManagementComponent::OnQueryPropertyData(Entity* originator, const } } } - message.moderatorRequested = moderatorRequested; - message.reputation = reputation; - message.LastUpdatedTime = LastUpdatedTime; - message.OwnerId = ownerId; - message.OwnerName = ownerName; - message.Name = name; - message.Description = description; - message.ClaimedTime = claimed; - message.PrivacyOption = privacy; + GameMessages::DownloadPropertyData message; + message.target = author; + message.propertyId = 0; + message.templateId = static_cast(propertyTemplate.id); + message.mapId = static_cast(propertyTemplate.mapID); + message.vendorMapId = static_cast(propertyTemplate.vendorMapID); message.cloneId = clone_Id; - message.rejectionReason = rejectionReason; - message.Paths = GetPaths(); + message.name = GeneralUtils::UTF8ToUTF16(name); + message.description = GeneralUtils::UTF8ToUTF16(description); + message.ownerName = GeneralUtils::UTF8ToUTF16(ownerName); + message.ownerId = ownerId; + message.propertyType = 0; + message.zoneCode = 0; + message.rent = 0; + message.rentalPeriod = 1; + message.expirationDate = LastUpdatedTime; + message.rentAmount = 1; + message.reputation = reputation; + message.spawnName = GeneralUtils::ASCIIToUTF16(propertyTemplate.spawnName); + message.rentDuration = 0; + message.votes = 1; + message.durationType = 1; + message.renew = static_cast(privacy); + message.ownerAccountID = 0; + if (rejectionReason != "") message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_REJECTED; + else if (moderatorRequested == true && rejectionReason == "") message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_APPROVED; + else message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_PENDING; + message.lastLogoutTime = 0; + message.dayOfMonthPlaqueWasBought = 1; + message.repAchievementReq = 1; + message.zonePosition = { 548.0f, 406.0f, 178.0f }; + message.maxBuildHeight = 128.0f; + message.rentalDate = claimed; + message.accessType = static_cast(privacy); + message.pathPositions = GetPaths(); - SendDownloadPropertyData(author, message, UNASSIGNED_SYSTEM_ADDRESS); + LOG("(%llu) sending property data (%d)", author, true); + message.Send(UNASSIGNED_SYSTEM_ADDRESS); // send rejection here? } void PropertyManagementComponent::OnUse(Entity* originator) { OnQueryPropertyData(originator, UNASSIGNED_SYSTEM_ADDRESS); - GameMessages::SendOpenPropertyManagment(m_Parent->GetObjectID(), originator->GetSystemAddress()); + GameMessages::OpenPropertyManagement msg; + msg.target = PropertyManagementComponent::Instance()->GetParent()->GetObjectID(); + msg.Send(originator->GetSystemAddress()); } void PropertyManagementComponent::SetOwnerId(const LWOOBJID value) { diff --git a/dGame/dComponents/PropertyVendorComponent.cpp b/dGame/dComponents/PropertyVendorComponent.cpp index 8b9e79f2c..bb4f7137e 100644 --- a/dGame/dComponents/PropertyVendorComponent.cpp +++ b/dGame/dComponents/PropertyVendorComponent.cpp @@ -1,6 +1,6 @@ #include "PropertyVendorComponent.h" -#include "PropertyDataMessage.h" +#include "PropertyMessages.h" #include "GameMessages.h" #include "Character.h" #include "EntityManager.h" @@ -21,7 +21,9 @@ void PropertyVendorComponent::OnUse(Entity* originator) { if (PropertyManagementComponent::Instance()->GetOwnerId() == LWOOBJID_EMPTY) { LOG("Property vendor opening!"); - GameMessages::SendOpenPropertyVendor(m_Parent->GetObjectID(), originator->GetSystemAddress()); + GameMessages::OpenPropertyVendor msg; + msg.target = m_Parent->GetObjectID(); + msg.Send(originator->GetSystemAddress()); return; } @@ -41,7 +43,13 @@ void PropertyVendorComponent::OnBuyFromVendor(Entity* originator, const bool con return; } - GameMessages::SendPropertyRentalResponse(m_Parent->GetObjectID(), 0, 0, 0, 0, originator->GetSystemAddress()); + GameMessages::PropertyRentalResponse msg; + msg.target = m_Parent->GetObjectID(); + msg.cloneid = 0; + msg.code = 0; + msg.propertyID = 0; + msg.rentdue = 0; + msg.Send(originator->GetSystemAddress()); auto* controller = Game::zoneManager->GetZoneControlObject(); diff --git a/dGame/dGameMessages/CMakeLists.txt b/dGame/dGameMessages/CMakeLists.txt index eb37bd62b..823ab7ed4 100644 --- a/dGame/dGameMessages/CMakeLists.txt +++ b/dGame/dGameMessages/CMakeLists.txt @@ -5,9 +5,8 @@ set(DGAME_DGAMEMESSAGES_SOURCES "GameMessages.cpp" "MissionMessages.cpp" "PetMessages.cpp" - "RacingMessages.cpp" - "PropertyDataMessage.cpp" - "PropertySelectQueryProperty.cpp") + "PropertyMessages.cpp" + "RacingMessages.cpp") add_library(dGameMessages OBJECT ${DGAME_DGAMEMESSAGES_SOURCES}) target_link_libraries(dGameMessages diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index ed7dff096..15282c2b1 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -35,6 +35,7 @@ #include "RacingMessages.h" #include "MissionMessages.h" #include "PetMessages.h" +#include "PropertyMessages.h" #include "eMissionTaskType.h" #include "eReplicaComponentType.h" #include "ServiceType.h" @@ -45,7 +46,6 @@ #include "eGameMasterLevel.h" #include "StringifiedEnum.h" #include "MessageInspector.h" -#include "PlayerReports.h" namespace { using enum MessageType::Game; @@ -83,6 +83,26 @@ namespace { { CLIENT_EXIT_TAMING_MINIGAME, []() { return std::make_unique(); } }, { COMMAND_PET, []() { return std::make_unique(); } }, { DESPAWN_PET, []() { return std::make_unique(); } }, + + // Property + { SET_PROPERTY_ACCESS, []() { return std::make_unique(); } }, + { UPDATE_PROPERTY_OR_MODEL_FOR_FILTER_CHECK, []() { return std::make_unique(); } }, + { QUERY_PROPERTY_DATA, []() { return std::make_unique(); } }, + { PROPERTY_EDITOR_BEGIN, []() { return std::make_unique(); } }, + { PROPERTY_EDITOR_END, []() { return std::make_unique(); } }, + { PROPERTY_CONTENTS_FROM_CLIENT, []() { return std::make_unique(); } }, + { ZONE_PROPERTY_MODEL_EQUIPPED, []() { return std::make_unique(); } }, + { ZONE_PROPERTY_MODEL_ROTATED, []() { return std::make_unique(); } }, + { PLACE_PROPERTY_MODEL, []() { return std::make_unique(); } }, + { UPDATE_MODEL_FROM_CLIENT, []() { return std::make_unique(); } }, + { DELETE_MODEL_FROM_CLIENT, []() { return std::make_unique(); } }, + { CONTROL_BEHAVIORS, []() { return std::make_unique(); } }, + { PROPERTY_ENTRANCE_SYNC, []() { return std::make_unique(); } }, + { ENTER_PROPERTY1, []() { return std::make_unique(); } }, + { UPDATE_PROPERTY_PERFORMANCE_COST, []() { return std::make_unique(); } }, + { REPORT_OFFENSIVE_MODEL, []() { return std::make_unique(); } }, + { REPORT_OFFENSIVE_PROPERTY, []() { return std::make_unique(); } }, + { GET_HOT_PROPERTY_DATA, []() { return std::make_unique(); } }, }; }; @@ -318,11 +338,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::SendResurrect(entity); break; } - case MessageType::Game::GET_HOT_PROPERTY_DATA: { - GameMessages::HandleGetHotPropertyData(inStream, entity, sysAddr); - break; - } - case MessageType::Game::REQUEST_SERVER_PROJECTILE_IMPACT: { auto message = RequestServerProjectileImpact(); @@ -524,11 +539,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::HandleChoiceBoxRespond(inStream, entity, sysAddr); break; - // Property - case MessageType::Game::QUERY_PROPERTY_DATA: - GameMessages::HandleQueryPropertyData(inStream, entity, sysAddr); - break; - case MessageType::Game::START_BUILDING_WITH_ITEM: GameMessages::HandleStartBuildingWithItem(inStream, entity, sysAddr); break; @@ -537,34 +547,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::HandleSetBuildMode(inStream, entity, sysAddr); break; - case MessageType::Game::PROPERTY_EDITOR_BEGIN: - GameMessages::HandlePropertyEditorBegin(inStream, entity, sysAddr); - break; - - case MessageType::Game::PROPERTY_EDITOR_END: - GameMessages::HandlePropertyEditorEnd(inStream, entity, sysAddr); - break; - - case MessageType::Game::PROPERTY_CONTENTS_FROM_CLIENT: - GameMessages::HandlePropertyContentsFromClient(inStream, entity, sysAddr); - break; - - case MessageType::Game::ZONE_PROPERTY_MODEL_EQUIPPED: - GameMessages::HandlePropertyModelEquipped(inStream, entity, sysAddr); - break; - - case MessageType::Game::PLACE_PROPERTY_MODEL: - GameMessages::HandlePlacePropertyModel(inStream, entity, sysAddr); - break; - - case MessageType::Game::UPDATE_MODEL_FROM_CLIENT: - GameMessages::HandleUpdatePropertyModel(inStream, entity, sysAddr); - break; - - case MessageType::Game::DELETE_MODEL_FROM_CLIENT: - GameMessages::HandleDeletePropertyModel(inStream, entity, sysAddr); - break; - case MessageType::Game::BBB_LOAD_ITEM_REQUEST: GameMessages::HandleBBBLoadItemRequest(inStream, entity, sysAddr); break; @@ -573,30 +555,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::HandleBBBSaveRequest(inStream, entity, sysAddr); break; - case MessageType::Game::CONTROL_BEHAVIORS: - GameMessages::HandleControlBehaviors(inStream, entity, sysAddr); - break; - - case MessageType::Game::PROPERTY_ENTRANCE_SYNC: - GameMessages::HandlePropertyEntranceSync(inStream, entity, sysAddr); - break; - - case MessageType::Game::ENTER_PROPERTY1: - GameMessages::HandleEnterProperty(inStream, entity, sysAddr); - break; - - case MessageType::Game::ZONE_PROPERTY_MODEL_ROTATED: - Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelRotated(usr->GetLastUsedChar()->GetEntity()); - break; - - case MessageType::Game::UPDATE_PROPERTY_OR_MODEL_FOR_FILTER_CHECK: - GameMessages::HandleUpdatePropertyOrModelForFilterCheck(inStream, entity, sysAddr); - break; - - case MessageType::Game::SET_PROPERTY_ACCESS: - GameMessages::HandleSetPropertyAccess(inStream, entity, sysAddr); - break; - // Racing: most racing messages are registered in g_MessageHandlers case MessageType::Game::ACKNOWLEDGE_POSSESSION: GameMessages::HandleAcknowledgePossession(inStream, entity, sysAddr); @@ -605,9 +563,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System case MessageType::Game::REQUEST_DIE: GameMessages::HandleRequestDie(inStream, entity, sysAddr); break; - case MessageType::Game::UPDATE_PROPERTY_PERFORMANCE_COST: - GameMessages::HandleUpdatePropertyPerformanceCost(inStream, entity, sysAddr); - break; // SG case MessageType::Game::UPDATE_SHOOTING_GALLERY_ROTATION: GameMessages::HandleUpdateShootingGalleryRotation(inStream, entity, sysAddr); @@ -635,14 +590,6 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream& inStream, const System GameMessages::HandleReportBug(inStream, entity); break; - case MessageType::Game::REPORT_OFFENSIVE_MODEL: - PlayerReports::HandleReportOffensiveModel(inStream, entity); - break; - - case MessageType::Game::REPORT_OFFENSIVE_PROPERTY: - PlayerReports::HandleReportOffensiveProperty(inStream, entity); - break; - case MessageType::Game::CLIENT_RAIL_MOVEMENT_READY: GameMessages::HandleClientRailMovementReady(inStream, entity, sysAddr); break; diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 3d73ea679..427a14aad 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -84,13 +84,8 @@ // Message includes: #include "dZoneManager.h" -#include "PropertyDataMessage.h" -#include "HotPropertySlots.h" -#include "CDPropertyTemplateTable.h" -#include "CDPropertyEntranceComponentTable.h" #include "PropertyManagementComponent.h" #include "PropertyVendorComponent.h" -#include "PropertySelectQueryProperty.h" #include "TradingManager.h" #include "ControlBehaviors.h" #include "AMFDeserialize.h" @@ -1757,59 +1752,6 @@ void GameMessages::SendBBBSaveResponse(const LWOOBJID& objectId, const LWOOBJID& // Property -void GameMessages::SendOpenPropertyVendor(const LWOOBJID objectId, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::OPEN_PROPERTY_VENDOR); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendOpenPropertyManagment(const LWOOBJID objectId, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(PropertyManagementComponent::Instance()->GetParent()->GetObjectID()); - bitStream.Write(MessageType::Game::OPEN_PROPERTY_MANAGEMENT); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendDownloadPropertyData(const LWOOBJID objectId, const PropertyDataMessage& data, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::DOWNLOAD_PROPERTY_DATA); - - data.Serialize(bitStream); - - LOG("(%llu) sending property data (%d)", objectId, sysAddr == UNASSIGNED_SYSTEM_ADDRESS); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendPropertyRentalResponse(const LWOOBJID objectId, const LWOCLONEID cloneId, const uint32_t code, const LWOOBJID propertyId, const int64_t rentDue, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::PROPERTY_RENTAL_RESPONSE); - - bitStream.Write(cloneId); - bitStream.Write(code); - bitStream.Write(propertyId); - bitStream.Write(rentDue); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - void GameMessages::SendLockNodeRotation(Entity* entity, std::string nodeName) { CBITSTREAM; CMSGHEADER; @@ -1845,124 +1787,6 @@ void GameMessages::SendSetBuildModeConfirmed(LWOOBJID objectId, const SystemAddr SEND_PACKET; } -void GameMessages::SendGetModelsOnProperty(LWOOBJID objectId, std::map models, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::GET_MODELS_ON_PROPERTY); - - bitStream.Write(models.size()); - - for (const auto& pair : models) { - bitStream.Write(pair.first); - bitStream.Write(pair.second); - } - - LOG("Sending property models to (%llu) (%d)", objectId, sysAddr == UNASSIGNED_SYSTEM_ADDRESS); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendZonePropertyModelEquipped(LWOOBJID objectId, LWOOBJID playerId, LWOOBJID propertyId, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::ZONE_PROPERTY_MODEL_EQUIPPED); - - bitStream.Write(playerId); - bitStream.Write(propertyId); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendPlaceModelResponse(LWOOBJID objectId, const SystemAddress& sysAddr, NiPoint3 position, - LWOOBJID plaque, int32_t response, NiQuaternion rotation) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::PLACE_MODEL_RESPONSE); - - bitStream.Write(position != NiPoint3Constant::ZERO); - if (position != NiPoint3Constant::ZERO) { - bitStream.Write(position); - } - - bitStream.Write(plaque != LWOOBJID_EMPTY); - if (plaque != LWOOBJID_EMPTY) { - bitStream.Write(plaque); - } - - bitStream.Write(response != 0); - if (response != 0) { - bitStream.Write(response); - } - - bitStream.Write(rotation != QuatUtils::IDENTITY); - if (rotation != QuatUtils::IDENTITY) { - bitStream.Write(response); - } - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; - -} - -void GameMessages::SendUGCEquipPreCreateBasedOnEditMode(LWOOBJID objectId, const SystemAddress& sysAddr, int modelCount, LWOOBJID model) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::HANDLE_UGC_POST_CREATE_BASED_ON_EDIT_MODE); - - bitStream.Write(modelCount); - bitStream.Write(model); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendUGCEquipPostDeleteBasedOnEditMode(LWOOBJID objectId, const SystemAddress& sysAddr, LWOOBJID inventoryItem, int itemTotal) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::HANDLE_UGC_POST_DELETE_BASED_ON_EDIT_MODE); - - bitStream.Write(inventoryItem); - - bitStream.Write(itemTotal != 0); - if (itemTotal != 0) { - bitStream.Write(itemTotal); - } - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::HandleSetPropertyAccess(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - uint8_t accessType{}; - int32_t renew{}; - - bool accessTypeIsDefault{}; - inStream.Read(accessTypeIsDefault); - if (accessTypeIsDefault != 0) inStream.Read(accessType); - - bool renewIsDefault{}; - inStream.Read(renewIsDefault); - if (renewIsDefault != 0) inStream.Read(renew); - - LOG("Set privacy option to: %i", accessType); - - if (PropertyManagementComponent::Instance() == nullptr) return; - - PropertyManagementComponent::Instance()->SetPrivacyOption(static_cast(accessType)); -} - void GameMessages::HandleUnUseModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { bool unknown{}; LWOOBJID objIdToAddToInventory{}; @@ -1989,68 +1813,6 @@ void GameMessages::HandleUnUseModel(RakNet::BitStream& inStream, Entity* entity, } } -void GameMessages::HandleUpdatePropertyOrModelForFilterCheck(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool isProperty{}; - LWOOBJID objectId{}; - LWOOBJID playerId{}; - LWOOBJID worldId{}; - uint32_t nameLength{}; - std::u16string name{}; - uint32_t descriptionLength{}; - std::u16string description{}; - - inStream.Read(isProperty); - inStream.Read(objectId); - inStream.Read(playerId); - inStream.Read(worldId); - - inStream.Read(descriptionLength); - if (descriptionLength > MAX_MESSAGE_LENGTH) return; - for (uint32_t i = 0; i < descriptionLength; ++i) { - uint16_t character; - inStream.Read(character); - description.push_back(character); - } - - inStream.Read(nameLength); - if (nameLength > MAX_MESSAGE_LENGTH) return; - for (uint32_t i = 0; i < nameLength; ++i) { - uint16_t character; - inStream.Read(character); - name.push_back(character); - } - - PropertyManagementComponent::Instance()->UpdatePropertyDetails(GeneralUtils::UTF16ToWTF8(name), GeneralUtils::UTF16ToWTF8(description)); -} - -void GameMessages::HandleQueryPropertyData(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - LOG("Entity (%i) requesting data", entity->GetLOT()); - - /* - auto entites = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::PROPERTY_VENDOR); - - entity = entites[0]; - */ - - auto* propertyVendorComponent = static_cast(entity->GetComponent(eReplicaComponentType::PROPERTY_VENDOR)); - - if (propertyVendorComponent != nullptr) { - propertyVendorComponent->OnQueryPropertyData(entity, sysAddr); - } - - /* - entites = Game::entityManager->GetEntitiesByComponent(eReplicaComponentType::PROPERTY_MANAGEMENT); - - entity = entites[0]; - */ - - auto* propertyManagerComponent = static_cast(entity->GetComponent(eReplicaComponentType::PROPERTY_MANAGEMENT)); - - if (propertyManagerComponent != nullptr) { - propertyManagerComponent->OnQueryPropertyData(entity, sysAddr); - } -} - void GameMessages::HandleSetBuildMode(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { bool start{}; int32_t distanceType = -1; @@ -2141,69 +1903,6 @@ void GameMessages::HandleStartBuildingWithItem(RakNet::BitStream& inStream, Enti ); } -void GameMessages::HandlePropertyEditorBegin(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - PropertyManagementComponent::Instance()->OnStartBuilding(); - - Game::zoneManager->GetZoneControlObject()->OnZonePropertyEditBegin(); -} - -void GameMessages::HandlePropertyEditorEnd(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - PropertyManagementComponent::Instance()->OnFinishBuilding(); - - Game::zoneManager->GetZoneControlObject()->OnZonePropertyEditEnd(); -} - -void GameMessages::HandlePropertyContentsFromClient(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - User* user = UserManager::Instance()->GetUser(sysAddr); - - Entity* player = Game::entityManager->GetEntity(user->GetLoggedInChar()); - - SendGetModelsOnProperty(player->GetObjectID(), PropertyManagementComponent::Instance()->GetModels(), UNASSIGNED_SYSTEM_ADDRESS); -} - -void GameMessages::HandlePropertyModelEquipped(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - Game::zoneManager->GetZoneControlObject()->OnZonePropertyModelEquipped(); -} - -void GameMessages::HandlePlacePropertyModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - LWOOBJID model; - - inStream.Read(model); - - PropertyManagementComponent::Instance()->UpdateModelPosition(model, NiPoint3Constant::ZERO, QuatUtils::IDENTITY); -} - -void GameMessages::HandleUpdatePropertyModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - LWOOBJID model; - NiPoint3 position; - NiQuaternion rotation = QuatUtils::IDENTITY; - - inStream.Read(model); - inStream.Read(position); - - if (inStream.ReadBit()) { - inStream.Read(rotation); - } - - PropertyManagementComponent::Instance()->UpdateModelPosition(model, position, rotation); -} - -void GameMessages::HandleDeletePropertyModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - LWOOBJID model = LWOOBJID_EMPTY; - int deleteReason = 0; - - if (inStream.ReadBit()) { - inStream.Read(model); - } - - if (inStream.ReadBit()) { - inStream.Read(deleteReason); - - } - - PropertyManagementComponent::Instance()->DeleteModel(model, deleteReason); -} - void GameMessages::HandleBBBLoadItemRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { LWOOBJID previousItemID = LWOOBJID_EMPTY; inStream.Read(previousItemID); @@ -2273,42 +1972,6 @@ void GameMessages::SendUnSmash(Entity* entity, LWOOBJID builderID, float duratio SEND_PACKET_BROADCAST; } -void GameMessages::HandleControlBehaviors(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - AMFDeserialize reader; - std::unique_ptr amfArguments; - try { - auto deserializedData = reader.Read(inStream); - if (!deserializedData || deserializedData->GetValueType() != eAmf::Array) { - LOG("Failed to deserialize AMF data for control behaviors command: not an array"); - return; - } - - amfArguments.reset(static_cast(deserializedData.release())); - } catch (...) { - LOG("Failed to deserialize AMF data for control behaviors command"); - return; - } - if (amfArguments->GetValueType() != eAmf::Array) return; - - uint32_t commandLength{}; - inStream.Read(commandLength); - - if (commandLength > MAX_MESSAGE_LENGTH) return; // Prevent DoS via unbounded command buffer - - std::string command; - command.reserve(commandLength); - for (uint32_t i = 0; i < commandLength; ++i) { - unsigned char character; - inStream.Read(character); - command.push_back(character); - } - - auto* const owner = PropertyManagementComponent::Instance()->GetOwner(); - if (!owner) return; - - ControlBehaviors::Instance().ProcessCommand(entity, *amfArguments, command, owner); -} - void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { /* ___ ___ @@ -2491,76 +2154,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent } } -void GameMessages::HandlePropertyEntranceSync(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - bool includeNullAddress{}; - bool includeNullDescription{}; - bool playerOwn{}; - bool updateUi{}; - int32_t numResults{}; - int32_t reputation{}; - int32_t sortMethod{}; - int32_t startIndex{}; - uint32_t filterTextLength{}; - std::string filterText{}; - - inStream.Read(includeNullAddress); - inStream.Read(includeNullDescription); - inStream.Read(playerOwn); - inStream.Read(updateUi); - inStream.Read(numResults); - inStream.Read(reputation); - inStream.Read(sortMethod); - inStream.Read(startIndex); - inStream.Read(filterTextLength); - - if (filterTextLength > MAX_MESSAGE_LENGTH) return; - for (auto i = 0u; i < filterTextLength; i++) { - char c; - inStream.Read(c); - filterText.push_back(c); - } - - auto* player = PlayerManager::GetPlayer(sysAddr); - - auto* entranceComponent = entity->GetComponent(); - - if (entranceComponent == nullptr) return; - - entranceComponent->OnPropertyEntranceSync(player, - includeNullAddress, - includeNullDescription, - playerOwn, - updateUi, - numResults, - reputation, - sortMethod, - startIndex, - filterText, - sysAddr - ); -} - -void GameMessages::HandleEnterProperty(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - uint32_t index{}; - bool returnToZone{}; - - inStream.Read(index); - inStream.Read(returnToZone); - - auto* player = PlayerManager::GetPlayer(sysAddr); - - auto* entranceComponent = entity->GetComponent(); - if (entranceComponent != nullptr) { - entranceComponent->OnEnterProperty(player, index, returnToZone, sysAddr); - return; - } - - auto multiZoneEntranceComponent = entity->GetComponent(); - if (multiZoneEntranceComponent != nullptr) { - multiZoneEntranceComponent->OnSelectWorld(player, index); - } -} - void GameMessages::HandleSetConsumableItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { LOT lot; @@ -2839,40 +2432,6 @@ void GameMessages::SendRemoveRunSpeedModifier(LWOOBJID objectId, uint32_t modifi SEND_PACKET; } -void GameMessages::SendPropertyEntranceBegin(LWOOBJID objectId, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::PROPERTY_ENTRANCE_BEGIN); - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - -void GameMessages::SendPropertySelectQuery(LWOOBJID objectId, int32_t navOffset, bool thereAreMore, int32_t cloneId, bool hasFeaturedProperty, bool wasFriends, const std::vector& entries, const SystemAddress& sysAddr) { - CBITSTREAM; - CMSGHEADER; - - bitStream.Write(objectId); - bitStream.Write(MessageType::Game::PROPERTY_SELECT_QUERY); - - bitStream.Write(navOffset); - bitStream.Write(thereAreMore); - bitStream.Write(cloneId); - bitStream.Write(hasFeaturedProperty); - bitStream.Write(wasFriends); - - bitStream.Write(entries.size()); - - for (auto& entry : entries) { - entry.Serialize(bitStream); - } - - if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; - SEND_PACKET; -} - void GameMessages::SendNotifyObject(LWOOBJID objectId, LWOOBJID objIDSender, std::u16string name, const SystemAddress& sysAddr, int param1, int param2) { CBITSTREAM; CMSGHEADER; @@ -3589,35 +3148,6 @@ void GameMessages::SendUpdateReputation(const LWOOBJID objectId, const int64_t r SEND_PACKET; } -void GameMessages::HandleUpdatePropertyPerformanceCost(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - float performanceCost = 0.0f; - - if (inStream.ReadBit()) inStream.Read(performanceCost); - - if (performanceCost == 0.0f) return; - - auto zone = Game::zoneManager->GetZone(); - if (!zone) { - LOG("If you see this message, something is very wrong."); - return; - } - - const auto* const propertyManagementComponent = entity->GetComponent(); - const auto* const ownerEntity = propertyManagementComponent ? propertyManagementComponent->GetOwner() : nullptr; - const auto* const character = ownerEntity ? ownerEntity->GetCharacter() : nullptr; - const auto& zoneID = zone->GetZoneID(); - if (character && character->GetPropertyCloneID() == zoneID.GetCloneID()) { - Database::Get()->UpdatePerformanceCost(zoneID, performanceCost); - } -} - - - - - - - - void GameMessages::SendSetResurrectRestoreValues(Entity* targetEntity, int32_t armorRestore, int32_t healthRestore, int32_t imaginationRestore) { CBITSTREAM; CMSGHEADER; @@ -4829,120 +4359,6 @@ void GameMessages::HandleMatchRequest(RakNet::BitStream& inStream, Entity* entit } } -void GameMessages::HandleGetHotPropertyData(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - SendGetHotPropertyData(inStream, entity, sysAddr); -} - -namespace { - using HotPropertyInfo = GameMessages::NewsSendHotPropertiesInfoToClient::HotPropertyInfo; - - // The news screen's slots and the property worlds they can show properties of, from the CDClient (see HotPropertySlots.h) - struct NewsWorlds { - std::vector slots; - std::vector worlds; - }; - - const NewsWorlds& GetNewsWorlds() { - static const auto news = [] { - std::vector templates; - for (const auto& row : CDClientManager::GetTable()->GetEntries()) templates.push_back({ row.id, row.mapID, row.spawnName }); - std::vector entrances; - for (const auto& row : CDClientManager::GetTable()->GetEntries()) entrances.push_back({ row.mapID, row.propertyName }); - return NewsWorlds{ HotPropertySlots::ResolveSlots(templates, entrances), HotPropertySlots::PropertyWorlds(templates, entrances) }; - }(); - return news; - } - - HotPropertyInfo ToHotProperty(const IProperty::Info& info, const std::string& ownerName, uint32_t templateId) { - HotPropertyInfo hot; - hot.propertyId = info.id; - hot.ownerId = info.ownerId; - hot.ownerName = GeneralUtils::UTF8ToUTF16(ownerName); - hot.reputation = info.reputation; - hot.templateId = static_cast(templateId); - hot.name = GeneralUtils::UTF8ToUTF16(info.name); // empty: the client shows the template's name - hot.description = GeneralUtils::UTF8ToUTF16(info.description); - hot.performanceCost = info.performanceCost; - hot.lastPublished = info.lastUpdatedTime; - hot.cloneId = info.cloneId; - return hot; - } - - // What each slot shows, as chosen on the dashboard (featured_properties, featured_properties_settings), resolved by - // HotPropertySlots::Resolve so no property is shown twice. Every entry carries its slot's template id, whatever - // world the property is on: that is what puts it in the slot. Empty slots are left out. - std::vector LoadHotProperties() { - const auto& news = GetNewsWorlds(); - const bool fullAuto = Database::Get()->GetFeaturedPropertiesSettings().fullAuto; - std::map chosen; - for (const auto& row : Database::Get()->GetFeaturedPropertySlots()) chosen[row.templateId] = row; - - std::vector choices; - for (const auto& slot : news.slots) { - const auto it = chosen.find(slot.templateId); - HotPropertySlots::Choice choice{ HotPropertySlots::eMode::AUTO, slot.mapId }; - if (it != chosen.end()) choice = { HotPropertySlots::ModeFromInt(it->second.mode), HotPropertySlots::Location(it->second.zoneId, slot, news.worlds), it->second.propertyId }; - choices.push_back(choice); - } - - // The candidates, with what to send for each - std::vector candidates; - std::map> properties; - for (const auto world : HotPropertySlots::CandidateWorlds(choices, fullAuto)) { - IProperty::ShowcaseQuery query; - query.zoneId = world; - query.sort = IProperty::ShowcaseSort::REPUTATION; - query.length = HotPropertySlots::CANDIDATES_PER_WORLD; - for (const auto& entry : Database::Get()->GetShowcaseProperties(query).entries) { - candidates.push_back({ entry.info.id, entry.info.zoneId, entry.info.reputation }); - properties.emplace(entry.info.id, std::make_pair(entry.info, entry.ownerName)); - } - } - for (const auto& choice : choices) { - if (fullAuto || choice.mode != HotPropertySlots::eMode::PICKED || properties.contains(choice.propertyId)) continue; - const auto info = Database::Get()->GetPropertyInfo(choice.propertyId); - const auto owner = info ? Database::Get()->GetCharacterInfo(info->ownerId) : std::nullopt; - if (!info || !owner || !HotPropertySlots::Featurable(info->modApproved, info->privacyOption, info->zoneId, choice.mapId)) continue; - candidates.push_back({ info->id, info->zoneId, info->reputation }); - properties.emplace(info->id, std::make_pair(*info, owner->name)); - } - - std::vector hot; - const auto showing = HotPropertySlots::Resolve(choices, fullAuto, candidates); - for (size_t i = 0; i < showing.size(); i++) { - if (!showing[i].propertyId) continue; - const auto& [info, ownerName] = properties.at(*showing[i].propertyId); - hot.push_back(ToHotProperty(info, ownerName, news.slots[i].templateId)); - } - return hot; - } -} - -void GameMessages::SendGetHotPropertyData(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr) { - if (!entity) return; - - // The screen asks every time it opens; the answer is the same for everyone, so it is kept for a little while - static std::vector cached; - static std::chrono::steady_clock::time_point loadedAt{}; - static bool loaded = false; - const auto now = std::chrono::steady_clock::now(); - if (!loaded || now - loadedAt > std::chrono::seconds(30)) { - try { - cached = LoadHotProperties(); - } catch (const std::exception& ex) { - LOG("Failed to load the news screen's top properties: %s", ex.what()); - cached.clear(); - } - loadedAt = now; - loaded = true; - } - - NewsSendHotPropertiesInfoToClient message; - message.target = entity->GetObjectID(); - for (const auto index : HotPropertySlots::NewsOrder(cached.size())) message.properties.push_back(cached[index]); - message.Send(sysAddr); -} - void GameMessages::HandleReportBug(RakNet::BitStream& inStream, Entity* entity) { //Definitely not stolen from autogenerated code, no sir: IBugReports::Info reportInfo; @@ -5609,24 +5025,4 @@ namespace GameMessages { void ToggleGMInvis::Serialize(RakNet::BitStream& stream) const { stream.Write(bStateOut); } - - void NewsSendHotPropertiesInfoToClient::Serialize(RakNet::BitStream& stream) const { - const auto writeWString = [&stream](const std::u16string& text) { - stream.Write(text.size()); - for (const auto character : text) stream.Write(character); - }; - stream.Write(properties.size()); - for (const auto& info : properties) { - stream.Write(info.propertyId); - stream.Write(info.ownerId); - writeWString(info.ownerName); - stream.Write(info.reputation); - stream.Write(info.templateId); - writeWString(info.name); - writeWString(info.description); - stream.Write(info.performanceCost); - stream.Write(info.lastPublished); - stream.Write(info.cloneId); - } - } } diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 5494df961..82c40c215 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -22,7 +22,6 @@ class Entity; class Item; class User; class Leaderboard; -class PropertySelectQueryProperty; class TradeItem; class LDFBaseData; @@ -126,7 +125,6 @@ namespace GameMessages { return event.Send(); } - class PropertyDataMessage; void SendFireEventClientSide(const LWOOBJID& objectID, const SystemAddress& sysAddr, std::u16string args, const LWOOBJID& object, int64_t param1, int param2, const LWOOBJID& sender); void SendTeleport(const LWOOBJID& objectID, const NiPoint3& pos, const NiQuaternion& rot, const SystemAddress& sysAddr, bool bSetRotation = false); void SendPlayAnimation(Entity* entity, const std::u16string& animationName, float fPriority = 0.0f, float fScale = 1.0f); @@ -249,19 +247,6 @@ namespace GameMessages { */ void SendUnSmash(Entity* entity, LWOOBJID builderID = LWOOBJID_EMPTY, float duration = 3.0f); - /** - * @brief This GameMessage is the one that handles all of the property behavior incoming messages from the client. - * - * The GameMessage struct can be located here https://lcdruniverse.org/lu_packets/lu_packets/world/gm/server/struct.ControlBehaviors.html - * For information on the AMF3 format can be found here https://rtmp.veriskope.com/pdf/amf3-file-format-spec.pdf - * For any questions regarding AMF3 you can contact EmosewaMC on GitHub - * - * @param inStream The incoming data sent from the client - * @param entity The Entity that sent the message - * @param sysAddr The SystemAddress that sent the message - */ - void HandleControlBehaviors(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - // Rails stuff void SendSetRailMovement(const LWOOBJID& objectID, bool pathGoForward, std::u16string pathName, uint32_t pathStart, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS, @@ -295,12 +280,6 @@ namespace GameMessages { // Property messages - void SendOpenPropertyVendor(LWOOBJID objectId, const SystemAddress& sysAddr); - - void SendOpenPropertyManagment(LWOOBJID objectId, const SystemAddress& sysAddr); - - void SendDownloadPropertyData(LWOOBJID objectId, const PropertyDataMessage& data, const SystemAddress& sysAddr); - /** * @brief Send an updated item id to the client when they load a blueprint in brick build mode * @@ -311,54 +290,18 @@ namespace GameMessages { */ void SendBlueprintLoadItemResponse(const SystemAddress& sysAddr, bool success, LWOOBJID oldItemId, LWOOBJID newItemId); - void SendPropertyRentalResponse(LWOOBJID objectId, LWOCLONEID cloneId, uint32_t code, LWOOBJID propertyId, int64_t rentDue, const SystemAddress& sysAddr); - void SendLockNodeRotation(Entity* entity, std::string nodeName); void SendSetBuildModeConfirmed(LWOOBJID objectId, const SystemAddress& sysAddr, bool start, bool warnVisitors, bool modePaused, int32_t modeValue, LWOOBJID playerId, NiPoint3 startPos = NiPoint3Constant::ZERO); - void SendGetModelsOnProperty(LWOOBJID objectId, std::map models, const SystemAddress& sysAddr); - - void SendZonePropertyModelEquipped(LWOOBJID objectId, LWOOBJID playerId, LWOOBJID propertyId, const SystemAddress& sysAddr); - - void SendPlaceModelResponse(LWOOBJID objectId, const SystemAddress& sysAddr, NiPoint3 position, LWOOBJID plaque, int32_t response, NiQuaternion rotation); - - void SendUGCEquipPreCreateBasedOnEditMode(LWOOBJID objectId, const SystemAddress& sysAddr, int modelCount, LWOOBJID model); - - void SendUGCEquipPostDeleteBasedOnEditMode(LWOOBJID objectId, const SystemAddress& sysAddr, LWOOBJID inventoryItem, int itemTotal); - - void HandleSetPropertyAccess(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleUpdatePropertyOrModelForFilterCheck(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleQueryPropertyData(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleSetBuildMode(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleStartBuildingWithItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandlePropertyEditorBegin(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandlePropertyEditorEnd(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandlePropertyContentsFromClient(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandlePropertyModelEquipped(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandlePlacePropertyModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleUpdatePropertyModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleDeletePropertyModel(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleBBBLoadItemRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandlePropertyEntranceSync(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - void HandleEnterProperty(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void HandleSetConsumableItem(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); void SendPlayCinematic(LWOOBJID objectId, std::u16string pathName, const SystemAddress& sysAddr, @@ -414,10 +357,6 @@ namespace GameMessages { void SendRemoveRunSpeedModifier(LWOOBJID objectId, uint32_t modifier, const SystemAddress& sysAddr); - void SendPropertyEntranceBegin(LWOOBJID objectId, const SystemAddress& sysAddr); - - void SendPropertySelectQuery(LWOOBJID objectId, int32_t navOffset, bool thereAreMore, int32_t cloneId, bool hasFeaturedProperty, bool wasFriends, const std::vector& entries, const SystemAddress& sysAddr); - void SendNotifyObject(LWOOBJID objectId, LWOOBJID objIDSender, std::u16string name, const SystemAddress& sysAddr, int param1 = 0, int param2 = 0); void HandleVerifyAck(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); @@ -500,38 +439,6 @@ namespace GameMessages { */ void HandleAcknowledgePossession(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - /** - * @brief A request from a client to get the hot properties that would appear on the news feed - * This incoming message has NO DATA and is simply a request that expects to send a reply to the sender. - * - * @param inStream packet of data - * @param entity The Entity that sent the request - * @param sysAddr The SystemAddress of the Entity that sent the request - */ - void HandleGetHotPropertyData(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - - /** - * @brief A request from a client to get the hot properties that would appear on the news feed - * The struct of data to send is as follows - * - * [u32] - Number of properties - * [objid] - property id - * [objid] - property owner id - * [wstring] - property owner name - * [u64] - total reputation - * [i32] - property template id - * [wstring] - property name - * [wstring] - property description - * [float] - performance cost - * [timestamp] - time last published - * [cloneid] - clone id - * - * @param inStream packet of data - * @param entity The Entity that sent the request - * @param sysAddr The SystemAddress of the Entity that sent the request - */ - void SendGetHotPropertyData(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - //Racing: void HandleRequestDie(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); @@ -550,8 +457,6 @@ namespace GameMessages { bool bUseLeaderboards ); - void HandleUpdatePropertyPerformanceCost(RakNet::BitStream& inStream, Entity* entity, const SystemAddress& sysAddr); - void SendNotifyClientShootingGalleryScore(LWOOBJID objectId, const SystemAddress& sysAddr, float addTime, int32_t score, @@ -930,31 +835,6 @@ namespace GameMessages { int32_t setId{ -1 }; }; - /** - * The news screen's "Today's Top Properties" (GM 1510), the answer to GetHotPropertyData (GM 1511). - * Client: GameMessage::NewsSendHotPropertiesInfoToClient, entries read by NewsHotPropertyInfo::Deserialize - * (0x00c0cc20 in 1.10.64) and shown by LWOCharacterComponent::HotPropertyData (0x00cf83f0). See HotPropertySlots.h. - */ - struct NewsSendHotPropertiesInfoToClient : public NetGameMsg { - NewsSendHotPropertiesInfoToClient() : NetGameMsg(MessageType::Game::SEND_HOT_PROPERTY_DATA) {} - void Serialize(RakNet::BitStream& bitStream) const override; - - struct HotPropertyInfo { - LWOOBJID propertyId{}; - LWOOBJID ownerId{}; - std::u16string ownerName; - uint64_t reputation{}; - int32_t templateId{}; // PropertyTemplate id: picks the news screen slot - std::u16string name; - std::u16string description; - float performanceCost{}; - uint64_t lastPublished{}; // unix time - uint32_t cloneId{}; - }; - - std::vector properties; - }; - struct ObjectLoaded : public GameMsg { ObjectLoaded() : GameMsg(MessageType::Game::OBJECT_LOADED) {} diff --git a/dGame/dGameMessages/PropertyDataMessage.cpp b/dGame/dGameMessages/PropertyDataMessage.cpp deleted file mode 100644 index ab7fe9c3f..000000000 --- a/dGame/dGameMessages/PropertyDataMessage.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "PropertyDataMessage.h" - -#include "GeneralUtils.h" - -#include "Game.h" -#include "Logger.h" -#include "CDClientManager.h" - -#include "CDPropertyTemplateTable.h" - -void GameMessages::PropertyDataMessage::Serialize(RakNet::BitStream& stream) const { - stream.Write(0); // - property id - - stream.Write(TemplateID); // - template id - stream.Write(ZoneId); // - map id - stream.Write(VendorMapId); // - vendor map id - stream.Write(cloneId); // clone id - - const auto& name = GeneralUtils::UTF8ToUTF16(Name); - stream.Write(name.size()); - for (uint32_t i = 0; i < name.size(); ++i) { - stream.Write(name[i]); - } - - const auto& description = GeneralUtils::UTF8ToUTF16(Description); - stream.Write(description.size()); - for (uint32_t i = 0; i < description.size(); ++i) { - stream.Write(description[i]); - } - - const auto& owner = GeneralUtils::UTF8ToUTF16(OwnerName); - stream.Write(owner.size()); - for (uint32_t i = 0; i < owner.size(); ++i) { - stream.Write(owner[i]); - } - - stream.Write(OwnerId); - - stream.Write(0); // - type - stream.Write(0); // - zone code - stream.Write(0); // - minimum price - stream.Write(1); // - rent duration - - stream.Write(LastUpdatedTime); // - timestamp - - stream.Write(1); - - stream.Write(reputation); // Reputation - stream.Write(0); - - const auto& spawn = GeneralUtils::ASCIIToUTF16(SpawnName); - stream.Write(spawn.size()); - for (uint32_t i = 0; i < spawn.size(); ++i) { - stream.Write(spawn[i]); - } - - stream.Write(0); // String length - stream.Write(0); // String length - - stream.Write(0); // - duration type - stream.Write(1); - stream.Write(1); - - stream.Write(PrivacyOption); - - stream.Write(0); - - if (rejectionReason != "") stream.Write(REJECTION_STATUS_REJECTED); - else if (moderatorRequested == true && rejectionReason == "") stream.Write(REJECTION_STATUS_APPROVED); - else stream.Write(REJECTION_STATUS_PENDING); - - // Does this go here??? - // const auto& rejectionReasonConverted = GeneralUtils::UTF8ToUTF16(rejectionReason); - // stream.Write(rejectionReasonConverted.size()); - // for (uint32_t i = 0; i < rejectionReasonConverted.size(); ++i) { - // stream.Write(rejectionReasonConverted[i]); - // } - - stream.Write(0); - - stream.Write(0); - - stream.Write(1); - stream.Write(1); - - stream.Write(ZonePosition.x); - stream.Write(ZonePosition.y); - stream.Write(ZonePosition.z); - - stream.Write(MaxBuildHeight); - - stream.Write(ClaimedTime); // - timestamp - - stream.Write(PrivacyOption); - - stream.Write(Paths.size()); - - for (const auto& path : Paths) { - stream.Write(path.x); - stream.Write(path.y); - stream.Write(path.z); - } -} - -GameMessages::PropertyDataMessage::PropertyDataMessage(uint32_t mapID) { - const auto propertyTemplate = CDClientManager::GetTable()->GetByMapID(mapID); - - TemplateID = propertyTemplate.id; - ZoneId = propertyTemplate.mapID; - VendorMapId = propertyTemplate.vendorMapID; - SpawnName = propertyTemplate.spawnName; -} diff --git a/dGame/dGameMessages/PropertyDataMessage.h b/dGame/dGameMessages/PropertyDataMessage.h deleted file mode 100644 index f7c9e2d59..000000000 --- a/dGame/dGameMessages/PropertyDataMessage.h +++ /dev/null @@ -1,49 +0,0 @@ -#pragma once - -#include -#include - -#include "BitStream.h" - -#include "NiPoint3.h" -#include "dCommonVars.h" - -namespace GameMessages { - class PropertyDataMessage final - { - public: - explicit PropertyDataMessage(uint32_t mapID); - - void Serialize(RakNet::BitStream& stream) const; - - std::string OwnerName = ""; - LWOOBJID OwnerId = LWOOBJID_EMPTY; - - // Temporary values - uint32_t TemplateID = 25166; - uint16_t ZoneId = 1150; - uint16_t VendorMapId = 1100; - std::string SpawnName = "AGSmallProperty"; - - std::string Name = ""; - std::string Description = ""; - std::string rejectionReason = ""; - - bool moderatorRequested = 0; - LWOCLONEID cloneId = 0; - uint32_t reputation = 0; - uint64_t ClaimedTime = 0; - uint64_t LastUpdatedTime = 0; - - NiPoint3 ZonePosition = { 548.0f, 406.0f, 178.0f }; - char PrivacyOption = 0; - float MaxBuildHeight = 128.0f; - std::vector Paths = {}; - private: - enum RejectionStatus : uint32_t { - REJECTION_STATUS_APPROVED = 0, - REJECTION_STATUS_PENDING = 1, - REJECTION_STATUS_REJECTED = 2 - }; - }; -} diff --git a/dGame/dGameMessages/PropertyMessages.cpp b/dGame/dGameMessages/PropertyMessages.cpp new file mode 100644 index 000000000..4ee41e004 --- /dev/null +++ b/dGame/dGameMessages/PropertyMessages.cpp @@ -0,0 +1,741 @@ +#include "PropertyMessages.h" + +#include "Amf3.h" +#include "AMFDeserialize.h" +#include "AmfSerialize.h" +#include "BitStreamUtils.h" +#include "CDClientManager.h" +#include "CDPropertyEntranceComponentTable.h" +#include "CDPropertyTemplateTable.h" +#include "Character.h" +#include "ControlBehaviors.h" +#include "Database.h" +#include "dZoneManager.h" +#include "eReplicaComponentType.h" +#include "Entity.h" +#include "EntityManager.h" +#include "Game.h" +#include "GeneralUtils.h" +#include "HotPropertySlots.h" +#include "Logger.h" +#include "MultiZoneEntranceComponent.h" +#include "PlayerManager.h" +#include "PlayerReports.h" +#include "PropertyEntranceComponent.h" +#include "PropertyManagementComponent.h" +#include "PropertyVendorComponent.h" +#include "User.h" +#include "UserManager.h" + +#include + +namespace { + using HotPropertyInfo = GameMessages::NewsSendHotPropertiesInfoToClient::HotPropertyInfo; + + // The news screen's slots and the property worlds they can show properties of, from the CDClient (see HotPropertySlots.h) + struct NewsWorlds { + std::vector slots; + std::vector worlds; + }; + + const NewsWorlds& GetNewsWorlds() { + static const auto news = [] { + std::vector templates; + for (const auto& row : CDClientManager::GetTable()->GetEntries()) templates.push_back({ row.id, row.mapID, row.spawnName }); + std::vector entrances; + for (const auto& row : CDClientManager::GetTable()->GetEntries()) entrances.push_back({ row.mapID, row.propertyName }); + return NewsWorlds{ HotPropertySlots::ResolveSlots(templates, entrances), HotPropertySlots::PropertyWorlds(templates, entrances) }; + }(); + return news; + } + + HotPropertyInfo ToHotProperty(const IProperty::Info& info, const std::string& ownerName, uint32_t templateId) { + HotPropertyInfo hot; + hot.propertyId = info.id; + hot.ownerId = info.ownerId; + hot.ownerName = GeneralUtils::UTF8ToUTF16(ownerName); + hot.reputation = info.reputation; + hot.templateId = static_cast(templateId); + hot.name = GeneralUtils::UTF8ToUTF16(info.name); // empty: the client shows the template's name + hot.description = GeneralUtils::UTF8ToUTF16(info.description); + hot.performanceCost = info.performanceCost; + hot.lastPublished = info.lastUpdatedTime; + hot.cloneId = info.cloneId; + return hot; + } + + // What each slot shows, as chosen on the dashboard (featured_properties, featured_properties_settings), resolved by + // HotPropertySlots::Resolve so no property is shown twice. Every entry carries its slot's template id, whatever + // world the property is on: that is what puts it in the slot. Empty slots are left out. + std::vector LoadHotProperties() { + const auto& news = GetNewsWorlds(); + const bool fullAuto = Database::Get()->GetFeaturedPropertiesSettings().fullAuto; + std::map chosen; + for (const auto& row : Database::Get()->GetFeaturedPropertySlots()) chosen[row.templateId] = row; + + std::vector choices; + for (const auto& slot : news.slots) { + const auto it = chosen.find(slot.templateId); + HotPropertySlots::Choice choice{ HotPropertySlots::eMode::AUTO, slot.mapId }; + if (it != chosen.end()) choice = { HotPropertySlots::ModeFromInt(it->second.mode), HotPropertySlots::Location(it->second.zoneId, slot, news.worlds), it->second.propertyId }; + choices.push_back(choice); + } + + // The candidates, with what to send for each + std::vector candidates; + std::map> properties; + for (const auto world : HotPropertySlots::CandidateWorlds(choices, fullAuto)) { + IProperty::ShowcaseQuery query; + query.zoneId = world; + query.sort = IProperty::ShowcaseSort::REPUTATION; + query.length = HotPropertySlots::CANDIDATES_PER_WORLD; + for (const auto& entry : Database::Get()->GetShowcaseProperties(query).entries) { + candidates.push_back({ entry.info.id, entry.info.zoneId, entry.info.reputation }); + properties.emplace(entry.info.id, std::make_pair(entry.info, entry.ownerName)); + } + } + for (const auto& choice : choices) { + if (fullAuto || choice.mode != HotPropertySlots::eMode::PICKED || properties.contains(choice.propertyId)) continue; + const auto info = Database::Get()->GetPropertyInfo(choice.propertyId); + const auto owner = info ? Database::Get()->GetCharacterInfo(info->ownerId) : std::nullopt; + if (!info || !owner || !HotPropertySlots::Featurable(info->modApproved, info->privacyOption, info->zoneId, choice.mapId)) continue; + candidates.push_back({ info->id, info->zoneId, info->reputation }); + properties.emplace(info->id, std::make_pair(*info, owner->name)); + } + + std::vector hot; + const auto showing = HotPropertySlots::Resolve(choices, fullAuto, candidates); + for (size_t i = 0; i < showing.size(); i++) { + if (!showing[i].propertyId) continue; + const auto& [info, ownerName] = properties.at(*showing[i].propertyId); + hot.push_back(ToHotProperty(info, ownerName, news.slots[i].templateId)); + } + return hot; + } +} + +namespace GameMessages { + void DownloadPropertyData::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(propertyId); + bitStream.Write(templateId); + bitStream.Write(mapId); + bitStream.Write(vendorMapId); + bitStream.Write(cloneId); + BitStreamUtils::WriteLengthPrefixed(bitStream, name); + BitStreamUtils::WriteLengthPrefixed(bitStream, description); + BitStreamUtils::WriteLengthPrefixed(bitStream, ownerName); + bitStream.Write(ownerId); + bitStream.Write(propertyType); + bitStream.Write(zoneCode); + bitStream.Write(rent); + bitStream.Write(rentalPeriod); + bitStream.Write(expirationDate); + bitStream.Write(rentAmount); + bitStream.Write(reputation); + BitStreamUtils::WriteLengthPrefixed(bitStream, spawnName); + BitStreamUtils::WriteLengthPrefixed(bitStream, templateName); + BitStreamUtils::WriteLengthPrefixed(bitStream, templateDescription); + bitStream.Write(rentDuration); + bitStream.Write(votes); + bitStream.Write(durationType); + bitStream.Write(renew); + bitStream.Write(ownerAccountID); + bitStream.Write(moderationStatus); + BitStreamUtils::WriteLengthPrefixed(bitStream, rejectionReason); + bitStream.Write(lastLogoutTime); + bitStream.Write(dayOfMonthPlaqueWasBought); + bitStream.Write(repAchievementReq); + bitStream.Write(zonePosition.x); + bitStream.Write(zonePosition.y); + bitStream.Write(zonePosition.z); + bitStream.Write(maxBuildHeight); + bitStream.Write(rentalDate); + bitStream.Write(accessType); + bitStream.Write(pathPositions.size()); + for (const auto& path : pathPositions) { + bitStream.Write(path.x); + bitStream.Write(path.y); + bitStream.Write(path.z); + } + } + + bool DownloadPropertyData::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(propertyId)); + VALIDATE_READ(bitStream.Read(templateId)); + VALIDATE_READ(bitStream.Read(mapId)); + VALIDATE_READ(bitStream.Read(vendorMapId)); + VALIDATE_READ(bitStream.Read(cloneId)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, name)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, description)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, ownerName)); + VALIDATE_READ(bitStream.Read(ownerId)); + VALIDATE_READ(bitStream.Read(propertyType)); + VALIDATE_READ(bitStream.Read(zoneCode)); + VALIDATE_READ(bitStream.Read(rent)); + VALIDATE_READ(bitStream.Read(rentalPeriod)); + VALIDATE_READ(bitStream.Read(expirationDate)); + VALIDATE_READ(bitStream.Read(rentAmount)); + VALIDATE_READ(bitStream.Read(reputation)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, spawnName)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, templateName)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, templateDescription)); + VALIDATE_READ(bitStream.Read(rentDuration)); + VALIDATE_READ(bitStream.Read(votes)); + VALIDATE_READ(bitStream.Read(durationType)); + VALIDATE_READ(bitStream.Read(renew)); + VALIDATE_READ(bitStream.Read(ownerAccountID)); + VALIDATE_READ(bitStream.Read(moderationStatus)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, rejectionReason)); + VALIDATE_READ(bitStream.Read(lastLogoutTime)); + VALIDATE_READ(bitStream.Read(dayOfMonthPlaqueWasBought)); + VALIDATE_READ(bitStream.Read(repAchievementReq)); + VALIDATE_READ(bitStream.Read(zonePosition.x)); + VALIDATE_READ(bitStream.Read(zonePosition.y)); + VALIDATE_READ(bitStream.Read(zonePosition.z)); + VALIDATE_READ(bitStream.Read(maxBuildHeight)); + VALIDATE_READ(bitStream.Read(rentalDate)); + VALIDATE_READ(bitStream.Read(accessType)); + uint32_t pathCount{}; + VALIDATE_READ(bitStream.Read(pathCount)); + if (static_cast(pathCount) * 96 > bitStream.GetNumberOfUnreadBits()) return false; + pathPositions.resize(pathCount); + for (auto& path : pathPositions) { + VALIDATE_READ(bitStream.Read(path.x)); + VALIDATE_READ(bitStream.Read(path.y)); + VALIDATE_READ(bitStream.Read(path.z)); + } + return true; + } + + void PropertyRentalResponse::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(cloneid); + bitStream.Write(code); + bitStream.Write(propertyID); + bitStream.Write(rentdue); + } + + bool PropertyRentalResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(cloneid)); + VALIDATE_READ(bitStream.Read(code)); + VALIDATE_READ(bitStream.Read(propertyID)); + VALIDATE_READ(bitStream.Read(rentdue)); + return true; + } + + void PropertySelectQuery::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(navOffset); + bitStream.Write(thereAreMore); + bitStream.Write(cloneId); + bitStream.Write(hasFeaturedProperty); + bitStream.Write(wasFriends); + bitStream.Write(properties.size()); + for (const auto& property : properties) { + bitStream.Write(property.cloneId); + BitStreamUtils::WriteLengthPrefixed(bitStream, property.ownerName); + BitStreamUtils::WriteLengthPrefixed(bitStream, property.name); + BitStreamUtils::WriteLengthPrefixed(bitStream, property.description); + bitStream.Write(property.reputation); + bitStream.Write(property.isBff); + bitStream.Write(property.isFriend); + bitStream.Write(property.isModApproved); + bitStream.Write(property.isAlt); + bitStream.Write(property.isOwned); + bitStream.Write(property.accessType); + bitStream.Write(property.dateLastPublished); + bitStream.Write(property.performanceCost); + } + } + + bool PropertySelectQuery::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(navOffset)); + VALIDATE_READ(bitStream.Read(thereAreMore)); + VALIDATE_READ(bitStream.Read(cloneId)); + VALIDATE_READ(bitStream.Read(hasFeaturedProperty)); + VALIDATE_READ(bitStream.Read(wasFriends)); + uint32_t count{}; + VALIDATE_READ(bitStream.Read(count)); + properties.clear(); + for (uint32_t i = 0; i < count; i++) { + auto& property = properties.emplace_back(); + VALIDATE_READ(bitStream.Read(property.cloneId)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, property.ownerName)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, property.name)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, property.description)); + VALIDATE_READ(bitStream.Read(property.reputation)); + VALIDATE_READ(bitStream.Read(property.isBff)); + VALIDATE_READ(bitStream.Read(property.isFriend)); + VALIDATE_READ(bitStream.Read(property.isModApproved)); + VALIDATE_READ(bitStream.Read(property.isAlt)); + VALIDATE_READ(bitStream.Read(property.isOwned)); + VALIDATE_READ(bitStream.Read(property.accessType)); + VALIDATE_READ(bitStream.Read(property.dateLastPublished)); + VALIDATE_READ(bitStream.Read(property.performanceCost)); + } + return true; + } + + void GetModelsOnProperty::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(models.size()); + for (const auto& [model, item] : models) { + bitStream.Write(model); + bitStream.Write(item); + } + } + + bool GetModelsOnProperty::Deserialize(RakNet::BitStream& bitStream) { + uint32_t count{}; + VALIDATE_READ(bitStream.Read(count)); + if (static_cast(count) * 128 > bitStream.GetNumberOfUnreadBits()) return false; + models.resize(count); + for (auto& [model, item] : models) { + VALIDATE_READ(bitStream.Read(model)); + VALIDATE_READ(bitStream.Read(item)); + } + return true; + } + + void PlaceModelResponse::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, position, NiPoint3Constant::ZERO); + BitStreamUtils::WriteOptional(bitStream, propertyPlaqueID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, response, 0); + // WIRE BUG (kept, see the struct): the value written after rotation's flag is response, not rotation. + bitStream.Write(rotation != QuatUtils::IDENTITY); + if (rotation != QuatUtils::IDENTITY) bitStream.Write(response); + } + + bool PlaceModelResponse::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, position, NiPoint3Constant::ZERO)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, propertyPlaqueID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, response, 0)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, rotation, QuatUtils::IDENTITY)); + return true; + } + + void HandleUGCEquipPreCreateBasedOnEditMode::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(modelCount); + bitStream.Write(modelID); + } + + bool HandleUGCEquipPreCreateBasedOnEditMode::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(modelCount)); + VALIDATE_READ(bitStream.Read(modelID)); + return true; + } + + void HandleUGCEquipPostDeleteBasedOnEditMode::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(invItem); + BitStreamUtils::WriteOptional(bitStream, itemsTotal, 0); + } + + bool HandleUGCEquipPostDeleteBasedOnEditMode::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(invItem)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, itemsTotal, 0)); + return true; + } + + void SetPropertyAccess::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, accessType, 0); + BitStreamUtils::WriteOptional(bitStream, renew, 0); + } + + bool SetPropertyAccess::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, accessType, 0)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, renew, 0)); + return true; + } + + void SetPropertyAccess::Handle(Entity& entity, const SystemAddress& sysAddr) { + LOG("Set privacy option to: %i", accessType); + + if (PropertyManagementComponent::Instance() == nullptr) return; + + PropertyManagementComponent::Instance()->SetPrivacyOption(static_cast(accessType)); + } + + void UpdatePropertyOrModelForFilterCheck::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(isProperty); + bitStream.Write(ugcId); + bitStream.Write(playerId); + bitStream.Write(worldId); + BitStreamUtils::WriteLengthPrefixed(bitStream, newDescription); + BitStreamUtils::WriteLengthPrefixed(bitStream, newName); + } + + bool UpdatePropertyOrModelForFilterCheck::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(isProperty)); + VALIDATE_READ(bitStream.Read(ugcId)); + VALIDATE_READ(bitStream.Read(playerId)); + VALIDATE_READ(bitStream.Read(worldId)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, newDescription)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, newName)); + return true; + } + + void UpdatePropertyOrModelForFilterCheck::Handle(Entity& entity, const SystemAddress& sysAddr) { + PropertyManagementComponent::Instance()->UpdatePropertyDetails(GeneralUtils::UTF16ToWTF8(newName), GeneralUtils::UTF16ToWTF8(newDescription)); + } + + void QueryPropertyData::Handle(Entity& entity, const SystemAddress& sysAddr) { + LOG("Entity (%i) requesting data", entity.GetLOT()); + + auto* propertyVendorComponent = static_cast(entity.GetComponent(eReplicaComponentType::PROPERTY_VENDOR)); + + if (propertyVendorComponent != nullptr) { + propertyVendorComponent->OnQueryPropertyData(&entity, sysAddr); + } + + auto* propertyManagerComponent = static_cast(entity.GetComponent(eReplicaComponentType::PROPERTY_MANAGEMENT)); + + if (propertyManagerComponent != nullptr) { + propertyManagerComponent->OnQueryPropertyData(&entity, sysAddr); + } + } + + void PropertyEditorBegin::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, distanceType, 0); + BitStreamUtils::WriteOptional(bitStream, propertyObjectID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, startMode, 1); + bitStream.Write(startPaused); + } + + bool PropertyEditorBegin::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, distanceType, 0)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, propertyObjectID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, startMode, 1)); + VALIDATE_READ(bitStream.Read(startPaused)); + return true; + } + + void PropertyEditorBegin::Handle(Entity& entity, const SystemAddress& sysAddr) { + PropertyManagementComponent::Instance()->OnStartBuilding(); + + Game::zoneManager->GetZoneControlObject()->OnZonePropertyEditBegin(); + } + + void PropertyEditorEnd::Handle(Entity& entity, const SystemAddress& sysAddr) { + PropertyManagementComponent::Instance()->OnFinishBuilding(); + + Game::zoneManager->GetZoneControlObject()->OnZonePropertyEditEnd(); + } + + void PropertyContentsFromClient::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(queryDB); + } + + bool PropertyContentsFromClient::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(queryDB)); + return true; + } + + void PropertyContentsFromClient::Handle(Entity& entity, const SystemAddress& sysAddr) { + User* user = UserManager::Instance()->GetUser(sysAddr); + + Entity* player = Game::entityManager->GetEntity(user->GetLoggedInChar()); + + const auto& models = PropertyManagementComponent::Instance()->GetModels(); + GetModelsOnProperty modelsOnProperty; + modelsOnProperty.target = player->GetObjectID(); + modelsOnProperty.models = { models.begin(), models.end() }; + LOG("Sending property models to (%llu) (%d)", modelsOnProperty.target, true); + modelsOnProperty.Send(UNASSIGNED_SYSTEM_ADDRESS); + } + + void ZonePropertyModelEquipped::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, playerID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, propertyID, LWOOBJID_EMPTY); + } + + bool ZonePropertyModelEquipped::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, playerID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, propertyID, LWOOBJID_EMPTY)); + return true; + } + + void ZonePropertyModelEquipped::Handle(Entity& entity, const SystemAddress& sysAddr) { + Game::zoneManager->GetZoneControlObject()->OnZonePropertyModelEquipped(); + } + + void ZonePropertyModelRotated::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, playerID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, propertyID, LWOOBJID_EMPTY); + } + + bool ZonePropertyModelRotated::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, playerID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, propertyID, LWOOBJID_EMPTY)); + return true; + } + + void ZonePropertyModelRotated::Handle(Entity& entity, const SystemAddress& sysAddr) { + User* usr = UserManager::Instance()->GetUser(sysAddr); + Game::entityManager->GetZoneControlEntity()->OnZonePropertyModelRotated(usr->GetLastUsedChar()->GetEntity()); + } + + void PlacePropertyModel::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(modelID); + } + + bool PlacePropertyModel::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(modelID)); + return true; + } + + void PlacePropertyModel::Handle(Entity& entity, const SystemAddress& sysAddr) { + PropertyManagementComponent::Instance()->UpdateModelPosition(modelID, NiPoint3Constant::ZERO, QuatUtils::IDENTITY); + } + + void UpdateModelFromClient::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(modelID); + bitStream.Write(position); + BitStreamUtils::WriteOptional(bitStream, rotation, QuatUtils::IDENTITY); + } + + bool UpdateModelFromClient::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(modelID)); + VALIDATE_READ(bitStream.Read(position)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, rotation, QuatUtils::IDENTITY)); + return true; + } + + void UpdateModelFromClient::Handle(Entity& entity, const SystemAddress& sysAddr) { + PropertyManagementComponent::Instance()->UpdateModelPosition(modelID, position, rotation); + } + + void DeleteModelFromClient::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, modelID, LWOOBJID_EMPTY); + BitStreamUtils::WriteOptional(bitStream, reason, 0); + } + + bool DeleteModelFromClient::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, modelID, LWOOBJID_EMPTY)); + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, reason, 0)); + return true; + } + + void DeleteModelFromClient::Handle(Entity& entity, const SystemAddress& sysAddr) { + PropertyManagementComponent::Instance()->DeleteModel(modelID, reason); + } + + void PropertyEntranceSync::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(includeNullAddress); + bitStream.Write(includeNullDescription); + bitStream.Write(playersOwn); + bitStream.Write(updateUI); + bitStream.Write(numResults); + bitStream.Write(reputationTime); + bitStream.Write(sortMethod); + bitStream.Write(startIndex); + BitStreamUtils::WriteLengthPrefixed(bitStream, filterText); + } + + bool PropertyEntranceSync::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(includeNullAddress)); + VALIDATE_READ(bitStream.Read(includeNullDescription)); + VALIDATE_READ(bitStream.Read(playersOwn)); + VALIDATE_READ(bitStream.Read(updateUI)); + VALIDATE_READ(bitStream.Read(numResults)); + VALIDATE_READ(bitStream.Read(reputationTime)); + VALIDATE_READ(bitStream.Read(sortMethod)); + VALIDATE_READ(bitStream.Read(startIndex)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, filterText)); + return true; + } + + void PropertyEntranceSync::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* player = PlayerManager::GetPlayer(sysAddr); + + auto* entranceComponent = entity.GetComponent(); + + if (entranceComponent == nullptr) return; + + entranceComponent->OnPropertyEntranceSync(player, + includeNullAddress, + includeNullDescription, + playersOwn, + updateUI, + numResults, + reputationTime, + sortMethod, + startIndex, + filterText, + sysAddr + ); + } + + void EnterProperty1::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(index); + bitStream.Write(returnToZone); + } + + bool EnterProperty1::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(bitStream.Read(index)); + VALIDATE_READ(bitStream.Read(returnToZone)); + return true; + } + + void EnterProperty1::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* player = PlayerManager::GetPlayer(sysAddr); + + auto* entranceComponent = entity.GetComponent(); + if (entranceComponent != nullptr) { + entranceComponent->OnEnterProperty(player, static_cast(index), returnToZone, sysAddr); + return; + } + + auto multiZoneEntranceComponent = entity.GetComponent(); + if (multiZoneEntranceComponent != nullptr) { + multiZoneEntranceComponent->OnSelectWorld(player, static_cast(index)); + } + } + + void UpdatePropertyPerformanceCost::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteOptional(bitStream, performanceCost, 0.0f); + } + + bool UpdatePropertyPerformanceCost::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadOptional(bitStream, performanceCost, 0.0f)); + return true; + } + + void UpdatePropertyPerformanceCost::Handle(Entity& entity, const SystemAddress& sysAddr) { + if (performanceCost == 0.0f) return; + + auto zone = Game::zoneManager->GetZone(); + if (!zone) { + LOG("If you see this message, something is very wrong."); + return; + } + + const auto* const propertyManagementComponent = entity.GetComponent(); + const auto* const ownerEntity = propertyManagementComponent ? propertyManagementComponent->GetOwner() : nullptr; + const auto* const character = ownerEntity ? ownerEntity->GetCharacter() : nullptr; + const auto& zoneID = zone->GetZoneID(); + if (character && character->GetPropertyCloneID() == zoneID.GetCloneID()) { + Database::Get()->UpdatePerformanceCost(zoneID, performanceCost); + } + } + + void ReportOffensiveModel::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteLengthPrefixed(bitStream, description); + bitStream.Write(offensiveObjectID); + } + + bool ReportOffensiveModel::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, description)); + VALIDATE_READ(bitStream.Read(offensiveObjectID)); + return true; + } + + void ReportOffensiveModel::Handle(Entity& entity, const SystemAddress& sysAddr) { + PlayerReports::ReportOffensiveModel(&entity, description, offensiveObjectID); + } + + void ReportOffensiveProperty::Serialize(RakNet::BitStream& bitStream) const { + BitStreamUtils::WriteLengthPrefixed(bitStream, description); + bitStream.Write(propertyPlaqueObjectID); + } + + bool ReportOffensiveProperty::Deserialize(RakNet::BitStream& bitStream) { + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, description)); + VALIDATE_READ(bitStream.Read(propertyPlaqueObjectID)); + return true; + } + + void ReportOffensiveProperty::Handle(Entity& entity, const SystemAddress& sysAddr) { + PlayerReports::ReportOffensiveProperty(&entity, description, propertyPlaqueObjectID); + } + + ControlBehaviors::ControlBehaviors() : NetGameMsg(MessageType::Game::CONTROL_BEHAVIORS) {} + ControlBehaviors::~ControlBehaviors() = default; + + void ControlBehaviors::Serialize(RakNet::BitStream& bitStream) const { + AMFArrayValue empty; + bitStream.Write(args ? *args : empty); + BitStreamUtils::WriteLengthPrefixed(bitStream, command); + } + + bool ControlBehaviors::Deserialize(RakNet::BitStream& bitStream) { + AMFDeserialize reader; + try { + auto deserializedData = reader.Read(bitStream); + if (!deserializedData || deserializedData->GetValueType() != eAmf::Array) { + LOG("Failed to deserialize AMF data for control behaviors command: not an array"); + return false; + } + + args.reset(static_cast(deserializedData.release())); + } catch (...) { + LOG("Failed to deserialize AMF data for control behaviors command"); + return false; + } + + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, command)); // Prevent DoS via unbounded command buffer + return true; + } + + void ControlBehaviors::Handle(Entity& entity, const SystemAddress& sysAddr) { + auto* const owner = PropertyManagementComponent::Instance()->GetOwner(); + if (!owner) return; + + ::ControlBehaviors::Instance().ProcessCommand(&entity, *args, command, owner); + } + + void GetHotPropertyData::Handle(Entity& entity, const SystemAddress& sysAddr) { + // The screen asks every time it opens; the answer is the same for everyone, so it is kept for a little while + static std::vector cached; + static std::chrono::steady_clock::time_point loadedAt{}; + static bool loaded = false; + const auto now = std::chrono::steady_clock::now(); + if (!loaded || now - loadedAt > std::chrono::seconds(30)) { + try { + cached = LoadHotProperties(); + } catch (const std::exception& ex) { + LOG("Failed to load the news screen's top properties: %s", ex.what()); + cached.clear(); + } + loadedAt = now; + loaded = true; + } + + NewsSendHotPropertiesInfoToClient message; + message.target = entity.GetObjectID(); + for (const auto index : HotPropertySlots::NewsOrder(cached.size())) message.properties.push_back(cached[index]); + message.Send(sysAddr); + } + + void NewsSendHotPropertiesInfoToClient::Serialize(RakNet::BitStream& bitStream) const { + bitStream.Write(properties.size()); + for (const auto& info : properties) { + bitStream.Write(info.propertyId); + bitStream.Write(info.ownerId); + BitStreamUtils::WriteLengthPrefixed(bitStream, info.ownerName); + bitStream.Write(info.reputation); + bitStream.Write(info.templateId); + BitStreamUtils::WriteLengthPrefixed(bitStream, info.name); + BitStreamUtils::WriteLengthPrefixed(bitStream, info.description); + bitStream.Write(info.performanceCost); + bitStream.Write(info.lastPublished); + bitStream.Write(info.cloneId); + } + } + + bool NewsSendHotPropertiesInfoToClient::Deserialize(RakNet::BitStream& bitStream) { + uint32_t count{}; + VALIDATE_READ(bitStream.Read(count)); + properties.clear(); + for (uint32_t i = 0; i < count; i++) { + auto& info = properties.emplace_back(); + VALIDATE_READ(bitStream.Read(info.propertyId)); + VALIDATE_READ(bitStream.Read(info.ownerId)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, info.ownerName)); + VALIDATE_READ(bitStream.Read(info.reputation)); + VALIDATE_READ(bitStream.Read(info.templateId)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, info.name)); + VALIDATE_READ(BitStreamUtils::ReadLengthPrefixed(bitStream, info.description)); + VALIDATE_READ(bitStream.Read(info.performanceCost)); + VALIDATE_READ(bitStream.Read(info.lastPublished)); + VALIDATE_READ(bitStream.Read(info.cloneId)); + } + return true; + } +} diff --git a/dGame/dGameMessages/PropertyMessages.h b/dGame/dGameMessages/PropertyMessages.h new file mode 100644 index 000000000..24f847d86 --- /dev/null +++ b/dGame/dGameMessages/PropertyMessages.h @@ -0,0 +1,398 @@ +#ifndef PROPERTYMESSAGES_H +#define PROPERTYMESSAGES_H + +#include "GameMessages.h" + +#include "NiQuaternion.h" + +#include +#include +#include +#include + +class AMFArrayValue; + +// Game messages for properties: renting, the property data, browsing and entering properties, placing and editing +// models, property behaviors, reports and the news screen's top properties. +// Field names follow the client (legouniverse.exe 1.10.64); fields are listed in wire order. +// Messages received from a client hand their work to the property components (PropertyManagementComponent, +// PropertyVendorComponent, PropertyEntranceComponent, MultiZoneEntranceComponent) as the old handlers did. +namespace GameMessages { + // Server -> client. No payload. + struct OpenPropertyVendor : public NetGameMsg { + OpenPropertyVendor() : NetGameMsg(MessageType::Game::OPEN_PROPERTY_VENDOR) {} + }; + + // Server -> client. No payload. Targets the property management object. + struct OpenPropertyManagement : public NetGameMsg { + OpenPropertyManagement() : NetGameMsg(MessageType::Game::OPEN_PROPERTY_MANAGEMENT) {} + }; + + // Server -> client. The client reads it with PropertyData::Deserialize (0x00c084f0). + struct DownloadPropertyData : public NetGameMsg { + DownloadPropertyData() : NetGameMsg(MessageType::Game::DOWNLOAD_PROPERTY_DATA) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + // PropertyData::moderationStatus + enum RejectionStatus : uint32_t { + REJECTION_STATUS_APPROVED = 0, + REJECTION_STATUS_PENDING = 1, + REJECTION_STATUS_REJECTED = 2 + }; + + LWOOBJID propertyId{}; + int32_t templateId{}; + uint16_t mapId{}; + uint16_t vendorMapId{}; + uint32_t cloneId{}; + std::u16string name{}; + std::u16string description{}; + std::u16string ownerName{}; + LWOOBJID ownerId{}; + uint32_t propertyType{}; + uint32_t zoneCode{}; + uint32_t rent{}; + uint32_t rentalPeriod{}; + uint64_t expirationDate{}; + uint32_t rentAmount{}; + uint64_t reputation{}; + std::u16string spawnName{}; + std::u16string templateName{}; + std::u16string templateDescription{}; + uint32_t rentDuration{}; + uint32_t votes{}; + uint32_t durationType{}; + uint8_t renew{}; + LWOOBJID ownerAccountID{}; + uint32_t moderationStatus{}; + std::u16string rejectionReason{}; + uint64_t lastLogoutTime{}; + uint32_t dayOfMonthPlaqueWasBought{}; + uint32_t repAchievementReq{}; + NiPoint3 zonePosition{}; + float maxBuildHeight{}; + uint64_t rentalDate{}; + uint8_t accessType{}; + std::vector pathPositions{}; // u32 count, then the points + }; + + // Server -> client. + struct PropertyRentalResponse : public NetGameMsg { + PropertyRentalResponse() : NetGameMsg(MessageType::Game::PROPERTY_RENTAL_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + LWOCLONEID cloneid{}; + uint32_t code{}; + LWOOBJID propertyID{}; + int64_t rentdue{}; + }; + + // Server -> client. No payload. Opens the property browser of a property launch pad. + struct PropertyEntranceBegin : public NetGameMsg { + PropertyEntranceBegin() : NetGameMsg(MessageType::Game::PROPERTY_ENTRANCE_BEGIN) {} + }; + + // Server -> client. One page of the property browser. + struct PropertySelectQuery : public NetGameMsg { + PropertySelectQuery() : NetGameMsg(MessageType::Game::PROPERTY_SELECT_QUERY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + // One property, as the client's PropertyInfo. + struct PropertyInfo { + LWOCLONEID cloneId{ LWOCLONEID_INVALID }; + std::u16string ownerName{}; + std::u16string name{}; + std::u16string description{}; + float reputation{}; + bool isBff{}; + bool isFriend{}; + bool isModApproved{}; + bool isAlt{}; + bool isOwned{}; + uint32_t accessType{}; + uint64_t dateLastPublished{}; + float performanceCost{}; + }; + + int32_t navOffset{}; + bool thereAreMore{}; + int32_t cloneId{}; + bool hasFeaturedProperty{}; + bool wasFriends{}; + std::vector properties{}; // u32 count, then the entries + }; + + // Server -> client. Every model on the property: pairs of (model object, model item). + struct GetModelsOnProperty : public NetGameMsg { + GetModelsOnProperty() : NetGameMsg(MessageType::Game::GET_MODELS_ON_PROPERTY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + std::vector> models{}; // u32 count, then both IDs of each pair + }; + + // Server -> client. + // WIRE BUG (kept): DLU writes rotation's flag, but then writes response (4 bytes) instead of the quaternion + // (16 bytes) that the client's PlaceModelResponse::Deserialize (0x00dc0170) reads. Deserialize reads the + // client's layout, so a message with a non-identity rotation does not round trip. + struct PlaceModelResponse : public NetGameMsg { + PlaceModelResponse() : NetGameMsg(MessageType::Game::PLACE_MODEL_RESPONSE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + NiPoint3 position{ NiPoint3Constant::ZERO }; // optional + LWOOBJID propertyPlaqueID{ LWOOBJID_EMPTY }; // optional + int32_t response{}; // optional + NiQuaternion rotation{ QuatUtils::IDENTITY }; // optional + }; + + // Server -> client. Sent with id HANDLE_UGC_POST_CREATE_BASED_ON_EDIT_MODE (1301), which the client names + // HandleUGCEquipPreCreateBasedOnEditMode. + struct HandleUGCEquipPreCreateBasedOnEditMode : public NetGameMsg { + HandleUGCEquipPreCreateBasedOnEditMode() : NetGameMsg(MessageType::Game::HANDLE_UGC_POST_CREATE_BASED_ON_EDIT_MODE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + int32_t modelCount{}; + LWOOBJID modelID{}; + }; + + // Server -> client. + struct HandleUGCEquipPostDeleteBasedOnEditMode : public NetGameMsg { + HandleUGCEquipPostDeleteBasedOnEditMode() : NetGameMsg(MessageType::Game::HANDLE_UGC_POST_DELETE_BASED_ON_EDIT_MODE) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + LWOOBJID invItem{}; + int32_t itemsTotal{}; // optional + }; + + // Client -> server. + struct SetPropertyAccess : public NetGameMsg { + SetPropertyAccess() : NetGameMsg(MessageType::Game::SET_PROPERTY_ACCESS) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + uint8_t accessType{}; // optional + int32_t renew{}; // optional + }; + + // Client -> server. A new name and description for the property. + struct UpdatePropertyOrModelForFilterCheck : public NetGameMsg { + UpdatePropertyOrModelForFilterCheck() : NetGameMsg(MessageType::Game::UPDATE_PROPERTY_OR_MODEL_FOR_FILTER_CHECK) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool isProperty{}; + LWOOBJID ugcId{}; + LWOOBJID playerId{}; + LWOOBJID worldId{}; + std::u16string newDescription{}; + std::u16string newName{}; + }; + + // Client -> server. No payload. + struct QueryPropertyData : public NetGameMsg { + QueryPropertyData() : NetGameMsg(MessageType::Game::QUERY_PROPERTY_DATA) {} + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + }; + + // Client -> server. + struct PropertyEditorBegin : public NetGameMsg { + PropertyEditorBegin() : NetGameMsg(MessageType::Game::PROPERTY_EDITOR_BEGIN) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + int32_t distanceType{}; // optional + LWOOBJID propertyObjectID{ LWOOBJID_EMPTY }; // optional + int32_t startMode{ 1 }; // optional + bool startPaused{}; + }; + + // Client -> server. No payload. + struct PropertyEditorEnd : public NetGameMsg { + PropertyEditorEnd() : NetGameMsg(MessageType::Game::PROPERTY_EDITOR_END) {} + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + }; + + // Client -> server. + struct PropertyContentsFromClient : public NetGameMsg { + PropertyContentsFromClient() : NetGameMsg(MessageType::Game::PROPERTY_CONTENTS_FROM_CLIENT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool queryDB{}; + }; + + // Client -> server (DLU never sends it). + struct ZonePropertyModelEquipped : public NetGameMsg { + ZonePropertyModelEquipped() : NetGameMsg(MessageType::Game::ZONE_PROPERTY_MODEL_EQUIPPED) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID playerID{ LWOOBJID_EMPTY }; // optional + LWOOBJID propertyID{ LWOOBJID_EMPTY }; // optional + }; + + // Client -> server. + struct ZonePropertyModelRotated : public NetGameMsg { + ZonePropertyModelRotated() : NetGameMsg(MessageType::Game::ZONE_PROPERTY_MODEL_ROTATED) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID playerID{ LWOOBJID_EMPTY }; // optional + LWOOBJID propertyID{ LWOOBJID_EMPTY }; // optional + }; + + // Client -> server. + struct PlacePropertyModel : public NetGameMsg { + PlacePropertyModel() : NetGameMsg(MessageType::Game::PLACE_PROPERTY_MODEL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID modelID{}; + }; + + // Client -> server. + struct UpdateModelFromClient : public NetGameMsg { + UpdateModelFromClient() : NetGameMsg(MessageType::Game::UPDATE_MODEL_FROM_CLIENT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID modelID{}; + NiPoint3 position{}; + NiQuaternion rotation{ QuatUtils::IDENTITY }; // optional + }; + + // Client -> server. + struct DeleteModelFromClient : public NetGameMsg { + DeleteModelFromClient() : NetGameMsg(MessageType::Game::DELETE_MODEL_FROM_CLIENT) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + LWOOBJID modelID{ LWOOBJID_EMPTY }; // optional + int32_t reason{}; // optional + }; + + // Client -> server. Asks a property launch pad for a page of properties. + struct PropertyEntranceSync : public NetGameMsg { + PropertyEntranceSync() : NetGameMsg(MessageType::Game::PROPERTY_ENTRANCE_SYNC) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + bool includeNullAddress{}; + bool includeNullDescription{}; + bool playersOwn{}; + bool updateUI{}; + int32_t numResults{}; + int32_t reputationTime{}; + int32_t sortMethod{}; + int32_t startIndex{}; + std::string filterText{}; + }; + + // Client -> server. The player picked a property (or a world, on a multi zone entrance). + struct EnterProperty1 : public NetGameMsg { + EnterProperty1() : NetGameMsg(MessageType::Game::ENTER_PROPERTY1) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + int32_t index{}; + bool returnToZone{ true }; + }; + + // Client -> server. + struct UpdatePropertyPerformanceCost : public NetGameMsg { + UpdatePropertyPerformanceCost() : NetGameMsg(MessageType::Game::UPDATE_PROPERTY_PERFORMANCE_COST) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + float performanceCost{}; // optional + }; + + // Client -> server. + struct ReportOffensiveModel : public NetGameMsg { + ReportOffensiveModel() : NetGameMsg(MessageType::Game::REPORT_OFFENSIVE_MODEL) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + std::u16string description{}; + LWOOBJID offensiveObjectID{}; + }; + + // Client -> server. + struct ReportOffensiveProperty : public NetGameMsg { + ReportOffensiveProperty() : NetGameMsg(MessageType::Game::REPORT_OFFENSIVE_PROPERTY) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + std::u16string description{}; + LWOOBJID propertyPlaqueObjectID{}; + }; + + /** + * Client -> server. Every property behavior (model behavior editor) command. + * args is an AMF3 array; for AMF3 see https://rtmp.veriskope.com/pdf/amf3-file-format-spec.pdf + */ + struct ControlBehaviors : public NetGameMsg { + ControlBehaviors(); + ~ControlBehaviors() override; + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + + std::unique_ptr args{}; + std::string command{}; + }; + + // Client -> server. No payload. The news screen asks for today's top properties. + struct GetHotPropertyData : public NetGameMsg { + GetHotPropertyData() : NetGameMsg(MessageType::Game::GET_HOT_PROPERTY_DATA) {} + void Handle(Entity& entity, const SystemAddress& sysAddr) override; + }; + + /** + * Server -> client. The news screen's "Today's Top Properties" (GM 1510), the answer to GetHotPropertyData (GM 1511). + * Client: GameMessage::NewsSendHotPropertiesInfoToClient, entries read by NewsHotPropertyInfo::Deserialize + * (0x00c0cc20 in 1.10.64) and shown by LWOCharacterComponent::HotPropertyData (0x00cf83f0). See HotPropertySlots.h. + */ + struct NewsSendHotPropertiesInfoToClient : public NetGameMsg { + NewsSendHotPropertiesInfoToClient() : NetGameMsg(MessageType::Game::SEND_HOT_PROPERTY_DATA) {} + void Serialize(RakNet::BitStream& bitStream) const override; + bool Deserialize(RakNet::BitStream& bitStream) override; + + struct HotPropertyInfo { + LWOOBJID propertyId{}; + LWOOBJID ownerId{}; + std::u16string ownerName; + uint64_t reputation{}; + int32_t templateId{}; // PropertyTemplate id: picks the news screen slot + std::u16string name; + std::u16string description; + float performanceCost{}; + uint64_t lastPublished{}; // unix time + uint32_t cloneId{}; + }; + + std::vector properties; + }; +}; + +#endif // PROPERTYMESSAGES_H diff --git a/dGame/dGameMessages/PropertySelectQueryProperty.cpp b/dGame/dGameMessages/PropertySelectQueryProperty.cpp deleted file mode 100644 index d388a71ae..000000000 --- a/dGame/dGameMessages/PropertySelectQueryProperty.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "PropertySelectQueryProperty.h" - -void PropertySelectQueryProperty::Serialize(RakNet::BitStream& stream) const { - stream.Write(CloneId); - - const auto& owner = GeneralUtils::UTF8ToUTF16(OwnerName); - stream.Write(owner.size()); - for (uint32_t i = 0; i < owner.size(); ++i) { - stream.Write(owner[i]); - } - - const auto& name = GeneralUtils::UTF8ToUTF16(Name); - stream.Write(name.size()); - for (uint32_t i = 0; i < name.size(); ++i) { - stream.Write(name[i]); - } - - const auto& description = GeneralUtils::UTF8ToUTF16(Description); - stream.Write(description.size()); - for (uint32_t i = 0; i < description.size(); ++i) { - stream.Write(description[i]); - } - - stream.Write(Reputation); - stream.Write(IsBestFriend); - stream.Write(IsFriend); - stream.Write(IsModeratorApproved); - stream.Write(IsAlt); - stream.Write(IsOwned); - stream.Write(AccessType); - stream.Write(DateLastPublished); - stream.Write(PerformanceCost); -} - -void PropertySelectQueryProperty::Deserialize(RakNet::BitStream& stream) const { - // Do we need this? - // no -} diff --git a/dGame/dGameMessages/PropertySelectQueryProperty.h b/dGame/dGameMessages/PropertySelectQueryProperty.h deleted file mode 100644 index 85d389dd9..000000000 --- a/dGame/dGameMessages/PropertySelectQueryProperty.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#ifndef PROPERTYSELECTQUERY_H -#define PROPERTYSELECTQUERY_H - -#include "Entity.h" - -class PropertySelectQueryProperty final { -public: - void Serialize(RakNet::BitStream& stream) const; - - void Deserialize(RakNet::BitStream& stream) const; - - LWOCLONEID CloneId = LWOCLONEID_INVALID; // The cloneID of the property - std::string OwnerName = ""; // The property owners name - std::string Name = ""; // The property name - std::string Description = ""; // The property description - float Reputation = 0; // The reputation of the property - bool IsBestFriend = false; // Whether or not the property belongs to a best friend - bool IsFriend = false; // Whether or not the property belongs to a friend - bool IsModeratorApproved = false; // Whether or not a moderator has approved this property - bool IsAlt = false; // Whether or not the property is owned by an alt of the account owner - bool IsOwned = false; // Whether or not the property is owned - uint32_t AccessType = 0; // The privacy option of the property - uint64_t DateLastPublished = 0; // The last day the property was published - float PerformanceCost = 0; // The performance cost of the property -}; - -#endif diff --git a/dGame/dUtilities/PlayerReports.cpp b/dGame/dUtilities/PlayerReports.cpp index 47b1826e6..d4e6354d4 100644 --- a/dGame/dUtilities/PlayerReports.cpp +++ b/dGame/dUtilities/PlayerReports.cpp @@ -2,7 +2,6 @@ #include -#include "BitStream.h" #include "Database.h" #include "DashboardNotify.h" #include "Entity.h" @@ -59,21 +58,6 @@ namespace { LOG("Player report %llu (%s) from %llu about %llu", id, report.kind.c_str(), report.reporterId, report.targetCharacterId); DashboardNotify::Changed("player_reports"); } - - // Description (u32 length + UTF-16) followed by an object ID - bool ReadDescriptionAndObject(RakNet::BitStream& inStream, std::string& description, LWOOBJID& objectId) { - uint32_t length{}; - if (!inStream.Read(length) || length > MAX_BODY) return false; - std::u16string text; - text.reserve(length); - for (uint32_t i = 0; i < length; i++) { - char16_t character{}; - if (!inStream.Read(character)) return false; - text.push_back(character); - } - description = GeneralUtils::UTF16ToWTF8(text); - return inStream.Read(objectId); - } } namespace PlayerReports { @@ -87,10 +71,9 @@ namespace PlayerReports { Save(report); } - void HandleReportOffensiveModel(RakNet::BitStream& inStream, Entity* reporter) { - std::string description; - LWOOBJID objectId{}; - if (!ReadDescriptionAndObject(inStream, description, objectId)) return; + void ReportOffensiveModel(Entity* reporter, const std::u16string& text, LWOOBJID objectId) { + if (text.size() > MAX_BODY) return; + const auto description = GeneralUtils::UTF16ToWTF8(text); auto report = NewReport(reporter, ePlayerReportKind::MODEL, description); report.objectId = objectId; auto* object = Game::entityManager->GetEntity(objectId); @@ -106,10 +89,9 @@ namespace PlayerReports { Save(report); } - void HandleReportOffensiveProperty(RakNet::BitStream& inStream, Entity* reporter) { - std::string description; - LWOOBJID plaqueId{}; - if (!ReadDescriptionAndObject(inStream, description, plaqueId)) return; + void ReportOffensiveProperty(Entity* reporter, const std::u16string& text, LWOOBJID plaqueId) { + if (text.size() > MAX_BODY) return; + const auto description = GeneralUtils::UTF16ToWTF8(text); auto report = NewReport(reporter, ePlayerReportKind::PROPERTY, description); report.objectId = plaqueId; if (auto* plaque = Game::entityManager->GetEntity(plaqueId)) report.objectLot = plaque->GetLOT(); diff --git a/dGame/dUtilities/PlayerReports.h b/dGame/dUtilities/PlayerReports.h index a0c7dd573..c99b3fc05 100644 --- a/dGame/dUtilities/PlayerReports.h +++ b/dGame/dUtilities/PlayerReports.h @@ -10,7 +10,6 @@ #include "dCommonVars.h" class Entity; -namespace RakNet { class BitStream; } /** * Reports players send from the game's Report Abuse window (the help menu, or "Report" on another player's name), kept @@ -45,11 +44,11 @@ namespace PlayerReports { // ReportBug with nOtherPlayerID set; body is what the player wrote void ReportPlayer(Entity* reporter, LWOOBJID reportedId, const std::string& body); - // ReportOffensiveModel: description, then the object the player picked - void HandleReportOffensiveModel(RakNet::BitStream& inStream, Entity* reporter); + // ReportOffensiveModel: what the player wrote, and the object they picked. Dropped if the text is too long. + void ReportOffensiveModel(Entity* reporter, const std::u16string& description, LWOOBJID objectId); - // ReportOffensiveProperty: description, then the property's plaque - void HandleReportOffensiveProperty(RakNet::BitStream& inStream, Entity* reporter); + // ReportOffensiveProperty: what the player wrote, and the property's plaque. Dropped if the text is too long. + void ReportOffensiveProperty(Entity* reporter, const std::u16string& description, LWOOBJID plaqueId); } #endif //!__PLAYERREPORTS__H__ diff --git a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt index 6d0ebfca9..61ca78cfe 100644 --- a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt +++ b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt @@ -4,6 +4,7 @@ SET(DGAMEMESSAGES_TESTS "GameMsgSplitTests.cpp" "MissionMessagesTests.cpp" "PetMessagesTests.cpp" + "PropertyMessagesTests.cpp" "RacingMessagesTests.cpp" "UnSmashTests.cpp" "LegacyGameMessageTests.cpp" diff --git a/tests/dGameTests/dGameMessagesTests/HotPropertyMessageTests.cpp b/tests/dGameTests/dGameMessagesTests/HotPropertyMessageTests.cpp index 0fcc0a415..7eda908e0 100644 --- a/tests/dGameTests/dGameMessagesTests/HotPropertyMessageTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/HotPropertyMessageTests.cpp @@ -1,4 +1,4 @@ -#include "GameMessages.h" +#include "PropertyMessages.h" #include "BitStream.h" #include diff --git a/tests/dGameTests/dGameMessagesTests/Legacy/PropertyMessagesLegacy.h b/tests/dGameTests/dGameMessagesTests/Legacy/PropertyMessagesLegacy.h new file mode 100644 index 000000000..abdf92935 --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/Legacy/PropertyMessagesLegacy.h @@ -0,0 +1,589 @@ +#ifndef PROPERTYMESSAGESLEGACY_H +#define PROPERTYMESSAGESLEGACY_H + +// FROZEN ORACLE - DO NOT EDIT. +// Verbatim copies of the hand written code that PropertyMessages.h replaced (dGame/dGameMessages/GameMessages.cpp, +// PropertyDataMessage.{h,cpp} and PropertySelectQueryProperty.{h,cpp}, branched from origin/main 129199e4 plus the +// news screen's hot properties). Only the namespace changed. The Read* functions are the read sequences of the +// replaced GameMessages::Handle* functions (and PlayerReports' report readers). + +#include "BitStreamUtils.h" +#include "CDClientManager.h" +#include "CDPropertyTemplateTable.h" +#include "dCommonVars.h" +#include "dServer.h" +#include "Entity.h" +#include "Game.h" +#include "GeneralUtils.h" +#include "Logger.h" +#include "MessageType/Client.h" +#include "MessageType/Game.h" +#include "NiPoint3.h" +#include "NiQuaternion.h" +#include "PropertyManagementComponent.h" +#include "PropertyMessages.h" +#include "ServiceType.h" + +#include +#include +#include + +namespace LegacyGameMessages { + class PropertyDataMessage final + { + public: + explicit PropertyDataMessage(uint32_t mapID); + + void Serialize(RakNet::BitStream& stream) const; + + std::string OwnerName = ""; + LWOOBJID OwnerId = LWOOBJID_EMPTY; + + // Temporary values + uint32_t TemplateID = 25166; + uint16_t ZoneId = 1150; + uint16_t VendorMapId = 1100; + std::string SpawnName = "AGSmallProperty"; + + std::string Name = ""; + std::string Description = ""; + std::string rejectionReason = ""; + + bool moderatorRequested = 0; + LWOCLONEID cloneId = 0; + uint32_t reputation = 0; + uint64_t ClaimedTime = 0; + uint64_t LastUpdatedTime = 0; + + NiPoint3 ZonePosition = { 548.0f, 406.0f, 178.0f }; + char PrivacyOption = 0; + float MaxBuildHeight = 128.0f; + std::vector Paths = {}; + private: + enum RejectionStatus : uint32_t { + REJECTION_STATUS_APPROVED = 0, + REJECTION_STATUS_PENDING = 1, + REJECTION_STATUS_REJECTED = 2 + }; + }; + + inline void PropertyDataMessage::Serialize(RakNet::BitStream& stream) const { + stream.Write(0); // - property id + + stream.Write(TemplateID); // - template id + stream.Write(ZoneId); // - map id + stream.Write(VendorMapId); // - vendor map id + stream.Write(cloneId); // clone id + + const auto& name = GeneralUtils::UTF8ToUTF16(Name); + stream.Write(name.size()); + for (uint32_t i = 0; i < name.size(); ++i) { + stream.Write(name[i]); + } + + const auto& description = GeneralUtils::UTF8ToUTF16(Description); + stream.Write(description.size()); + for (uint32_t i = 0; i < description.size(); ++i) { + stream.Write(description[i]); + } + + const auto& owner = GeneralUtils::UTF8ToUTF16(OwnerName); + stream.Write(owner.size()); + for (uint32_t i = 0; i < owner.size(); ++i) { + stream.Write(owner[i]); + } + + stream.Write(OwnerId); + + stream.Write(0); // - type + stream.Write(0); // - zone code + stream.Write(0); // - minimum price + stream.Write(1); // - rent duration + + stream.Write(LastUpdatedTime); // - timestamp + + stream.Write(1); + + stream.Write(reputation); // Reputation + stream.Write(0); + + const auto& spawn = GeneralUtils::ASCIIToUTF16(SpawnName); + stream.Write(spawn.size()); + for (uint32_t i = 0; i < spawn.size(); ++i) { + stream.Write(spawn[i]); + } + + stream.Write(0); // String length + stream.Write(0); // String length + + stream.Write(0); // - duration type + stream.Write(1); + stream.Write(1); + + stream.Write(PrivacyOption); + + stream.Write(0); + + if (rejectionReason != "") stream.Write(REJECTION_STATUS_REJECTED); + else if (moderatorRequested == true && rejectionReason == "") stream.Write(REJECTION_STATUS_APPROVED); + else stream.Write(REJECTION_STATUS_PENDING); + + // Does this go here??? + // const auto& rejectionReasonConverted = GeneralUtils::UTF8ToUTF16(rejectionReason); + // stream.Write(rejectionReasonConverted.size()); + // for (uint32_t i = 0; i < rejectionReasonConverted.size(); ++i) { + // stream.Write(rejectionReasonConverted[i]); + // } + + stream.Write(0); + + stream.Write(0); + + stream.Write(1); + stream.Write(1); + + stream.Write(ZonePosition.x); + stream.Write(ZonePosition.y); + stream.Write(ZonePosition.z); + + stream.Write(MaxBuildHeight); + + stream.Write(ClaimedTime); // - timestamp + + stream.Write(PrivacyOption); + + stream.Write(Paths.size()); + + for (const auto& path : Paths) { + stream.Write(path.x); + stream.Write(path.y); + stream.Write(path.z); + } + } + + inline PropertyDataMessage::PropertyDataMessage(uint32_t mapID) { + const auto propertyTemplate = CDClientManager::GetTable()->GetByMapID(mapID); + + TemplateID = propertyTemplate.id; + ZoneId = propertyTemplate.mapID; + VendorMapId = propertyTemplate.vendorMapID; + SpawnName = propertyTemplate.spawnName; + } + + class PropertySelectQueryProperty final { + public: + void Serialize(RakNet::BitStream& stream) const; + + void Deserialize(RakNet::BitStream& stream) const; + + LWOCLONEID CloneId = LWOCLONEID_INVALID; // The cloneID of the property + std::string OwnerName = ""; // The property owners name + std::string Name = ""; // The property name + std::string Description = ""; // The property description + float Reputation = 0; // The reputation of the property + bool IsBestFriend = false; // Whether or not the property belongs to a best friend + bool IsFriend = false; // Whether or not the property belongs to a friend + bool IsModeratorApproved = false; // Whether or not a moderator has approved this property + bool IsAlt = false; // Whether or not the property is owned by an alt of the account owner + bool IsOwned = false; // Whether or not the property is owned + uint32_t AccessType = 0; // The privacy option of the property + uint64_t DateLastPublished = 0; // The last day the property was published + float PerformanceCost = 0; // The performance cost of the property + }; + + inline void PropertySelectQueryProperty::Serialize(RakNet::BitStream& stream) const { + stream.Write(CloneId); + + const auto& owner = GeneralUtils::UTF8ToUTF16(OwnerName); + stream.Write(owner.size()); + for (uint32_t i = 0; i < owner.size(); ++i) { + stream.Write(owner[i]); + } + + const auto& name = GeneralUtils::UTF8ToUTF16(Name); + stream.Write(name.size()); + for (uint32_t i = 0; i < name.size(); ++i) { + stream.Write(name[i]); + } + + const auto& description = GeneralUtils::UTF8ToUTF16(Description); + stream.Write(description.size()); + for (uint32_t i = 0; i < description.size(); ++i) { + stream.Write(description[i]); + } + + stream.Write(Reputation); + stream.Write(IsBestFriend); + stream.Write(IsFriend); + stream.Write(IsModeratorApproved); + stream.Write(IsAlt); + stream.Write(IsOwned); + stream.Write(AccessType); + stream.Write(DateLastPublished); + stream.Write(PerformanceCost); + } + + inline void PropertySelectQueryProperty::Deserialize(RakNet::BitStream& stream) const { + // Do we need this? + // no + } + + // GameMessages::NewsSendHotPropertiesInfoToClient::Serialize (was a member function; now takes the entries) + inline void SerializeHotProperties(RakNet::BitStream& stream, const std::vector& properties) { + const auto writeWString = [&stream](const std::u16string& text) { + stream.Write(text.size()); + for (const auto character : text) stream.Write(character); + }; + stream.Write(properties.size()); + for (const auto& info : properties) { + stream.Write(info.propertyId); + stream.Write(info.ownerId); + writeWString(info.ownerName); + stream.Write(info.reputation); + stream.Write(info.templateId); + writeWString(info.name); + writeWString(info.description); + stream.Write(info.performanceCost); + stream.Write(info.lastPublished); + stream.Write(info.cloneId); + } + } + + inline void SendOpenPropertyVendor(const LWOOBJID objectId, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::OPEN_PROPERTY_VENDOR); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendOpenPropertyManagment(const LWOOBJID objectId, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(PropertyManagementComponent::Instance()->GetParent()->GetObjectID()); + bitStream.Write(MessageType::Game::OPEN_PROPERTY_MANAGEMENT); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendDownloadPropertyData(const LWOOBJID objectId, const PropertyDataMessage& data, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::DOWNLOAD_PROPERTY_DATA); + + data.Serialize(bitStream); + + LOG("(%llu) sending property data (%d)", objectId, sysAddr == UNASSIGNED_SYSTEM_ADDRESS); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendPropertyRentalResponse(const LWOOBJID objectId, const LWOCLONEID cloneId, const uint32_t code, const LWOOBJID propertyId, const int64_t rentDue, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::PROPERTY_RENTAL_RESPONSE); + + bitStream.Write(cloneId); + bitStream.Write(code); + bitStream.Write(propertyId); + bitStream.Write(rentDue); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendGetModelsOnProperty(LWOOBJID objectId, std::map models, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::GET_MODELS_ON_PROPERTY); + + bitStream.Write(models.size()); + + for (const auto& pair : models) { + bitStream.Write(pair.first); + bitStream.Write(pair.second); + } + + LOG("Sending property models to (%llu) (%d)", objectId, sysAddr == UNASSIGNED_SYSTEM_ADDRESS); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendPlaceModelResponse(LWOOBJID objectId, const SystemAddress& sysAddr, NiPoint3 position, + LWOOBJID plaque, int32_t response, NiQuaternion rotation) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::PLACE_MODEL_RESPONSE); + + bitStream.Write(position != NiPoint3Constant::ZERO); + if (position != NiPoint3Constant::ZERO) { + bitStream.Write(position); + } + + bitStream.Write(plaque != LWOOBJID_EMPTY); + if (plaque != LWOOBJID_EMPTY) { + bitStream.Write(plaque); + } + + bitStream.Write(response != 0); + if (response != 0) { + bitStream.Write(response); + } + + bitStream.Write(rotation != QuatUtils::IDENTITY); + if (rotation != QuatUtils::IDENTITY) { + bitStream.Write(response); + } + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + + } + + inline void SendUGCEquipPreCreateBasedOnEditMode(LWOOBJID objectId, const SystemAddress& sysAddr, int modelCount, LWOOBJID model) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::HANDLE_UGC_POST_CREATE_BASED_ON_EDIT_MODE); + + bitStream.Write(modelCount); + bitStream.Write(model); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendUGCEquipPostDeleteBasedOnEditMode(LWOOBJID objectId, const SystemAddress& sysAddr, LWOOBJID inventoryItem, int itemTotal) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::HANDLE_UGC_POST_DELETE_BASED_ON_EDIT_MODE); + + bitStream.Write(inventoryItem); + + bitStream.Write(itemTotal != 0); + if (itemTotal != 0) { + bitStream.Write(itemTotal); + } + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendPropertyEntranceBegin(LWOOBJID objectId, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::PROPERTY_ENTRANCE_BEGIN); + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + inline void SendPropertySelectQuery(LWOOBJID objectId, int32_t navOffset, bool thereAreMore, int32_t cloneId, bool hasFeaturedProperty, bool wasFriends, const std::vector& entries, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(MessageType::Game::PROPERTY_SELECT_QUERY); + + bitStream.Write(navOffset); + bitStream.Write(thereAreMore); + bitStream.Write(cloneId); + bitStream.Write(hasFeaturedProperty); + bitStream.Write(wasFriends); + + bitStream.Write(entries.size()); + + for (auto& entry : entries) { + entry.Serialize(bitStream); + } + + if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST; + SEND_PACKET; + } + + // GameMessages::HandleSetPropertyAccess + struct LegacySetPropertyAccess { uint8_t accessType{}; int32_t renew{}; }; + inline LegacySetPropertyAccess ReadSetPropertyAccess(RakNet::BitStream& inStream) { + uint8_t accessType{}; + int32_t renew{}; + + bool accessTypeIsDefault{}; + inStream.Read(accessTypeIsDefault); + if (accessTypeIsDefault != 0) inStream.Read(accessType); + + bool renewIsDefault{}; + inStream.Read(renewIsDefault); + if (renewIsDefault != 0) inStream.Read(renew); + return { accessType, renew }; + } + + // GameMessages::HandleUpdatePropertyOrModelForFilterCheck + struct LegacyFilterCheck { bool isProperty{}; LWOOBJID objectId{}; LWOOBJID playerId{}; LWOOBJID worldId{}; std::u16string name; std::u16string description; bool rejected{}; }; + inline LegacyFilterCheck ReadUpdatePropertyOrModelForFilterCheck(RakNet::BitStream& inStream) { + bool isProperty{}; + LWOOBJID objectId{}; + LWOOBJID playerId{}; + LWOOBJID worldId{}; + uint32_t nameLength{}; + std::u16string name{}; + uint32_t descriptionLength{}; + std::u16string description{}; + + inStream.Read(isProperty); + inStream.Read(objectId); + inStream.Read(playerId); + inStream.Read(worldId); + + inStream.Read(descriptionLength); + if (descriptionLength > MAX_MESSAGE_LENGTH) return { .rejected = true }; + for (uint32_t i = 0; i < descriptionLength; ++i) { + uint16_t character; + inStream.Read(character); + description.push_back(character); + } + + inStream.Read(nameLength); + if (nameLength > MAX_MESSAGE_LENGTH) return { .rejected = true }; + for (uint32_t i = 0; i < nameLength; ++i) { + uint16_t character; + inStream.Read(character); + name.push_back(character); + } + return { isProperty, objectId, playerId, worldId, name, description, false }; + } + + // GameMessages::HandlePlacePropertyModel + inline LWOOBJID ReadPlacePropertyModel(RakNet::BitStream& inStream) { + LWOOBJID model; + + inStream.Read(model); + return model; + } + + // GameMessages::HandleUpdatePropertyModel + struct LegacyUpdatePropertyModel { LWOOBJID model{}; NiPoint3 position; NiQuaternion rotation; }; + inline LegacyUpdatePropertyModel ReadUpdatePropertyModel(RakNet::BitStream& inStream) { + LWOOBJID model; + NiPoint3 position; + NiQuaternion rotation = QuatUtils::IDENTITY; + + inStream.Read(model); + inStream.Read(position); + + if (inStream.ReadBit()) { + inStream.Read(rotation); + } + return { model, position, rotation }; + } + + // GameMessages::HandleDeletePropertyModel + struct LegacyDeletePropertyModel { LWOOBJID model{}; int deleteReason{}; }; + inline LegacyDeletePropertyModel ReadDeletePropertyModel(RakNet::BitStream& inStream) { + LWOOBJID model = LWOOBJID_EMPTY; + int deleteReason = 0; + + if (inStream.ReadBit()) { + inStream.Read(model); + } + + if (inStream.ReadBit()) { + inStream.Read(deleteReason); + + } + return { model, deleteReason }; + } + + // GameMessages::HandlePropertyEntranceSync + struct LegacyPropertyEntranceSync { + bool includeNullAddress{}; bool includeNullDescription{}; bool playerOwn{}; bool updateUi{}; + int32_t numResults{}; int32_t reputation{}; int32_t sortMethod{}; int32_t startIndex{}; std::string filterText{}; bool rejected{}; + }; + inline LegacyPropertyEntranceSync ReadPropertyEntranceSync(RakNet::BitStream& inStream) { + bool includeNullAddress{}; + bool includeNullDescription{}; + bool playerOwn{}; + bool updateUi{}; + int32_t numResults{}; + int32_t reputation{}; + int32_t sortMethod{}; + int32_t startIndex{}; + uint32_t filterTextLength{}; + std::string filterText{}; + + inStream.Read(includeNullAddress); + inStream.Read(includeNullDescription); + inStream.Read(playerOwn); + inStream.Read(updateUi); + inStream.Read(numResults); + inStream.Read(reputation); + inStream.Read(sortMethod); + inStream.Read(startIndex); + inStream.Read(filterTextLength); + + if (filterTextLength > MAX_MESSAGE_LENGTH) return { .rejected = true }; + for (auto i = 0u; i < filterTextLength; i++) { + char c; + inStream.Read(c); + filterText.push_back(c); + } + return { includeNullAddress, includeNullDescription, playerOwn, updateUi, numResults, reputation, sortMethod, startIndex, filterText, false }; + } + + // GameMessages::HandleEnterProperty + struct LegacyEnterProperty { uint32_t index{}; bool returnToZone{}; }; + inline LegacyEnterProperty ReadEnterProperty(RakNet::BitStream& inStream) { + uint32_t index{}; + bool returnToZone{}; + + inStream.Read(index); + inStream.Read(returnToZone); + return { index, returnToZone }; + } + + // GameMessages::HandleUpdatePropertyPerformanceCost + inline float ReadUpdatePropertyPerformanceCost(RakNet::BitStream& inStream) { + float performanceCost = 0.0f; + + if (inStream.ReadBit()) inStream.Read(performanceCost); + return performanceCost; + } + + // PlayerReports' ReadDescriptionAndObject (ReportOffensiveModel / ReportOffensiveProperty), MAX_BODY = 2000 + constexpr uint32_t MAX_BODY = 2000; + // Description (u32 length + UTF-16) followed by an object ID + inline bool ReadDescriptionAndObject(RakNet::BitStream& inStream, std::string& description, LWOOBJID& objectId) { + uint32_t length{}; + if (!inStream.Read(length) || length > MAX_BODY) return false; + std::u16string text; + text.reserve(length); + for (uint32_t i = 0; i < length; i++) { + char16_t character{}; + if (!inStream.Read(character)) return false; + text.push_back(character); + } + description = GeneralUtils::UTF16ToWTF8(text); + return inStream.Read(objectId); + } +} + +#endif // PROPERTYMESSAGESLEGACY_H diff --git a/tests/dGameTests/dGameMessagesTests/PropertyMessagesTests.cpp b/tests/dGameTests/dGameMessagesTests/PropertyMessagesTests.cpp new file mode 100644 index 000000000..8bba893d7 --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/PropertyMessagesTests.cpp @@ -0,0 +1,609 @@ +#include "PropertyMessages.h" +#include "GameDependencies.h" +#include "GameMessageTestUtils.h" +#include "Legacy/PropertyMessagesLegacy.h" + +#include "Amf3.h" +#include "GeneralUtils.h" + +#include +#include +#include +#include +#include + +#include + +using namespace GameMessageTestUtils; + +namespace { + const std::vector g_Ints = { 0, 1, -1, 1727, std::numeric_limits::max(), std::numeric_limits::min() }; + // UTF-8 as the database holds it; the structs take UTF-16 converted the way the old code converted. + const std::vector g_Utf8 = { "", "Home", "Caf\xc3\xa9 \xe2\x98\x83 \xf0\x9f\x8f\xa0", std::string(300, 'x') }; + const std::vector g_Points = { NiPoint3Constant::ZERO, NiPoint3(1.5f, -2.0f, 1e6f) }; + const std::vector g_Rotations = { QuatUtils::IDENTITY, NiQuaternion(0.5f, 0.5f, -0.5f, 0.5f) }; + + PacketBytes Payload(const GameMessages::NetGameMsg& msg) { + RakNet::BitStream bitStream; + msg.Serialize(bitStream); + return FromBitStream(bitStream); + } + + // Serializes msg, then reads it with the legacy read sequence; both must consume exactly the same bits. + template + Result ReadWithLegacy(const GameMessages::NetGameMsg& msg, const std::function& read) { + RakNet::BitStream wire; + msg.Serialize(wire); + RakNet::BitStream legacyStream(wire.GetData(), wire.GetNumberOfBytesUsed(), false); + auto result = read(legacyStream); + EXPECT_EQ(legacyStream.GetReadOffset(), wire.GetNumberOfBitsUsed()); + return result; + } + + // The inputs PropertyManagementComponent::OnQueryPropertyData fills in. + struct PropertyDataInput { + uint32_t templateId{}; + uint32_t mapId{}; + uint32_t vendorMapId{}; + std::string spawnName; + bool moderatorRequested{}; + uint32_t reputation{}; + uint32_t lastUpdatedTime{}; + LWOOBJID ownerId{}; + std::string ownerName; + std::string name; + std::string description; + uint64_t claimed{}; + char privacy{}; + LWOCLONEID cloneId{}; + std::string rejectionReason; + std::vector paths; + }; + + // Builds the struct exactly as PropertyManagementComponent::OnQueryPropertyData does. + GameMessages::DownloadPropertyData Build(const PropertyDataInput& in, LWOOBJID author) { + GameMessages::DownloadPropertyData message; + message.target = author; + message.propertyId = 0; + message.templateId = static_cast(in.templateId); + message.mapId = static_cast(in.mapId); + message.vendorMapId = static_cast(in.vendorMapId); + message.cloneId = in.cloneId; + message.name = GeneralUtils::UTF8ToUTF16(in.name); + message.description = GeneralUtils::UTF8ToUTF16(in.description); + message.ownerName = GeneralUtils::UTF8ToUTF16(in.ownerName); + message.ownerId = in.ownerId; + message.propertyType = 0; + message.zoneCode = 0; + message.rent = 0; + message.rentalPeriod = 1; + message.expirationDate = in.lastUpdatedTime; + message.rentAmount = 1; + message.reputation = in.reputation; + message.spawnName = GeneralUtils::ASCIIToUTF16(in.spawnName); + message.rentDuration = 0; + message.votes = 1; + message.durationType = 1; + message.renew = static_cast(in.privacy); + message.ownerAccountID = 0; + if (in.rejectionReason != "") message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_REJECTED; + else if (in.moderatorRequested == true && in.rejectionReason == "") message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_APPROVED; + else message.moderationStatus = GameMessages::DownloadPropertyData::REJECTION_STATUS_PENDING; + message.lastLogoutTime = 0; + message.dayOfMonthPlaqueWasBought = 1; + message.repAchievementReq = 1; + message.zonePosition = { 548.0f, 406.0f, 178.0f }; + message.maxBuildHeight = 128.0f; + message.rentalDate = in.claimed; + message.accessType = static_cast(in.privacy); + message.pathPositions = in.paths; + return message; + } + + LegacyGameMessages::PropertyDataMessage BuildLegacy(const PropertyDataInput& in) { + LegacyGameMessages::PropertyDataMessage message(in.mapId); + message.TemplateID = in.templateId; + message.ZoneId = static_cast(in.mapId); + message.VendorMapId = static_cast(in.vendorMapId); + message.SpawnName = in.spawnName; + message.moderatorRequested = in.moderatorRequested; + message.reputation = in.reputation; + message.LastUpdatedTime = in.lastUpdatedTime; + message.OwnerId = in.ownerId; + message.OwnerName = in.ownerName; + message.Name = in.name; + message.Description = in.description; + message.ClaimedTime = in.claimed; + message.PrivacyOption = in.privacy; + message.cloneId = in.cloneId; + message.rejectionReason = in.rejectionReason; + message.Paths = in.paths; + return message; + } + + std::vector ToInfos(const std::vector& legacy) { + std::vector out; + for (const auto& entry : legacy) { + auto& info = out.emplace_back(); + info.cloneId = entry.CloneId; + info.ownerName = GeneralUtils::UTF8ToUTF16(entry.OwnerName); + info.name = GeneralUtils::UTF8ToUTF16(entry.Name); + info.description = GeneralUtils::UTF8ToUTF16(entry.Description); + info.reputation = entry.Reputation; + info.isBff = entry.IsBestFriend; + info.isFriend = entry.IsFriend; + info.isModApproved = entry.IsModeratorApproved; + info.isAlt = entry.IsAlt; + info.isOwned = entry.IsOwned; + info.accessType = entry.AccessType; + info.dateLastPublished = entry.DateLastPublished; + info.performanceCost = entry.PerformanceCost; + } + return out; + } +} + +class PropertyMessagesTests : public GameDependenciesTest { +protected: + void SetUp() override { SetUpDependencies(); } + void TearDown() override { TearDownDependencies(); } +}; + +TEST_F(PropertyMessagesTests, NoPayloadMessagesMatchLegacy) { + for (const auto target : g_Targets) { + GameMessages::OpenPropertyVendor vendor; + vendor.target = target; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendOpenPropertyVendor(target, a); }, vendor); + EXPECT_EQ(Payload(vendor).bits, 0); + + GameMessages::PropertyEntranceBegin begin; + begin.target = target; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPropertyEntranceBegin(target, a); }, begin); + EXPECT_EQ(Payload(begin).bits, 0); + } + + // The legacy SendOpenPropertyManagment took its target from PropertyManagementComponent::Instance(), which needs a + // property world, so it is checked against hand written bytes: header, target, message id and nothing else. + GameMessages::OpenPropertyManagement management; + management.target = 0x0102030405060708LL; + EXPECT_PACKET_EQ(FromHex("53 05 00 0c 00 00 00 00 08 07 06 05 04 03 02 01 5c 03"), StructPacket(management)); +} + +TEST_F(PropertyMessagesTests, DownloadPropertyDataMatchesLegacy) { + std::vector inputs; + for (const auto& text : g_Utf8) { + PropertyDataInput in; + in.templateId = 25166; + in.mapId = 1150; + in.vendorMapId = 1100; + in.spawnName = "AGSmallProperty"; + in.name = text; + in.description = text + "!"; + in.ownerName = text.substr(0, 10); + inputs.push_back(in); + } + PropertyDataInput full; + full.templateId = 0xFFFFFFFF; + full.mapId = 0x12345; + full.vendorMapId = 0xFFFF; + full.spawnName = "NSSmallProperty"; + full.moderatorRequested = true; + full.reputation = 0xFFFFFFFF; + full.lastUpdatedTime = 1727000000; + full.ownerId = 0x1000000000000456LL; + full.ownerName = "Owner"; + full.name = "Name"; + full.description = "Desc"; + full.claimed = 0x0102030405060708ULL; + full.privacy = 2; + full.cloneId = 1234567; + full.paths = { NiPoint3(1, 2, 3), NiPoint3(-4, 5.5f, 6), NiPoint3Constant::ZERO }; + inputs.push_back(full); + full.rejectionReason = "no"; + inputs.push_back(full); + full.rejectionReason = ""; + full.moderatorRequested = false; + full.privacy = static_cast(-1); + inputs.push_back(full); + + for (const auto target : g_Targets) { + for (const auto& in : inputs) { + const auto msg = Build(in, target); + const auto legacy = BuildLegacy(in); + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendDownloadPropertyData(target, legacy, a); }, msg); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.name, msg.name); + EXPECT_EQ(copy.moderationStatus, msg.moderationStatus); + EXPECT_EQ(copy.pathPositions, msg.pathPositions); + EXPECT_EQ(copy.rentalDate, msg.rentalDate); + ExpectTruncatedFails(msg); + } + } +} + +TEST_F(PropertyMessagesTests, PropertyRentalResponseMatchesLegacy) { + for (const auto target : g_Targets) { + for (const auto value : g_Ints) { + GameMessages::PropertyRentalResponse msg; + msg.target = target; + msg.cloneid = static_cast(value); + msg.code = static_cast(value / 3); + msg.propertyID = target; + msg.rentdue = static_cast(value) * 1000; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPropertyRentalResponse(target, msg.cloneid, msg.code, msg.propertyID, msg.rentdue, a); }, msg); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.cloneid, msg.cloneid); + EXPECT_EQ(copy.rentdue, msg.rentdue); + ExpectTruncatedFails(msg); + } + } +} + +TEST_F(PropertyMessagesTests, PropertySelectQueryMatchesLegacy) { + std::vector> pages = { {} }; + std::vector page; + for (size_t i = 0; i < g_Utf8.size(); i++) { + LegacyGameMessages::PropertySelectQueryProperty entry; + entry.CloneId = static_cast(i * 1000); + entry.OwnerName = g_Utf8[i]; + entry.Name = g_Utf8[(i + 1) % g_Utf8.size()]; + entry.Description = g_Utf8[(i + 2) % g_Utf8.size()]; + entry.Reputation = static_cast(i) * 12.5f; + entry.IsBestFriend = i & 1; + entry.IsFriend = i & 2; + entry.IsModeratorApproved = i & 1; + entry.IsAlt = i & 2; + entry.IsOwned = i == 0; + entry.AccessType = static_cast(i); + entry.DateLastPublished = 1727000000ULL + i; + entry.PerformanceCost = 0.25f * static_cast(i); + page.push_back(entry); + } + page.emplace_back(); // defaults (cloneId LWOCLONEID_INVALID) + pages.push_back(page); + + for (const auto target : g_Targets) { + for (const auto& entries : pages) { + for (const auto navOffset : { 0, 7, -1 }) { + for (const bool flag : { false, true }) { + GameMessages::PropertySelectQuery msg; + msg.target = target; + msg.navOffset = navOffset; + msg.thereAreMore = flag; + msg.cloneId = navOffset * 11; + msg.hasFeaturedProperty = !flag; + msg.wasFriends = flag; + msg.properties = ToInfos(entries); + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPropertySelectQuery(target, navOffset, flag, msg.cloneId, !flag, flag, entries, a); }, msg); + const auto copy = RoundTrip(msg); + ASSERT_EQ(copy.properties.size(), msg.properties.size()); + for (size_t i = 0; i < copy.properties.size(); i++) { + EXPECT_EQ(copy.properties[i].name, msg.properties[i].name); + EXPECT_EQ(copy.properties[i].cloneId, msg.properties[i].cloneId); + EXPECT_EQ(copy.properties[i].isAlt, msg.properties[i].isAlt); + } + ExpectTruncatedFails(msg); + } + } + } + } +} + +TEST_F(PropertyMessagesTests, GetModelsOnPropertyMatchesLegacy) { + const std::vector> modelSets = { {}, { { 5, 6 } }, { { 0x1000000000000001LL, 1 }, { 2, 0x0102030405060708LL }, { -1, 0 } } }; + for (const auto target : g_Targets) { + for (const auto& models : modelSets) { + GameMessages::GetModelsOnProperty msg; + msg.target = target; + msg.models = { models.begin(), models.end() }; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendGetModelsOnProperty(target, models, a); }, msg); + EXPECT_EQ(RoundTrip(msg).models, msg.models); + ExpectTruncatedFails(msg); + } + } +} + +TEST_F(PropertyMessagesTests, PlaceModelResponseMatchesLegacy) { + for (const auto target : g_Targets) { + for (const auto& position : g_Points) { + for (const auto plaque : g_Targets) { + for (const auto response : { 0, 14, 16, -1 }) { + for (const auto& rotation : g_Rotations) { + GameMessages::PlaceModelResponse msg; + msg.target = target; + msg.position = position; + msg.propertyPlaqueID = plaque; + msg.response = response; + msg.rotation = rotation; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendPlaceModelResponse(target, a, position, plaque, response, rotation); }, msg); + // The kept wire bug (response written where rotation belongs) only round trips with the default rotation. + if (rotation == QuatUtils::IDENTITY) { + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.position, position); + EXPECT_EQ(copy.propertyPlaqueID, plaque); + EXPECT_EQ(copy.response, response); + ExpectTruncatedFails(msg); + } + } + } + } + } + } +} + +TEST_F(PropertyMessagesTests, UgcEquipMessagesMatchLegacy) { + for (const auto target : g_Targets) { + for (const auto id : g_Targets) { + for (const auto value : g_Ints) { + GameMessages::HandleUGCEquipPreCreateBasedOnEditMode pre; + pre.target = target; + pre.modelCount = value; + pre.modelID = id; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendUGCEquipPreCreateBasedOnEditMode(target, a, value, id); }, pre); + EXPECT_EQ(RoundTrip(pre).modelCount, value); + + GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode post; + post.target = target; + post.invItem = id; + post.itemsTotal = value; + ExpectSameAsLegacy([&](const SystemAddress& a) { LegacyGameMessages::SendUGCEquipPostDeleteBasedOnEditMode(target, a, id, value); }, post); + const auto copy = RoundTrip(post); + EXPECT_EQ(copy.invItem, id); + EXPECT_EQ(copy.itemsTotal, value); + ExpectTruncatedFails(post); + } + } + } +} + +TEST_F(PropertyMessagesTests, HotPropertiesMatchLegacy) { + GameMessages::NewsSendHotPropertiesInfoToClient msg; + for (int i = 0; i < 3; i++) { + RakNet::BitStream legacy; + LegacyGameMessages::SerializeHotProperties(legacy, msg.properties); + EXPECT_PACKET_EQ(FromBitStream(legacy), Payload(msg)); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.properties.size(), msg.properties.size()); + + auto& info = msg.properties.emplace_back(); + info.propertyId = 0x1000000000000123LL + i; + info.ownerId = 0x1000000000000456LL; + info.ownerName = GeneralUtils::UTF8ToUTF16(g_Utf8[i + 1]); + info.reputation = 12345ULL << (i * 10); + info.templateId = 25166 + i; + info.name = GeneralUtils::UTF8ToUTF16(g_Utf8[i]); + info.description = u"Desc"; + info.performanceCost = 0.5f * static_cast(i); + info.lastPublished = 1727000000ULL; + info.cloneId = static_cast(i); + } + ExpectTruncatedFails(msg); +} + +TEST_F(PropertyMessagesTests, InboundReadsLikeLegacy) { + for (const uint8_t access : { 0, 1, 2, 255 }) { + for (const auto renew : g_Ints) { + GameMessages::SetPropertyAccess msg; + msg.accessType = access; + msg.renew = renew; + const auto legacy = ReadWithLegacy(msg, LegacyGameMessages::ReadSetPropertyAccess); + const auto copy = RoundTrip(msg); + EXPECT_EQ(copy.accessType, legacy.accessType); + EXPECT_EQ(copy.renew, legacy.renew); + ExpectTruncatedFails(msg); + } + } + + for (const auto id : g_Targets) { + for (const auto& text : g_Utf8) { + for (const bool isProperty : { false, true }) { + GameMessages::UpdatePropertyOrModelForFilterCheck msg; + msg.isProperty = isProperty; + msg.ugcId = id; + msg.playerId = ~id; + msg.worldId = id / 3; + msg.newDescription = GeneralUtils::UTF8ToUTF16(text); + msg.newName = GeneralUtils::UTF8ToUTF16(text.substr(0, 5)); + const auto legacy = ReadWithLegacy(msg, LegacyGameMessages::ReadUpdatePropertyOrModelForFilterCheck); + const auto copy = RoundTrip(msg); + EXPECT_FALSE(legacy.rejected); + EXPECT_EQ(copy.isProperty, legacy.isProperty); + EXPECT_EQ(copy.ugcId, legacy.objectId); + EXPECT_EQ(copy.playerId, legacy.playerId); + EXPECT_EQ(copy.worldId, legacy.worldId); + EXPECT_EQ(copy.newName, legacy.name); + EXPECT_EQ(copy.newDescription, legacy.description); + ExpectTruncatedFails(msg); + } + + for (const auto reason : { 0, 1, 2, -1 }) { + GameMessages::DeleteModelFromClient del; + del.modelID = id; + del.reason = reason; + const auto legacy = ReadWithLegacy(del, LegacyGameMessages::ReadDeletePropertyModel); + const auto copy = RoundTrip(del); + EXPECT_EQ(copy.modelID, legacy.model); + EXPECT_EQ(copy.reason, legacy.deleteReason); + ExpectTruncatedFails(del); + } + + GameMessages::ReportOffensiveModel model; + model.description = GeneralUtils::UTF8ToUTF16(text); + model.offensiveObjectID = id; + GameMessages::ReportOffensiveProperty property; + property.description = model.description; + property.propertyPlaqueObjectID = id; + for (const GameMessages::NetGameMsg* report : std::initializer_list{ &model, &property }) { + std::string description; + LWOOBJID objectId{}; + const bool read = ReadWithLegacy(*report, [&](RakNet::BitStream& bs) { return LegacyGameMessages::ReadDescriptionAndObject(bs, description, objectId); }); + EXPECT_TRUE(read); + EXPECT_EQ(description, GeneralUtils::UTF16ToWTF8(model.description)); + EXPECT_EQ(objectId, id); + } + EXPECT_EQ(RoundTrip(model).description, model.description); + EXPECT_EQ(RoundTrip(property).propertyPlaqueObjectID, id); + ExpectTruncatedFails(model); + ExpectTruncatedFails(property); + } + + GameMessages::PlacePropertyModel place; + place.modelID = id; + EXPECT_EQ(ReadWithLegacy(place, LegacyGameMessages::ReadPlacePropertyModel), id); + EXPECT_EQ(RoundTrip(place).modelID, id); + ExpectTruncatedFails(place); + + for (const auto& position : g_Points) { + for (const auto& rotation : g_Rotations) { + GameMessages::UpdateModelFromClient update; + update.modelID = id; + update.position = position; + update.rotation = rotation; + const auto legacy = ReadWithLegacy(update, LegacyGameMessages::ReadUpdatePropertyModel); + const auto copy = RoundTrip(update); + EXPECT_EQ(copy.modelID, legacy.model); + EXPECT_EQ(copy.position, legacy.position); + EXPECT_EQ(copy.rotation, legacy.rotation); + ExpectTruncatedFails(update); + } + } + } + + for (const auto value : g_Ints) { + for (const bool flag : { false, true }) { + GameMessages::PropertyEntranceSync sync; + sync.includeNullAddress = flag; + sync.includeNullDescription = !flag; + sync.playersOwn = flag; + sync.updateUI = !flag; + sync.numResults = value; + sync.reputationTime = -value; + sync.sortMethod = value / 2; + sync.startIndex = value / 3; + sync.filterText = flag ? "search" : ""; + const auto legacy = ReadWithLegacy(sync, LegacyGameMessages::ReadPropertyEntranceSync); + const auto copy = RoundTrip(sync); + EXPECT_FALSE(legacy.rejected); + EXPECT_EQ(copy.includeNullAddress, legacy.includeNullAddress); + EXPECT_EQ(copy.includeNullDescription, legacy.includeNullDescription); + EXPECT_EQ(copy.playersOwn, legacy.playerOwn); + EXPECT_EQ(copy.updateUI, legacy.updateUi); + EXPECT_EQ(copy.numResults, legacy.numResults); + EXPECT_EQ(copy.reputationTime, legacy.reputation); + EXPECT_EQ(copy.sortMethod, legacy.sortMethod); + EXPECT_EQ(copy.startIndex, legacy.startIndex); + EXPECT_EQ(copy.filterText, legacy.filterText); + ExpectTruncatedFails(sync); + + GameMessages::EnterProperty1 enter; + enter.index = value; + enter.returnToZone = flag; + const auto legacyEnter = ReadWithLegacy(enter, LegacyGameMessages::ReadEnterProperty); + const auto enterCopy = RoundTrip(enter); + EXPECT_EQ(static_cast(enterCopy.index), legacyEnter.index); + EXPECT_EQ(enterCopy.returnToZone, legacyEnter.returnToZone); + ExpectTruncatedFails(enter); + } + } + + for (const float cost : { 0.0f, 1.5f, -2.25f, 1e9f }) { + GameMessages::UpdatePropertyPerformanceCost msg; + msg.performanceCost = cost; + EXPECT_EQ(ReadWithLegacy(msg, LegacyGameMessages::ReadUpdatePropertyPerformanceCost), cost); + EXPECT_EQ(RoundTrip(msg).performanceCost, cost); + ExpectTruncatedFails(msg); + } +} + +// Messages whose payload DLU used to ignore: layouts from the client's Deserialize functions. +TEST_F(PropertyMessagesTests, ClientLayoutsRoundTrip) { + GameMessages::PropertyEditorBegin begin; + EXPECT_PACKET_EQ(FromHex("00", 4), Payload(begin)); + begin.distanceType = 2; + begin.propertyObjectID = 0x11; + begin.startMode = 3; + begin.startPaused = true; + const auto beginCopy = RoundTrip(begin); + EXPECT_EQ(beginCopy.distanceType, 2); + EXPECT_EQ(beginCopy.propertyObjectID, 0x11); + EXPECT_EQ(beginCopy.startMode, 3); + EXPECT_TRUE(beginCopy.startPaused); + ExpectTruncatedFails(begin); + + for (const bool queryDB : { false, true }) { + GameMessages::PropertyContentsFromClient contents; + contents.queryDB = queryDB; + EXPECT_EQ(RoundTrip(contents).queryDB, queryDB); + ExpectTruncatedFails(contents); + } + + for (const auto id : g_Targets) { + GameMessages::ZonePropertyModelEquipped equipped; + equipped.playerID = id; + equipped.propertyID = ~id; + const auto equippedCopy = RoundTrip(equipped); + EXPECT_EQ(equippedCopy.playerID, id); + EXPECT_EQ(equippedCopy.propertyID, ~id); + ExpectTruncatedFails(equipped); + + GameMessages::ZonePropertyModelRotated rotated; + rotated.playerID = ~id; + rotated.propertyID = id; + const auto rotatedCopy = RoundTrip(rotated); + EXPECT_EQ(rotatedCopy.playerID, ~id); + EXPECT_EQ(rotatedCopy.propertyID, id); + ExpectTruncatedFails(rotated); + } +} + +TEST_F(PropertyMessagesTests, ControlBehaviorsRoundTrips) { + GameMessages::ControlBehaviors msg; + msg.args = std::make_unique(); + // One key: the associative part is unordered, so more keys may come back in another order. + msg.args->Insert("BehaviorID", "10446"); + msg.command = "rename"; + RakNet::BitStream bitStream; + msg.Serialize(bitStream); + GameMessages::ControlBehaviors copy; + ASSERT_TRUE(copy.Deserialize(bitStream)); + EXPECT_EQ(bitStream.GetNumberOfUnreadBits(), 0); + EXPECT_EQ(copy.command, "rename"); + ASSERT_NE(copy.args, nullptr); + ASSERT_NE(copy.args->Get("BehaviorID"), nullptr); + EXPECT_EQ(copy.args->Get("BehaviorID")->GetValue(), "10446"); + RakNet::BitStream again; + copy.Serialize(again); + EXPECT_PACKET_EQ(FromBitStream(bitStream), FromBitStream(again)); + + // Not an AMF array: dropped. + RakNet::BitStream notArray; + notArray.Write(0x04); // integer marker + notArray.Write(0x01); + GameMessages::ControlBehaviors bad; + EXPECT_FALSE(bad.Deserialize(notArray)); +} + +// Independent of the legacy code: hand computed payloads (RakNet writes MSB first). +TEST_F(PropertyMessagesTests, GoldenBytes) { + GameMessages::HandleUGCEquipPostDeleteBasedOnEditMode post; + post.invItem = 0x22; + EXPECT_PACKET_EQ(FromHex("22 00 00 00 00 00 00 00 00", 65), Payload(post)); + + GameMessages::PlaceModelResponse response; + response.response = 16; + // three flags 0, 0, 1, then 10 00 00 00, then the rotation flag 0 + EXPECT_PACKET_EQ(FromHex("22 00 00 00 00", 36), Payload(response)); + + GameMessages::SetPropertyAccess access; + access.accessType = 2; + // 1, 02, 0 + EXPECT_PACKET_EQ(FromHex("81 00", 10), Payload(access)); + + GameMessages::GetModelsOnProperty models; + models.models = { { 1, 2 } }; + EXPECT_PACKET_EQ(FromHex("01 00 00 00 01 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00"), Payload(models)); + + GameMessages::EnterProperty1 enter; + enter.index = -1; + EXPECT_PACKET_EQ(FromHex("ff ff ff ff 80", 33), Payload(enter)); +}