Implement undo action for pre-built models (#830)

Brick building as of right now does not implement the undo action properly.  This commit addresses the issue with undoing button being non-functional server side and implements the GM needed for addressing further issues.

Implement GameMessage UnUseModel which is called when a model in BrickBuilding is UnUsed.  Important for UGC content down the line.  Final code has been tested as follows:
1. Placed a model in brick build
2. saved placed a brick
3. repeat 2 and 3 twice more for 6 total models
4. Place a new model in brick mode and then edit all 7 models into one brick model instance
5. Pressing undo returns the converted model to the inventory and properly discards the other 6 without crashing.  Intended live behavior is to store this in the inventory instead however behind the scenes work is needed to implement UGC models properly.

Implement enum

Implement the BlueprintSaveResponseType enum so there are less magic numbers sent via packets.
Correct int sizes from unsigned int to uint32_t

Add deserialize test

Add a test for de-serializing a GM that is sent to the client.  Assertions verify the data is in the correct order and has no extra information.
This commit is contained in:
David Markowitz 2022-11-27 16:48:46 -08:00 committed by GitHub
parent 3939f19b08
commit 3222e78815
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 188 additions and 26 deletions

View File

@ -434,6 +434,7 @@ enum eInventoryType : uint32_t {
ITEMS = 0,
VAULT_ITEMS,
BRICKS,
MODELS_IN_BBB,
TEMP_ITEMS = 4,
MODELS,
TEMP_MODELS,

View File

@ -433,6 +433,7 @@ enum GAME_MSG : unsigned short {
GAME_MSG_ORIENT_TO_POSITION = 906,
GAME_MSG_ORIENT_TO_ANGLE = 907,
GAME_MSG_BOUNCER_ACTIVE_STATUS = 942,
GAME_MSG_UN_USE_BBB_MODEL = 999,
GAME_MSG_BBB_LOAD_ITEM_REQUEST = 1000,
GAME_MSG_BBB_SAVE_REQUEST = 1001,
GAME_MSG_BBB_SAVE_RESPONSE = 1006,

View File

@ -0,0 +1,26 @@
#pragma once
#ifndef __EBLUEPRINTSAVERESPONSETYPE__H__
#define __EBLUEPRINTSAVERESPONSETYPE__H__
#include <cstdint>
enum class eBlueprintSaveResponseType : uint32_t {
EverythingWorked = 0,
SaveCancelled,
CantBeginTransaction,
SaveBlueprintFailed,
SaveUgobjectFailed,
CantEndTransaction,
SaveFilesFailed,
BadInput,
NotEnoughBricks,
InventoryFull,
ModelGenerationFailed,
PlacementFailed,
GmLevelInsufficient,
WaitForPreviousSave,
FindMatchesFailed
};
#endif //!__EBLUEPRINTSAVERESPONSETYPE__H__

View File

@ -606,16 +606,17 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument* document) {
return;
}
std::vector<Inventory*> inventories;
std::vector<Inventory*> inventoriesToSave;
// Need to prevent some transfer inventories from being saved
for (const auto& pair : this->m_Inventories) {
auto* inventory = pair.second;
if (inventory->GetType() == VENDOR_BUYBACK) {
if (inventory->GetType() == VENDOR_BUYBACK || inventory->GetType() == eInventoryType::MODELS_IN_BBB) {
continue;
}
inventories.push_back(inventory);
inventoriesToSave.push_back(inventory);
}
inventoryElement->SetAttribute("csl", m_Consumable);
@ -630,7 +631,7 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument* document) {
bags->DeleteChildren();
for (const auto* inventory : inventories) {
for (const auto* inventory : inventoriesToSave) {
auto* bag = document->NewElement("b");
bag->SetAttribute("t", inventory->GetType());
@ -649,7 +650,7 @@ void InventoryComponent::UpdateXml(tinyxml2::XMLDocument* document) {
items->DeleteChildren();
for (auto* inventory : inventories) {
for (auto* inventory : inventoriesToSave) {
if (inventory->GetSize() == 0) {
continue;
}
@ -1260,7 +1261,7 @@ BehaviorSlot InventoryComponent::FindBehaviorSlot(const eItemType type) {
}
bool InventoryComponent::IsTransferInventory(eInventoryType type) {
return type == VENDOR_BUYBACK || type == VAULT_ITEMS || type == VAULT_MODELS || type == TEMP_ITEMS || type == TEMP_MODELS;
return type == VENDOR_BUYBACK || type == VAULT_ITEMS || type == VAULT_MODELS || type == TEMP_ITEMS || type == TEMP_MODELS || type == MODELS_IN_BBB;
}
uint32_t InventoryComponent::FindSkill(const LOT lot) {

View File

@ -474,7 +474,7 @@ void PropertyManagementComponent::DeleteModel(const LWOOBJID id, const int delet
settings.push_back(propertyObjectID);
settings.push_back(modelType);
inventoryComponent->AddItem(6662, 1, eLootSourceType::LOOT_SOURCE_DELETION, eInventoryType::HIDDEN, settings, LWOOBJID_EMPTY, false, false, spawnerId);
inventoryComponent->AddItem(6662, 1, eLootSourceType::LOOT_SOURCE_DELETION, eInventoryType::MODELS_IN_BBB, settings, LWOOBJID_EMPTY, false, false, spawnerId);
auto* item = inventoryComponent->FindItemBySubKey(spawnerId);
if (item == nullptr) {

View File

@ -46,6 +46,10 @@ void GameMessageHandler::HandleMessage(RakNet::BitStream* inStream, const System
switch (messageID) {
case GAME_MSG_UN_USE_BBB_MODEL: {
GameMessages::HandleUnUseModel(inStream, entity, sysAddr);
break;
}
case GAME_MSG_PLAY_EMOTE: {
GameMessages::HandlePlayEmote(inStream, entity);
break;

View File

@ -70,6 +70,7 @@
#include "TradingManager.h"
#include "ControlBehaviors.h"
#include "AMFDeserialize.h"
#include "eBlueprintSaveResponseType.h"
void GameMessages::SendFireEventClientSide(const LWOOBJID& objectID, const SystemAddress& sysAddr, std::u16string args, const LWOOBJID& object, int64_t param1, int param2, const LWOOBJID& sender) {
CBITSTREAM;
@ -2139,6 +2140,32 @@ void GameMessages::HandleSetPropertyAccess(RakNet::BitStream* inStream, Entity*
PropertyManagementComponent::Instance()->SetPrivacyOption(static_cast<PropertyPrivacyOption>(accessType));
}
void GameMessages::HandleUnUseModel(RakNet::BitStream* inStream, Entity* entity, const SystemAddress& sysAddr) {
bool unknown{};
LWOOBJID objIdToAddToInventory{};
inStream->Read(unknown);
inStream->Read(objIdToAddToInventory);
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent) {
auto* inventory = inventoryComponent->GetInventory(eInventoryType::MODELS_IN_BBB);
auto* item = inventory->FindItemById(objIdToAddToInventory);
if (item) {
inventoryComponent->MoveItemToInventory(item, eInventoryType::MODELS, 1);
} else {
Game::logger->Log("GameMessages", "item id %llu not found in MODELS_IN_BBB inventory, likely because it does not exist", objIdToAddToInventory);
}
}
if (unknown) {
CBITSTREAM;
PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_BLUEPRINT_SAVE_RESPONSE);
bitStream.Write<LWOOBJID>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
bitStream.Write(eBlueprintSaveResponseType::PlacementFailed); // Sending a non-zero error code here prevents the client from deleting its in progress build for some reason?
bitStream.Write<uint32_t>(0);
SEND_PACKET;
}
}
void GameMessages::HandleUpdatePropertyOrModelForFilterCheck(RakNet::BitStream* inStream, Entity* entity, const SystemAddress& sysAddr) {
bool isProperty{};
LWOOBJID objectId{};
@ -2353,16 +2380,40 @@ void GameMessages::HandleDeletePropertyModel(RakNet::BitStream* inStream, Entity
}
void GameMessages::HandleBBBLoadItemRequest(RakNet::BitStream* inStream, Entity* entity, const SystemAddress& sysAddr) {
LWOOBJID itemID = LWOOBJID_EMPTY;
inStream->Read(itemID);
LWOOBJID previousItemID = LWOOBJID_EMPTY;
inStream->Read(previousItemID);
Game::logger->Log("BBB", "Load item request for: %lld", itemID);
Game::logger->Log("BBB", "Load item request for: %lld", previousItemID);
LWOOBJID newId = previousItemID;
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent) {
auto* inventory = inventoryComponent->GetInventory(eInventoryType::MODELS);
auto* itemToMove = inventory->FindItemById(previousItemID);
if (itemToMove) {
LOT previousLot = itemToMove->GetLot();
inventoryComponent->MoveItemToInventory(itemToMove, eInventoryType::MODELS_IN_BBB, 1, false);
auto* destinationInventory = inventoryComponent->GetInventory(eInventoryType::MODELS_IN_BBB);
if (destinationInventory) {
auto* movedItem = destinationInventory->FindItemByLot(previousLot);
if (movedItem) newId = movedItem->GetId();
}
} else {
Game::logger->Log("GameMessages", "item id %llu not found in MODELS inventory, likely because it does not exist", previousItemID);
}
}
// Second argument always true (successful) for now
SendBlueprintLoadItemResponse(sysAddr, true, previousItemID, newId);
}
void GameMessages::SendBlueprintLoadItemResponse(const SystemAddress& sysAddr, bool success, LWOOBJID oldItemId, LWOOBJID newItemId) {
CBITSTREAM;
PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_BLUEPRINT_LOAD_RESPONSE_ITEMID);
bitStream.Write(static_cast<uint8_t>(1));
bitStream.Write<LWOOBJID>(itemID);
bitStream.Write<LWOOBJID>(itemID);
bitStream.Write(static_cast<uint8_t>(success));
bitStream.Write<LWOOBJID>(oldItemId);
bitStream.Write<LWOOBJID>(newItemId);
SEND_PACKET;
}
@ -2413,7 +2464,7 @@ void GameMessages::HandleControlBehaviors(RakNet::BitStream* inStream, Entity* e
}
auto owner = PropertyManagementComponent::Instance()->GetOwner();
if (!owner) return;
if (!owner) return;
ControlBehaviors::ProcessCommand(entity, sysAddr, static_cast<AMFArrayValue*>(amfArguments.get()), command, owner);
}
@ -2609,8 +2660,8 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream* inStream, Entity* ent
CBITSTREAM;
PacketUtils::WriteHeader(bitStream, CLIENT, CLIENT::MSG_CLIENT_BLUEPRINT_SAVE_RESPONSE);
bitStream.Write(localId);
bitStream.Write<unsigned int>(0);
bitStream.Write<unsigned int>(1);
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
bitStream.Write<uint32_t>(1);
bitStream.Write(blueprintID);
bitStream.Write<uint32_t>(sd0Size);
@ -2620,7 +2671,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream* inStream, Entity* ent
}
SEND_PACKET;
// PacketUtils::SavePacket("MSG_CLIENT_BLUEPRINT_SAVE_RESPONSE.bin", (char*)bitStream.GetData(), bitStream.GetNumberOfBytesUsed());
//Now we have to construct this object:

View File

@ -121,6 +121,7 @@ namespace GameMessages {
void SendMatchResponse(Entity* entity, const SystemAddress& sysAddr, int response);
void SendMatchUpdate(Entity* entity, const SystemAddress& sysAddr, std::string data, int type);
void HandleUnUseModel(RakNet::BitStream* inStream, Entity* entity, const SystemAddress& sysAddr);
void SendStartCelebrationEffect(Entity* entity, const SystemAddress& sysAddr, int celebrationID);
/**
@ -197,6 +198,16 @@ namespace GameMessages {
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
*
* @param sysAddr SystemAddress to respond to
* @param oldItemId The item ID that was requested to be loaded
* @param newItemId The new item ID of the loaded item
*
*/
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);

View File

@ -95,7 +95,7 @@ public:
* @param lot the lot to find items for
* @param ignoreEquipped ignores equipped items
* @param ignoreBound ignores bound items
* @return item in the inventory for the provided LOT
* @return item with the lowest stack count in the inventory for the provided LOT
*/
Item* FindItemByLot(LOT lot, bool ignoreEquipped = false, bool ignoreBound = false) const;

View File

@ -51,13 +51,13 @@ uint32_t ObjectIDManager::GeneratePersistentID(void) {
uint32_t toReturn = ++this->currentPersistentID;
// So we peroidically save our ObjID to the database:
if (toReturn % 25 == 0) { // TEMP: DISABLED FOR DEBUG / DEVELOPMENT!
// if (toReturn % 25 == 0) { // TEMP: DISABLED FOR DEBUG / DEVELOPMENT!
sql::PreparedStatement* stmt = Database::CreatePreppedStmt(
"UPDATE object_id_tracker SET last_object_id=?");
stmt->setUInt(1, toReturn);
stmt->execute();
delete stmt;
}
// }
return toReturn;
}

View File

@ -57,6 +57,7 @@
#include "Player.h"
#include "PropertyManagementComponent.h"
#include "AssetManager.h"
#include "eBlueprintSaveResponseType.h"
#include "ZCompression.h"
@ -1082,9 +1083,9 @@ void HandlePacket(Packet* packet) {
CBITSTREAM;
PacketUtils::WriteHeader(bitStream, CLIENT, MSG_CLIENT_BLUEPRINT_SAVE_RESPONSE);
bitStream.Write<LWOOBJID>(0); //always zero so that a check on the client passes
bitStream.Write<unsigned int>(0);
bitStream.Write<unsigned int>(1);
bitStream.Write<LWOOBJID>(LWOOBJID_EMPTY); //always zero so that a check on the client passes
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
bitStream.Write<uint32_t>(1);
bitStream.Write(blueprintID);
bitStream.Write<uint32_t>(lxfmlSize);

View File

@ -5,6 +5,9 @@ set(DGAMETEST_SOURCES
add_subdirectory(dComponentsTests)
list(APPEND DGAMETEST_SOURCES ${DCOMPONENTS_TESTS})
add_subdirectory(dGameMessagesTests)
list(APPEND DGAMETEST_SOURCES ${DGAMEMESSAGES_TESTS})
# Add the executable. Remember to add all tests above this!
add_executable(dGameTests ${DGAMETEST_SOURCES})

View File

@ -5,15 +5,18 @@
#include "dLogger.h"
#include "dServer.h"
#include "EntityManager.h"
class dZoneManager;
class AssetManager;
#include <gtest/gtest.h>
class dZoneManager;
class AssetManager;
class dServerMock : public dServer {
RakNet::BitStream* sentBitStream = nullptr;
public:
dServerMock() {};
~dServerMock() {};
void Send(RakNet::BitStream* bitStream, const SystemAddress& sysAddr, bool broadcast) override {};
RakNet::BitStream* GetMostRecentBitStream() { return sentBitStream; };
void Send(RakNet::BitStream* bitStream, const SystemAddress& sysAddr, bool broadcast) override { sentBitStream = bitStream; };
};
class GameDependenciesTest : public ::testing::Test {

View File

@ -0,0 +1,9 @@
SET(DGAMEMESSAGES_TESTS
"GameMessageTests.cpp")
# Get the folder name and prepend it to the files above
get_filename_component(thisFolderName ${CMAKE_CURRENT_SOURCE_DIR} NAME)
list(TRANSFORM DGAMEMESSAGES_TESTS PREPEND "${thisFolderName}/")
# Export to parent scope
set(DGAMEMESSAGES_TESTS ${DGAMEMESSAGES_TESTS} PARENT_SCOPE)

View File

@ -0,0 +1,52 @@
#include "GameMessages.h"
#include "GameDependencies.h"
#include <gtest/gtest.h>
class GameMessageTests : public GameDependenciesTest {
protected:
void SetUp() override {
SetUpDependencies();
}
void TearDown() override {
TearDownDependencies();
}
};
/**
* @brief Tests that the serialization struct BlueprintLoadItemResponse is serialized correctly
*
*/
TEST_F(GameMessageTests, SendBlueprintLoadItemResponse) {
GameMessages::SendBlueprintLoadItemResponse(UNASSIGNED_SYSTEM_ADDRESS, true, 515, 990);
auto* bitStream = static_cast<dServerMock*>(Game::server)->GetMostRecentBitStream();
ASSERT_NE(bitStream, nullptr);
ASSERT_EQ(bitStream->GetNumberOfUnreadBits(), 200);
// First read in the packets' header
uint8_t rakNetPacketId{};
uint16_t remoteConnectionType{};
uint32_t packetId{};
uint8_t always0{};
bitStream->Read(rakNetPacketId);
bitStream->Read(remoteConnectionType);
bitStream->Read(packetId);
bitStream->Read(always0);
ASSERT_EQ(rakNetPacketId, 0x53);
ASSERT_EQ(remoteConnectionType, 0x05);
ASSERT_EQ(packetId, 0x17);
ASSERT_EQ(always0, 0x00);
// Next read in packet data
uint8_t bSuccess{}; // unsigned bool
LWOOBJID previousId{};
LWOOBJID newId{};
bitStream->Read(bSuccess);
bitStream->Read(previousId);
bitStream->Read(newId);
ASSERT_EQ(bSuccess, static_cast<uint8_t>(true));
ASSERT_EQ(previousId, 515);
ASSERT_EQ(newId, 990);
ASSERT_EQ(bitStream->GetNumberOfUnreadBits(), 0);
}