diff --git a/dDatabase/Tables/CDMissionsTable.h b/dDatabase/Tables/CDMissionsTable.h index c083bd31..c961bb80 100644 --- a/dDatabase/Tables/CDMissionsTable.h +++ b/dDatabase/Tables/CDMissionsTable.h @@ -3,6 +3,7 @@ // Custom Classes #include "CDTable.h" #include +#include /*! \file CDMissionsTable.hpp @@ -17,9 +18,9 @@ struct CDMissions { int UISortOrder; //!< The UI Sort Order for the mission int offer_objectID; //!< The LOT of the mission giver int target_objectID; //!< The LOT of the mission's target - __int64 reward_currency; //!< The amount of currency to reward the player + int64_t reward_currency; //!< The amount of currency to reward the player int LegoScore; //!< The amount of LEGO Score to reward the player - __int64 reward_reputation; //!< The reputation to award the player + int64_t reward_reputation; //!< The reputation to award the player bool isChoiceReward; //!< Whether or not the user has the option to choose their loot int reward_item1; //!< The first rewarded item int reward_item1_count; //!< The count of the first item to be rewarded @@ -40,7 +41,7 @@ struct CDMissions { int reward_maxwidget; //!< ??? int reward_maxwallet; //!< ??? bool repeatable; //!< Whether or not this mission can be repeated (for instance, is it a daily mission) - __int64 reward_currency_repeatable; //!< The repeatable reward + int64_t reward_currency_repeatable; //!< The repeatable reward int reward_item1_repeatable; //!< The first rewarded item int reward_item1_repeat_count; //!< The count of the first item to be rewarded int reward_item2_repeatable; //!< The second rewarded item @@ -55,7 +56,7 @@ struct CDMissions { std::string prereqMissionID; //!< A '|' seperated list of prerequisite missions bool localize; //!< Whether or not to localize the mission bool inMOTD; //!< In Match of the Day(?) - __int64 cooldownTime; //!< The mission cooldown time + int64_t cooldownTime; //!< The mission cooldown time bool isRandom; //!< ??? std::string randomPool; //!< ??? int UIPrereqID; //!< ??? diff --git a/dGame/dComponents/PropertyEntranceComponent.cpp b/dGame/dComponents/PropertyEntranceComponent.cpp index 9e286ffa..4dd2eed0 100644 --- a/dGame/dComponents/PropertyEntranceComponent.cpp +++ b/dGame/dComponents/PropertyEntranceComponent.cpp @@ -1,12 +1,15 @@ -#include -#include "PropertyEntranceComponent.h" +#include "PropertyEntranceComponent.h" + +#include + +#include "Character.h" +#include "Database.h" +#include "GameMessages.h" +#include "PropertyManagementComponent.h" #include "PropertySelectQueryProperty.h" #include "RocketLaunchpadControlComponent.h" -#include "Character.h" -#include "GameMessages.h" +#include "UserManager.h" #include "dLogger.h" -#include "Database.h" -#include "PropertyManagementComponent.h" PropertyEntranceComponent::PropertyEntranceComponent(uint32_t componentID, Entity* parent) : Component(parent) { @@ -49,6 +52,7 @@ void PropertyEntranceComponent::OnEnterProperty(Entity* entity, uint32_t index, } else if (index >= 0) { + // Increment index once here because the first index of other player properties is 2 in the propertyQueries cache. index++; const auto& pair = propertyQueries.find(entity->GetObjectID()); @@ -71,167 +75,269 @@ void PropertyEntranceComponent::OnEnterProperty(Entity* entity, uint32_t index, launcher->SetSelectedCloneId(entity->GetObjectID(), cloneId); - launcher->Launch(entity, LWOOBJID_EMPTY, LWOMAPID_INVALID, cloneId); + launcher->Launch(entity, LWOOBJID_EMPTY, launcher->GetTargetZone(), cloneId); } -void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, - bool includeNullAddress, - bool includeNullDescription, - bool playerOwn, - bool updateUi, - int32_t numResults, - int32_t reputation, - int32_t sortMethod, - int32_t startIndex, - std::string filterText, - const SystemAddress& sysAddr) -{ - Game::logger->Log("PropertyEntranceComponent", "On Sync %d %d %d %d %i %i %i %i %s\n", - includeNullAddress, - includeNullDescription, - playerOwn, - updateUi, - numResults, - reputation, - sortMethod, - startIndex, - filterText.c_str() - ); +PropertySelectQueryProperty PropertyEntranceComponent::SetPropertyValues(PropertySelectQueryProperty property, LWOCLONEID cloneId, std::string ownerName, std::string propertyName, std::string propertyDescription, float reputation, bool isBFF, bool isFriend, bool isModeratorApproved, bool isAlt, bool isOwned, uint32_t privacyOption, uint32_t timeLastUpdated, float performanceCost) { + property.CloneId = cloneId; + property.OwnerName = ownerName; + property.Name = propertyName; + property.Description = propertyDescription; + property.Reputation = reputation; + property.IsBestFriend = isBFF; + property.IsFriend = isFriend; + property.IsModeratorApproved = isModeratorApproved; + property.IsAlt = isAlt; + property.IsOwned = isOwned; + property.AccessType = privacyOption; + property.DateLastPublished = timeLastUpdated; + property.PerformanceCost = performanceCost; - auto* launchpadComponent = m_Parent->GetComponent(); - if (launchpadComponent == nullptr) - return; + return property; +} + +std::string PropertyEntranceComponent::BuildQuery(Entity* entity, int32_t sortMethod, std::string customQuery, bool wantLimits) { + std::string base; + if (customQuery == "") { + base = baseQueryForProperties; + } else { + base = customQuery; + } + std::string orderBy = ""; + if (sortMethod == SORT_TYPE_FEATURED || sortMethod == SORT_TYPE_FRIENDS) { + std::string friendsList = " AND p.owner_id IN ("; + + auto friendsListQuery = Database::CreatePreppedStmt("SELECT * FROM (SELECT CASE WHEN player_id = ? THEN friend_id WHEN friend_id = ? THEN player_id END AS requested_player FROM dlu.friends ) AS fr WHERE requested_player IS NOT NULL ORDER BY requested_player DESC;"); + + friendsListQuery->setInt64(1, entity->GetObjectID()); + friendsListQuery->setInt64(2, entity->GetObjectID()); + + auto friendsListQueryResult = friendsListQuery->executeQuery(); + + while (friendsListQueryResult->next()) { + auto playerIDToConvert = friendsListQueryResult->getInt64(1); + playerIDToConvert = GeneralUtils::ClearBit(playerIDToConvert, OBJECT_BIT_CHARACTER); + playerIDToConvert = GeneralUtils::ClearBit(playerIDToConvert, OBJECT_BIT_PERSISTENT); + friendsList = friendsList + std::to_string(playerIDToConvert) + ","; + } + // Replace trailing comma with the closing parenthesis. + if (friendsList.at(friendsList.size() - 1) == ',') friendsList.erase(friendsList.size() - 1, 1); + friendsList += ") "; + + // If we have no friends then use a -1 for the query. + if (friendsList.find("()") != std::string::npos) friendsList = " AND p.owner_id IN (-1) "; + + orderBy += friendsList + "ORDER BY ci.name ASC "; + + delete friendsListQueryResult; + friendsListQueryResult = nullptr; + + delete friendsListQuery; + friendsListQuery = nullptr; + } + else if (sortMethod == SORT_TYPE_RECENT) { + orderBy = "ORDER BY p.last_updated DESC "; + } + else if (sortMethod == SORT_TYPE_REPUTATION) { + orderBy = "ORDER BY p.reputation DESC, p.last_updated DESC "; + } + else { + orderBy = "ORDER BY p.last_updated DESC "; + } + return base + orderBy + (wantLimits ? "LIMIT ? OFFSET ?;" : ";"); +} + +void PropertyEntranceComponent::OnPropertyEntranceSync(Entity* entity, bool includeNullAddress, bool includeNullDescription, bool playerOwn, bool updateUi, int32_t numResults, int32_t lReputationTime, int32_t sortMethod, int32_t startIndex, std::string filterText, const SystemAddress& sysAddr){ std::vector entries {}; PropertySelectQueryProperty playerEntry {}; - auto* character = entity->GetCharacter(); - playerEntry.OwnerName = character->GetName(); - playerEntry.Description = "No description."; - playerEntry.Name = "Your property!"; - playerEntry.IsModeratorApproved = true; - playerEntry.AccessType = 2; - playerEntry.CloneId = character->GetPropertyCloneID(); + auto character = entity->GetCharacter(); + if (!character) return; + + // Player property goes in index 1 of the vector. This is how the client expects it. + auto playerPropertyLookup = Database::CreatePreppedStmt("SELECT * FROM properties WHERE owner_id = ? AND zone_id = ?"); + + playerPropertyLookup->setInt(1, character->GetID()); + playerPropertyLookup->setInt(2, this->m_MapID); + + auto playerPropertyLookupResults = playerPropertyLookup->executeQuery(); + + // If the player has a property this query will have a single result. + if (playerPropertyLookupResults->next()) { + const auto cloneId = playerPropertyLookupResults->getUInt64(4); + const auto propertyName = playerPropertyLookupResults->getString(5).asStdString(); + const auto propertyDescription = playerPropertyLookupResults->getString(6).asStdString(); + const auto privacyOption = playerPropertyLookupResults->getInt(9); + const auto modApproved = playerPropertyLookupResults->getBoolean(10); + const auto dateLastUpdated = playerPropertyLookupResults->getInt64(11); + const auto reputation = playerPropertyLookupResults->getUInt(14); + const auto performanceCost = (float)playerPropertyLookupResults->getDouble(16); + + playerEntry = SetPropertyValues(playerEntry, cloneId, character->GetName(), propertyName, propertyDescription, reputation, true, true, modApproved, true, true, privacyOption, dateLastUpdated, performanceCost); + } else { + playerEntry = SetPropertyValues(playerEntry, character->GetPropertyCloneID(), character->GetName(), "", "", 0, true, true); + } + + delete playerPropertyLookupResults; + playerPropertyLookupResults = nullptr; + + delete playerPropertyLookup; + playerPropertyLookup = nullptr; entries.push_back(playerEntry); - sql::ResultSet* propertyEntry; - sql::PreparedStatement* propertyLookup; + const auto query = BuildQuery(entity, sortMethod); - const auto moderating = entity->GetGMLevel() >= GAME_MASTER_LEVEL_LEAD_MODERATOR; - - if (!moderating) - { - propertyLookup = Database::CreatePreppedStmt( - "SELECT * FROM properties WHERE (name LIKE ? OR description LIKE ? OR " - "((SELECT name FROM charinfo WHERE prop_clone_id = clone_id) LIKE ?)) AND " - "(privacy_option = 2 AND mod_approved = true) OR (privacy_option >= 1 " - "AND (owner_id IN (SELECT friend_id FROM friends WHERE player_id = ?) OR owner_id IN (SELECT player_id FROM " - "friends WHERE friend_id = ?))) AND zone_id = ? LIMIT ? OFFSET ?;" - ); + auto propertyLookup = Database::CreatePreppedStmt(query); + + const auto searchString = "%" + filterText + "%"; + propertyLookup->setUInt(1, this->m_MapID); + propertyLookup->setString(2, searchString.c_str()); + propertyLookup->setString(3, searchString.c_str()); + propertyLookup->setString(4, searchString.c_str()); + propertyLookup->setInt(5, sortMethod == SORT_TYPE_FEATURED || sortMethod == SORT_TYPE_FRIENDS ? (uint32_t)PropertyPrivacyOption::Friends : (uint32_t)PropertyPrivacyOption::Public); + propertyLookup->setInt(6, numResults); + propertyLookup->setInt(7, startIndex); - const std::string searchString = "%" + filterText + "%"; - Game::logger->Log("PropertyEntranceComponent", "%s\n", searchString.c_str()); - propertyLookup->setString(1, searchString.c_str()); - propertyLookup->setString(2, searchString.c_str()); - propertyLookup->setString(3, searchString.c_str()); - propertyLookup->setInt64(4, entity->GetObjectID()); - propertyLookup->setInt64(5, entity->GetObjectID()); - propertyLookup->setUInt(6, launchpadComponent->GetTargetZone()); - propertyLookup->setInt(7, numResults); - propertyLookup->setInt(8, startIndex); + auto propertyEntry = propertyLookup->executeQuery(); - propertyEntry = propertyLookup->executeQuery(); - } - else - { - propertyLookup = Database::CreatePreppedStmt( - "SELECT * FROM properties WHERE privacy_option = 2 AND mod_approved = false AND zone_id = ?;" - ); - - propertyLookup->setUInt(1, launchpadComponent->GetTargetZone()); - - propertyEntry = propertyLookup->executeQuery(); - } - - while (propertyEntry->next()) - { - const auto propertyId = propertyEntry->getUInt64(1); - const auto owner = propertyEntry->getUInt64(2); + while (propertyEntry->next()) { + const auto propertyId = propertyEntry->getUInt64(1); + const auto owner = propertyEntry->getInt(2); const auto cloneId = propertyEntry->getUInt64(4); - const auto name = propertyEntry->getString(5).asStdString(); - const auto description = propertyEntry->getString(6).asStdString(); - const auto privacyOption = propertyEntry->getInt(9); - const auto reputation = propertyEntry->getInt(15); + const auto propertyNameFromDb = propertyEntry->getString(5).asStdString(); + const auto propertyDescriptionFromDb = propertyEntry->getString(6).asStdString(); + const auto privacyOption = propertyEntry->getInt(9); + const auto modApproved = propertyEntry->getBoolean(10); + const auto dateLastUpdated = propertyEntry->getInt(11); + const float reputation = propertyEntry->getInt(14); + const auto performanceCost = (float)propertyEntry->getDouble(16); - PropertySelectQueryProperty entry {}; - - auto* nameLookup = Database::CreatePreppedStmt("SELECT name FROM charinfo WHERE prop_clone_id = ?;"); + PropertySelectQueryProperty entry{}; + + std::string ownerName = ""; + bool isOwned = true; + auto nameLookup = Database::CreatePreppedStmt("SELECT name FROM charinfo WHERE prop_clone_id = ?;"); nameLookup->setUInt64(1, cloneId); - auto* nameResult = nameLookup->executeQuery(); + auto nameResult = nameLookup->executeQuery(); - if (!nameResult->next()) - { + if (!nameResult->next()) { delete nameLookup; + nameLookup = nullptr; Game::logger->Log("PropertyEntranceComponent", "Failed to find property owner name for %llu!\n", cloneId); continue; + } else { + isOwned = cloneId == character->GetPropertyCloneID(); + ownerName = nameResult->getString(1).asStdString(); } - else - { - entry.IsOwner = owner == entity->GetObjectID(); - entry.OwnerName = nameResult->getString(1).asStdString(); - } - - if (!moderating) - { - entry.Name = name; - entry.Description = description; - } - else - { - entry.Name = "[Awaiting approval] " + name; - entry.Description = "[Awaiting approval] " + description; - } - - entry.IsFriend = privacyOption == static_cast(PropertyPrivacyOption::Friends); - entry.Reputation = reputation; - entry.CloneId = cloneId; - entry.IsModeratorApproved = true; - entry.AccessType = 3; - entries.push_back(entry); + delete nameResult; + nameResult = nullptr; delete nameLookup; - } + nameLookup = nullptr; + + std::string propertyName = propertyNameFromDb; + std::string propertyDescription = propertyDescriptionFromDb; + + bool isBestFriend = false; + bool isFriend = false; + + // Convert owner char id to LWOOBJID + LWOOBJID ownerObjId = owner; + ownerObjId = GeneralUtils::SetBit(ownerObjId, OBJECT_BIT_CHARACTER); + ownerObjId = GeneralUtils::SetBit(ownerObjId, OBJECT_BIT_PERSISTENT); + + // Query to get friend and best friend fields + auto friendCheck = Database::CreatePreppedStmt("SELECT best_friend FROM friends WHERE (player_id = ? AND friend_id = ?) OR (player_id = ? AND friend_id = ?)"); + + friendCheck->setInt64(1, entity->GetObjectID()); + friendCheck->setInt64(2, ownerObjId); + friendCheck->setInt64(3, ownerObjId); + friendCheck->setInt64(4, entity->GetObjectID()); + + auto friendResult = friendCheck->executeQuery(); + + // If we got a result than the two players are friends. + if (friendResult->next()) { + isFriend = true; + if (friendResult->getInt(1) == 2) { + isBestFriend = true; + } + } + + delete friendCheck; + friendCheck = nullptr; + + delete friendResult; + friendResult = nullptr; + + bool isModeratorApproved = propertyEntry->getBoolean(10); + + if (!isModeratorApproved && entity->GetGMLevel() >= GAME_MASTER_LEVEL_LEAD_MODERATOR) { + propertyName = "[AWAITING APPROVAL]"; + propertyDescription = "[AWAITING APPROVAL]"; + isModeratorApproved = true; + } + + bool isAlt = false; + // Query to determine whether this property is an alt character of the entity. + auto isAltQuery = Database::CreatePreppedStmt("SELECT id FROM charinfo where account_id in (SELECT account_id from charinfo WHERE id = ?) AND id = ?;"); + + isAltQuery->setInt(1, character->GetID()); + isAltQuery->setInt(2, owner); + + auto isAltQueryResults = isAltQuery->executeQuery(); + + if (isAltQueryResults->next()) { + isAlt = true; + } + + delete isAltQueryResults; + isAltQueryResults = nullptr; + + delete isAltQuery; + isAltQuery = nullptr; + + entry = SetPropertyValues(entry, cloneId, ownerName, propertyName, propertyDescription, reputation, isBestFriend, isFriend, isModeratorApproved, isAlt, isOwned, privacyOption, dateLastUpdated, performanceCost); + + entries.push_back(entry); + } + + delete propertyEntry; + propertyEntry = nullptr; delete propertyLookup; - - /* - const auto entriesSize = entries.size(); - - if (startIndex != 0 && entriesSize > startIndex) - { - for (size_t i = 0; i < startIndex; i++) - { - entries.erase(entries.begin()); - } - } - */ + propertyLookup = nullptr; propertyQueries[entity->GetObjectID()] = entries; + + // Query here is to figure out whether or not to display the button to go to the next page or not. + int32_t numberOfProperties = 0; - GameMessages::SendPropertySelectQuery( - m_Parent->GetObjectID(), - startIndex, - entries.size() >= numResults, - character->GetPropertyCloneID(), - false, - true, - entries, - sysAddr - ); + auto buttonQuery = BuildQuery(entity, sortMethod, "SELECT COUNT(*) FROM properties as p JOIN charinfo as ci ON ci.prop_clone_id = p.clone_id where p.zone_id = ? AND (p.description LIKE ? OR p.name LIKE ? OR ci.name LIKE ?) AND p.privacy_option >= ? ", false); + auto propertiesLeft = Database::CreatePreppedStmt(buttonQuery); + + propertiesLeft->setUInt(1, this->m_MapID); + propertiesLeft->setString(2, searchString.c_str()); + propertiesLeft->setString(3, searchString.c_str()); + propertiesLeft->setString(4, searchString.c_str()); + propertiesLeft->setInt(5, sortMethod == SORT_TYPE_FEATURED || sortMethod == SORT_TYPE_FRIENDS ? 1 : 2); + + auto result = propertiesLeft->executeQuery(); + result->next(); + numberOfProperties = result->getInt(1); + + delete result; + result = nullptr; + + delete propertiesLeft; + propertiesLeft = nullptr; + + GameMessages::SendPropertySelectQuery(m_Parent->GetObjectID(), startIndex, numberOfProperties - (startIndex + numResults) > 0, character->GetPropertyCloneID(), false, true, entries, sysAddr); } \ No newline at end of file diff --git a/dGame/dComponents/PropertyEntranceComponent.h b/dGame/dComponents/PropertyEntranceComponent.h index 8e35fd91..a3be38a6 100644 --- a/dGame/dComponents/PropertyEntranceComponent.h +++ b/dGame/dComponents/PropertyEntranceComponent.h @@ -1,17 +1,17 @@ #pragma once +#include + +#include "Component.h" #include "Entity.h" #include "EntityManager.h" #include "GameMessages.h" -#include "Component.h" -#include /** * Represents the launch pad that's used to select and browse properties */ -class PropertyEntranceComponent : public Component -{ -public: +class PropertyEntranceComponent : public Component { + public: static const uint32_t ComponentType = COMPONENT_TYPE_PROPERTY_ENTRANCE; explicit PropertyEntranceComponent(uint32_t componentID, Entity* parent); @@ -24,11 +24,11 @@ public: /** * Handles the event triggered when the entity selects a property to visit and makes the entity to there * @param entity the entity that triggered the event - * @param index the clone ID of the property to visit + * @param index the index of the property property * @param returnToZone whether or not the entity wishes to go back to the launch zone * @param sysAddr the address to send gamemessage responses to */ - void OnEnterProperty(Entity* entity, uint32_t index, bool returnToZone, const SystemAddress &sysAddr); + void OnEnterProperty(Entity* entity, uint32_t index, bool returnToZone, const SystemAddress& sysAddr); /** * Handles a request for information on available properties when an entity lands on the property @@ -38,23 +38,13 @@ public: * @param playerOwn only query properties owned by the entity * @param updateUi unused * @param numResults unused - * @param reputation unused + * @param lReputationTime unused * @param sortMethod unused * @param startIndex the minimum index to start the query off * @param filterText property names to search for * @param sysAddr the address to send gamemessage responses to */ - void OnPropertyEntranceSync(Entity* entity, - bool includeNullAddress, - bool includeNullDescription, - bool playerOwn, - bool updateUi, - int32_t numResults, - int32_t reputation, - int32_t sortMethod, - int32_t startIndex, - std::string filterText, - const SystemAddress &sysAddr); + void OnPropertyEntranceSync(Entity* entity, bool includeNullAddress, bool includeNullDescription, bool playerOwn, bool updateUi, int32_t numResults, int32_t lReputationTime, int32_t sortMethod, int32_t startIndex, std::string filterText, const SystemAddress& sysAddr); /** * Returns the name of this property @@ -68,8 +58,11 @@ public: */ [[nodiscard]] LWOMAPID GetMapID() const { return m_MapID; }; -private: + PropertySelectQueryProperty SetPropertyValues(PropertySelectQueryProperty property, LWOCLONEID cloneId = LWOCLONEID_INVALID, std::string ownerName = "", std::string propertyName = "", std::string propertyDescription = "", float reputation = 0, bool isBFF = false, bool isFriend = false, bool isModeratorApproved = false, bool isAlt = false, bool isOwned = false, uint32_t privacyOption = 0, uint32_t timeLastUpdated = 0, float performanceCost = 0.0f); + std::string BuildQuery(Entity* entity, int32_t sortMethod, std::string customQuery = "", bool wantLimits = true); + + private: /** * Cache of property information that was queried for property launched, indexed by property ID */ @@ -84,4 +77,13 @@ private: * The base map ID for this property (Avant Grove, etc). */ LWOMAPID m_MapID; + + enum ePropertySortType : int32_t { + SORT_TYPE_FRIENDS = 0, + SORT_TYPE_REPUTATION = 1, + SORT_TYPE_RECENT = 3, + SORT_TYPE_FEATURED = 5 + }; + + std::string baseQueryForProperties = "SELECT p.* FROM properties as p JOIN charinfo as ci ON ci.prop_clone_id = p.clone_id where p.zone_id = ? AND (p.description LIKE ? OR p.name LIKE ? OR ci.name LIKE ?) AND p.privacy_option >= ? "; }; diff --git a/dGame/dComponents/PropertyManagementComponent.cpp b/dGame/dComponents/PropertyManagementComponent.cpp index 506c1608..f982326c 100644 --- a/dGame/dComponents/PropertyManagementComponent.cpp +++ b/dGame/dComponents/PropertyManagementComponent.cpp @@ -68,13 +68,18 @@ PropertyManagementComponent::PropertyManagementComponent(Entity* parent) : Compo this->owner = propertyEntry->getUInt64(2); this->owner = GeneralUtils::SetBit(this->owner, OBJECT_BIT_CHARACTER); this->owner = GeneralUtils::SetBit(this->owner, OBJECT_BIT_PERSISTENT); + this->clone_Id = propertyEntry->getInt(2); this->propertyName = propertyEntry->getString(5).c_str(); this->propertyDescription = propertyEntry->getString(6).c_str(); this->privacyOption = static_cast(propertyEntry->getUInt(9)); - this->claimedTime = propertyEntry->getUInt64(13); + this->moderatorRequested = propertyEntry->getInt(10) == 0 && rejectionReason == "" && privacyOption == PropertyPrivacyOption::Public; + this->LastUpdatedTime = propertyEntry->getUInt64(11); + this->claimedTime = propertyEntry->getUInt64(12); + this->rejectionReason = propertyEntry->getString(13).asStdString(); + this->reputation = propertyEntry->getUInt(14); Load(); - } + } delete propertyLookup; } @@ -152,12 +157,18 @@ void PropertyManagementComponent::SetPrivacyOption(PropertyPrivacyOption value) value = PropertyPrivacyOption::Private; } + if (value == PropertyPrivacyOption::Public && privacyOption != PropertyPrivacyOption::Public) { + rejectionReason = ""; + moderatorRequested = true; + } privacyOption = value; - auto* propertyUpdate = Database::CreatePreppedStmt("UPDATE properties SET privacy_option = ? WHERE id = ?;"); + auto* propertyUpdate = Database::CreatePreppedStmt("UPDATE properties SET privacy_option = ?, rejection_reason = ?, mod_approved = ? WHERE id = ?;"); propertyUpdate->setInt(1, static_cast(value)); - propertyUpdate->setInt64(2, propertyId); + propertyUpdate->setString(2, ""); + propertyUpdate->setInt(3, 0); + propertyUpdate->setInt64(4, propertyId); propertyUpdate->executeUpdate(); } @@ -181,38 +192,46 @@ void PropertyManagementComponent::UpdatePropertyDetails(std::string name, std::s OnQueryPropertyData(GetOwner(), UNASSIGNED_SYSTEM_ADDRESS); } -void PropertyManagementComponent::Claim(const LWOOBJID playerId) +bool PropertyManagementComponent::Claim(const LWOOBJID playerId) { if (owner != LWOOBJID_EMPTY) { - return; + return false; } - - SetOwnerId(playerId); - - auto* zone = dZoneManager::Instance()->GetZone(); - - const auto& worldId = zone->GetZoneID(); - const auto zoneId = worldId.GetMapID(); - const auto cloneId = worldId.GetCloneID(); auto* entity = EntityManager::Instance()->GetEntity(playerId); auto* user = entity->GetParentUser(); + auto character = entity->GetCharacter(); + if (!character) return false; + + auto* zone = dZoneManager::Instance()->GetZone(); + + const auto& worldId = zone->GetZoneID(); + const auto propertyZoneId = worldId.GetMapID(); + const auto propertyCloneId = worldId.GetCloneID(); + + const auto playerCloneId = character->GetPropertyCloneID(); + + // If we are not on our clone do not allow us to claim the property + if (propertyCloneId != playerCloneId) return false; + + SetOwnerId(playerId); + propertyId = ObjectIDManager::GenerateRandomObjectID(); - + auto* insertion = Database::CreatePreppedStmt( "INSERT INTO properties" - "(id, owner_id, template_id, clone_id, name, description, rent_amount, rent_due, privacy_option, last_updated, time_claimed, rejection_reason, reputation, zone_id)" - "VALUES (?, ?, ?, ?, ?, '', 0, 0, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '', 0, ?)" + "(id, owner_id, template_id, clone_id, name, description, rent_amount, rent_due, privacy_option, last_updated, time_claimed, rejection_reason, reputation, zone_id, performance_cost)" + "VALUES (?, ?, ?, ?, ?, '', 0, 0, 0, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), '', 0, ?, 0.0)" ); insertion->setUInt64(1, propertyId); insertion->setUInt64(2, (uint32_t) playerId); insertion->setUInt(3, templateId); - insertion->setUInt64(4, cloneId); + insertion->setUInt64(4, playerCloneId); insertion->setString(5, zone->GetZoneName().c_str()); - insertion->setInt(6, zoneId); + insertion->setInt(6, propertyZoneId); // Try and execute the query, print an error if it fails. try @@ -224,12 +243,14 @@ void PropertyManagementComponent::Claim(const LWOOBJID playerId) Game::logger->Log("PropertyManagementComponent", "Failed to execute query: (%s)!\n", exception.what()); throw exception; + return false; } auto* zoneControlObject = dZoneManager::Instance()->GetZoneControlObject(); for (CppScripts::Script* script : CppScripts::GetEntityScripts(zoneControlObject)) { script->OnZonePropertyRented(zoneControlObject, entity); } + return true; } void PropertyManagementComponent::OnStartBuilding() @@ -275,6 +296,8 @@ void PropertyManagementComponent::OnFinishBuilding() SetPrivacyOption(originalPrivacyOption); UpdateApprovedStatus(false); + + Save(); } void PropertyManagementComponent::UpdateModelPosition(const LWOOBJID id, const NiPoint3 position, NiQuaternion rotation) @@ -685,9 +708,12 @@ void PropertyManagementComponent::Save() auto* remove = Database::CreatePreppedStmt("DELETE FROM properties_contents WHERE id = ?;"); lookup->setUInt64(1, propertyId); - - auto* lookupResult = lookup->executeQuery(); - + sql::ResultSet* lookupResult = nullptr; + try { + lookupResult = lookup->executeQuery(); + } catch (sql::SQLException& ex) { + Game::logger->Log("PropertyManagementComponent", "lookup error %s\n", ex.what()); + } std::vector present; while (lookupResult->next()) @@ -730,8 +756,11 @@ void PropertyManagementComponent::Save() insertion->setDouble(9, rotation.y); insertion->setDouble(10, rotation.z); insertion->setDouble(11, rotation.w); - - insertion->execute(); + try { + insertion->execute(); + } catch (sql::SQLException& ex) { + Game::logger->Log("PropertyManagementComponent", "Error inserting into properties_contents. Error %s\n", ex.what()); + } } else { @@ -744,8 +773,11 @@ void PropertyManagementComponent::Save() update->setDouble(7, rotation.w); update->setInt64(8, id); - - update->executeUpdate(); + try { + update->executeUpdate(); + } catch (sql::SQLException& ex) { + Game::logger->Log("PropertyManagementComponent", "Error updating properties_contents. Error: %s\n", ex.what()); + } } } @@ -757,8 +789,11 @@ void PropertyManagementComponent::Save() } remove->setInt64(1, id); - - remove->execute(); + try { + remove->execute(); + } catch (sql::SQLException& ex) { + Game::logger->Log("PropertyManagementComponent", "Error removing from properties_contents. Error %s\n", ex.what()); + } } auto* removeUGC = Database::CreatePreppedStmt("DELETE FROM ugc WHERE id NOT IN (SELECT ugc_id FROM properties_contents);"); @@ -782,7 +817,7 @@ PropertyManagementComponent* PropertyManagementComponent::Instance() return instance; } -void PropertyManagementComponent::OnQueryPropertyData(Entity* originator, const SystemAddress& sysAddr, LWOOBJID author) const +void PropertyManagementComponent::OnQueryPropertyData(Entity* originator, const SystemAddress& sysAddr, LWOOBJID author) { if (author == LWOOBJID_EMPTY) { author = m_Parent->GetObjectID(); @@ -821,18 +856,44 @@ void PropertyManagementComponent::OnQueryPropertyData(Entity* originator, const description = propertyDescription; claimed = claimedTime; privacy = static_cast(this->privacyOption); - } + if (moderatorRequested) { + auto checkStatus = Database::CreatePreppedStmt("SELECT rejection_reason, mod_approved FROM properties WHERE id = ?;"); + checkStatus->setInt64(1, propertyId); + + auto result = checkStatus->executeQuery(); + + result->next(); + + const auto reason = result->getString(1).asStdString();; + const auto modApproved = result->getInt(2); + if (reason != "") { + moderatorRequested = false; + rejectionReason = reason; + } else if (reason == "" && modApproved == 1) { + moderatorRequested = false; + rejectionReason = ""; + } else { + moderatorRequested = true; + rejectionReason = ""; + } + } + } + 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; - + message.cloneId = clone_Id; + message.rejectionReason = rejectionReason; message.Paths = GetPaths(); SendDownloadPropertyData(author, message, UNASSIGNED_SYSTEM_ADDRESS); + // send rejection here? } void PropertyManagementComponent::OnUse(Entity* originator) diff --git a/dGame/dComponents/PropertyManagementComponent.h b/dGame/dComponents/PropertyManagementComponent.h index bf577760..c37282e9 100644 --- a/dGame/dComponents/PropertyManagementComponent.h +++ b/dGame/dComponents/PropertyManagementComponent.h @@ -1,5 +1,6 @@ #pragma once +#include #include "Entity.h" #include "Component.h" @@ -40,7 +41,7 @@ public: * @param sysAddr the address to send game message responses to * @param author optional explicit ID for the property, if not set defaults to the originator */ - void OnQueryPropertyData(Entity* originator, const SystemAddress& sysAddr, LWOOBJID author = LWOOBJID_EMPTY) const; + void OnQueryPropertyData(Entity* originator, const SystemAddress& sysAddr, LWOOBJID author = LWOOBJID_EMPTY); /** * Handles an OnUse event, telling the client who owns this property, etc. @@ -100,8 +101,10 @@ public: /** * Makes this property owned by the passed player ID, storing it in the database * @param playerId the ID of the entity that claimed the property + * + * @return If the claim is successful return true. */ - void Claim(LWOOBJID playerId); + bool Claim(LWOOBJID playerId); /** * Event triggered when the owner of the property starts building, will kick other entities out @@ -182,7 +185,7 @@ private: /** * The time since this property was claimed */ - uint64_t claimedTime = 0; + uint64_t claimedTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); /** * The models that are placed on this property @@ -194,11 +197,36 @@ private: */ std::string propertyName = ""; + /** + * The clone ID of this property + */ + LWOCLONEID clone_Id = 0; + + /** + * Whether a moderator was requested + */ + bool moderatorRequested = false; + + /** + * The rejection reason for the property + */ + std::string rejectionReason = ""; + /** * The description of this property */ std::string propertyDescription = ""; + /** + * The reputation of this property + */ + uint32_t reputation = 0; + + /** + * The last time this property was updated + */ + uint32_t LastUpdatedTime = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + /** * Determines which players may visit this property */ diff --git a/dGame/dComponents/PropertyVendorComponent.cpp b/dGame/dComponents/PropertyVendorComponent.cpp index e6b8fe97..72e8ad64 100644 --- a/dGame/dComponents/PropertyVendorComponent.cpp +++ b/dGame/dComponents/PropertyVendorComponent.cpp @@ -41,13 +41,16 @@ void PropertyVendorComponent::OnBuyFromVendor(Entity* originator, const bool con { if (PropertyManagementComponent::Instance() == nullptr) return; + if (PropertyManagementComponent::Instance()->Claim(originator->GetObjectID()) == false) { + Game::logger->Log("PropertyVendorComponent", "FAILED TO CLAIM PROPERTY. PLAYER ID IS %llu\n", originator->GetObjectID()); + return; + } + GameMessages::SendPropertyRentalResponse(m_Parent->GetObjectID(), 0, 0, 0, 0, originator->GetSystemAddress()); auto* controller = dZoneManager::Instance()->GetZoneControlObject(); controller->OnFireEventServerSide(m_Parent, "propertyRented"); - - PropertyManagementComponent::Instance()->Claim(originator->GetObjectID()); PropertyManagementComponent::Instance()->SetOwner(originator); diff --git a/dGame/dGameMessages/GameMessageHandler.cpp b/dGame/dGameMessages/GameMessageHandler.cpp index 83187e2f..45760cc9 100644 --- a/dGame/dGameMessages/GameMessageHandler.cpp +++ b/dGame/dGameMessages/GameMessageHandler.cpp @@ -595,7 +595,9 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream* inStream, const System case GAME_MSG_VEHICLE_NOTIFY_HIT_IMAGINATION_SERVER: GameMessages::HandleVehicleNotifyHitImaginationServer(inStream, entity, sysAddr); break; - + case GAME_MSG_UPDATE_PROPERTY_PERFORMANCE_COST: + GameMessages::HandleUpdatePropertyPerformanceCost(inStream, entity, sysAddr); + break; // SG case GAME_MSG_UPDATE_SHOOTING_GALLERY_ROTATION: GameMessages::HandleUpdateShootingGalleryRotation(inStream, entity, sysAddr); diff --git a/dGame/dGameMessages/GameMessages.cpp b/dGame/dGameMessages/GameMessages.cpp index 53699f4e..1a1795f2 100644 --- a/dGame/dGameMessages/GameMessages.cpp +++ b/dGame/dGameMessages/GameMessages.cpp @@ -4137,6 +4137,41 @@ void GameMessages::HandleRacingPlayerInfoResetFinished(RakNet::BitStream* inStre } } +void GameMessages::SendUpdateReputation(const LWOOBJID objectId, const int64_t reputation, const SystemAddress& sysAddr) { + CBITSTREAM; + CMSGHEADER; + + bitStream.Write(objectId); + bitStream.Write(GAME_MSG::GAME_MSG_UPDATE_REPUTATION); + + bitStream.Write(reputation); + + 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 = dZoneManager::Instance()->GetZone(); + const auto& worldId = zone->GetZoneID(); + const auto cloneId = worldId.GetCloneID(); + const auto zoneId = worldId.GetMapID(); + + auto updatePerformanceCostQuery = Database::CreatePreppedStmt("UPDATE properties SET performance_cost = ? WHERE clone_id = ? AND zone_id = ?"); + + updatePerformanceCostQuery->setDouble(1, performanceCost); + updatePerformanceCostQuery->setInt(2, cloneId); + updatePerformanceCostQuery->setInt(3, zoneId); + + updatePerformanceCostQuery->executeUpdate(); + + delete updatePerformanceCostQuery; + updatePerformanceCostQuery = nullptr; +} void GameMessages::HandleVehicleNotifyHitImaginationServer(RakNet::BitStream* inStream, Entity* entity, const SystemAddress& sysAddr) { diff --git a/dGame/dGameMessages/GameMessages.h b/dGame/dGameMessages/GameMessages.h index 671c3b3a..ababed5f 100644 --- a/dGame/dGameMessages/GameMessages.h +++ b/dGame/dGameMessages/GameMessages.h @@ -386,6 +386,8 @@ 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, @@ -395,6 +397,8 @@ namespace GameMessages { void HandleUpdateShootingGalleryRotation(RakNet::BitStream* inStream, Entity* entity, const SystemAddress& sysAddr); + void SendUpdateReputation(const LWOOBJID objectId, const int64_t reputation, const SystemAddress& sysAddr); + // Leaderboards void SendActivitySummaryLeaderboardData(const LWOOBJID& objectID, const Leaderboard* leaderboard, const SystemAddress& sysAddr = UNASSIGNED_SYSTEM_ADDRESS); diff --git a/dGame/dGameMessages/PropertyDataMessage.cpp b/dGame/dGameMessages/PropertyDataMessage.cpp index 9c3b6d3f..85eb54cb 100644 --- a/dGame/dGameMessages/PropertyDataMessage.cpp +++ b/dGame/dGameMessages/PropertyDataMessage.cpp @@ -13,7 +13,7 @@ void GameMessages::PropertyDataMessage::Serialize(RakNet::BitStream& stream) con stream.Write(TemplateID); // - template id stream.Write(ZoneId); // - map id stream.Write(VendorMapId); // - vendor map id - stream.Write(1); + stream.Write(cloneId); // clone id const auto& name = GeneralUtils::ASCIIToUTF16(Name); stream.Write(uint32_t(name.size())); @@ -40,11 +40,12 @@ void GameMessages::PropertyDataMessage::Serialize(RakNet::BitStream& stream) con stream.Write(0); // - minimum price stream.Write(1); // - rent duration - stream.Write(ClaimedTime); // - timestamp + stream.Write(LastUpdatedTime); // - timestamp stream.Write(1); - stream.Write(0); + stream.Write(reputation); // Reputation + stream.Write(0); const auto& spawn = GeneralUtils::ASCIIToUTF16(SpawnName); stream.Write(uint32_t(spawn.size())); @@ -63,9 +64,18 @@ void GameMessages::PropertyDataMessage::Serialize(RakNet::BitStream& stream) con stream.Write(0); - stream.Write(1); + if (rejectionReason != "") stream.Write(REJECTION_STATUS_REJECTED); + else if (moderatorRequested == true && rejectionReason == "") stream.Write(REJECTION_STATUS_APPROVED); + else stream.Write(REJECTION_STATUS_PENDING); - stream.Write(0); // String length + // Does this go here??? + // const auto& rejectionReasonConverted = GeneralUtils::ASCIIToUTF16(rejectionReason); + // stream.Write(uint32_t(rejectionReasonConverted.size())); + // for (uint32_t i = 0; i < rejectionReasonConverted.size(); ++i) { + // stream.Write(uint16_t(rejectionReasonConverted[i])); + // } + + stream.Write(0); stream.Write(0); diff --git a/dGame/dGameMessages/PropertyDataMessage.h b/dGame/dGameMessages/PropertyDataMessage.h index 219ac08d..5b5d7d0f 100644 --- a/dGame/dGameMessages/PropertyDataMessage.h +++ b/dGame/dGameMessages/PropertyDataMessage.h @@ -28,12 +28,23 @@ namespace GameMessages 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 + }; }; } \ No newline at end of file diff --git a/dGame/dGameMessages/PropertySelectQueryProperty.cpp b/dGame/dGameMessages/PropertySelectQueryProperty.cpp index f32ce537..38e7cea2 100644 --- a/dGame/dGameMessages/PropertySelectQueryProperty.cpp +++ b/dGame/dGameMessages/PropertySelectQueryProperty.cpp @@ -22,15 +22,16 @@ void PropertySelectQueryProperty::Serialize(RakNet::BitStream& stream) const stream.Write(static_cast(description[i])); } - stream.Write(Reputation); - stream.Write(IsBestFriend); - stream.Write(IsFriend); - stream.Write(IsModeratorApproved); - stream.Write(IsAlt); - stream.Write(IsOwner); - stream.Write(AccessType); - stream.Write(DatePublished); - stream.Write(PerformanceCost); + 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(PerformanceIndex); + stream.Write(PerformanceCost); } void PropertySelectQueryProperty::Deserialize(RakNet::BitStream& stream) const diff --git a/dGame/dGameMessages/PropertySelectQueryProperty.h b/dGame/dGameMessages/PropertySelectQueryProperty.h index 0aaab912..61fa7b86 100644 --- a/dGame/dGameMessages/PropertySelectQueryProperty.h +++ b/dGame/dGameMessages/PropertySelectQueryProperty.h @@ -9,17 +9,18 @@ public: void Deserialize(RakNet::BitStream& stream) const; - LWOCLONEID CloneId = LWOCLONEID_INVALID; - std::string OwnerName = ""; - std::string Name = ""; - std::string Description = ""; - uint32_t Reputation = 0; - bool IsBestFriend = false; - bool IsFriend = false; - bool IsModeratorApproved = false; - bool IsAlt = false; - bool IsOwner = false; - uint32_t AccessType = 0; - uint32_t DatePublished = 0; - uint64_t PerformanceCost = 0; + 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 + uint32_t DateLastPublished = 0; // The last day the property was published + float PerformanceCost = 0; // The performance cost of the property + uint32_t PerformanceIndex = 0; // The performance index of the property? Always 0? }; diff --git a/dGame/dMission/Mission.cpp b/dGame/dMission/Mission.cpp index 2719a6cf..748bebd4 100644 --- a/dGame/dMission/Mission.cpp +++ b/dGame/dMission/Mission.cpp @@ -16,6 +16,7 @@ #include "dLogger.h" #include "dServer.h" #include "dZoneManager.h" +#include "Database.h" Mission::Mission(MissionComponent* missionComponent, const uint32_t missionId) { m_MissionComponent = missionComponent; @@ -516,8 +517,25 @@ void Mission::YieldRewards() { } if (info->reward_reputation > 0) { - // TODO: Track reputation in the character and database. missionComponent->Progress(MissionTaskType::MISSION_TASK_TYPE_EARN_REPUTATION, 0, 0L, "", info->reward_reputation); + auto character = entity->GetCharacter(); + if (!character) return; + + auto charId = character->GetID(); + auto propertyCloneId = character->GetPropertyCloneID(); + + auto reputationUpdate = Database::CreatePreppedStmt("UPDATE properties SET reputation = reputation + ? where owner_id = ? AND clone_id = ?"); + + reputationUpdate->setInt64(1, info->reward_reputation); + reputationUpdate->setInt(2, charId); + reputationUpdate->setInt64(3, propertyCloneId); + + reputationUpdate->executeUpdate(); + + delete reputationUpdate; + reputationUpdate = nullptr; + + GameMessages::SendUpdateReputation(entity->GetObjectID(), info->reward_reputation, entity->GetSystemAddress()); } if (info->reward_maxhealth > 0) { diff --git a/dNet/dMessageIdentifiers.h b/dNet/dMessageIdentifiers.h index 36e85df2..eae3f88a 100644 --- a/dNet/dMessageIdentifiers.h +++ b/dNet/dMessageIdentifiers.h @@ -382,6 +382,7 @@ enum GAME_MSG : unsigned short { GAME_MSG_PROPERTY_EDITOR_END = 725, GAME_MSG_START_PATHING = 735, GAME_MSG_NOTIFY_CLIENT_ZONE_OBJECT = 737, + GAME_MSG_UPDATE_REPUTATION = 746, GAME_MSG_PROPERTY_RENTAL_RESPONSE = 750, GAME_MSG_REQUEST_PLATFORM_RESYNC = 760, GAME_MSG_PLATFORM_RESYNC = 761, @@ -527,6 +528,7 @@ enum GAME_MSG : unsigned short { GAME_MSG_VEHICLE_NOTIFY_HIT_IMAGINATION_SERVER = 1606, GAME_MSG_ADD_RUN_SPEED_MODIFIER = 1505, GAME_MSG_REMOVE_RUN_SPEED_MODIFIER = 1506, + GAME_MSG_UPDATE_PROPERTY_PERFORMANCE_COST = 1547, GAME_MSG_PROPERTY_ENTRANCE_BEGIN = 1553, GAME_MSG_REQUEST_MOVE_ITEM_BETWEEN_INVENTORY_TYPES = 1666, GAME_MSG_RESPONSE_MOVE_ITEM_BETWEEN_INVENTORY_TYPES = 1667, diff --git a/migrations/dlu/3_add_performance_cost.sql b/migrations/dlu/3_add_performance_cost.sql new file mode 100644 index 00000000..15da4470 --- /dev/null +++ b/migrations/dlu/3_add_performance_cost.sql @@ -0,0 +1 @@ +ALTER TABLE properties ADD COLUMN performance_cost DOUBLE(20, 15) DEFAULT 0.0; \ No newline at end of file