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)); +}