feat: lxfml splitting for bbb (#1877)

* LXFML SPLITTING
Included test file

* move base to global namespace

* wip need to test

* update last fixes

* update world sending bbb to be more efficient

* Address feedback form Emo in doscord

* Make LXFML class for robust and add more tests to edge cases and malformed data

* get rid of the string copy and make the deep clone have a recursive limit

* cleanup tests

* fix test file locations

* fix file path

* KISS

* add cmakelists

* fix typos

* NL @ EOF

* tabs and split out to func

* naming standard
This commit is contained in:
Aaron Kimbrell
2025-10-10 23:07:16 -05:00
committed by GitHub
parent cbdd5d9bc6
commit ce28834dce
16 changed files with 1267 additions and 118 deletions

View File

@@ -2566,9 +2566,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
inStream.Read(timeTaken);
/*
Disabled this, as it's kinda silly to do this roundabout way of storing plaintext lxfml, then recompressing
it to send it back to the client.
On DLU we had agreed that bricks wouldn't be taken anyway, but if your server decides otherwise, feel free to
comment this back out and add the needed code to get the bricks used from lxfml and take them from the inventory.
@@ -2582,23 +2579,6 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
//We need to get a new ID for our model first:
if (!entity || !entity->GetCharacter() || !entity->GetCharacter()->GetParentUser()) return;
const uint32_t maxRetries = 100;
uint32_t retries = 0;
bool blueprintIDExists = true;
bool modelExists = true;
// Legacy logic to check for old random IDs (regenerating these is not really feasible)
// Probably good to have this anyway in case someone messes with the last_object_id or it gets reset somehow
LWOOBJID newIDL = LWOOBJID_EMPTY;
LWOOBJID blueprintID = LWOOBJID_EMPTY;
do {
if (newIDL != LWOOBJID_EMPTY) LOG("Generating blueprintID for UGC model, collision with existing model ID: %llu", blueprintID);
newIDL = ObjectIDManager::GetPersistentID();
blueprintID = ObjectIDManager::GetPersistentID();
++retries;
blueprintIDExists = Database::Get()->GetUgcModel(blueprintID).has_value();
modelExists = Database::Get()->GetModel(newIDL).has_value();
} while ((blueprintIDExists || modelExists) && retries < maxRetries);
//We need to get the propertyID: (stolen from Wincent's propertyManagementComp)
const auto& worldId = Game::zoneManager->GetZone()->GetZoneID();
@@ -2615,85 +2595,112 @@ void GameMessages::HandleBBBSaveRequest(RakNet::BitStream& inStream, Entity* ent
std::istringstream sd0DataStream(str);
Sd0 sd0(sd0DataStream);
// Uncompress the data and normalize the position
// Uncompress the data, split, and nornmalize the model
const auto asStr = sd0.GetAsStringUncompressed();
const auto [newLxfml, newCenter] = Lxfml::NormalizePosition(asStr);
auto splitLxfmls = Lxfml::Split(asStr);
LOG_DEBUG("Split into %zu models", splitLxfmls.size());
// Recompress the data and save to the database
sd0.FromData(reinterpret_cast<const uint8_t*>(newLxfml.data()), newLxfml.size());
auto sd0AsStream = sd0.GetAsStream();
Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintID, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID());
//Insert into the db as a BBB model:
IPropertyContents::Model model;
model.id = newIDL;
model.ugcId = blueprintID;
model.position = newCenter;
model.rotation = NiQuaternion(0.0f, 0.0f, 0.0f, 0.0f);
model.lot = 14;
Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_14_name");
/*
Commented out until UGC server would be updated to use a sd0 file instead of lxfml stream.
(or you uncomment the lxfml decomp stuff above)
*/
// //Send off to UGC for processing, if enabled:
// if (Game::config->GetValue("ugc_remote") == "1") {
// std::string ugcIP = Game::config->GetValue("ugc_ip");
// int ugcPort = std::stoi(Game::config->GetValue("ugc_port"));
// httplib::Client cli(ugcIP, ugcPort); //connect to UGC HTTP server using our config above ^
// //Send out a request:
// std::string request = "/3dservices/UGCC150/150" + std::to_string(blueprintID) + ".lxfml";
// cli.Put(request.c_str(), lxfml.c_str(), "text/lxfml");
// //When the "put" above returns, it means that the UGC HTTP server is done processing our model &
// //the nif, hkx and checksum files are ready to be downloaded from cache.
// }
//Tell the client their model is saved: (this causes us to actually pop out of our current state):
const auto& newSd0 = sd0.GetAsVector();
uint32_t newSd0Size{};
for (const auto& chunk : newSd0) newSd0Size += chunk.size();
CBITSTREAM;
BitStreamUtils::WriteHeader(bitStream, ServiceType::CLIENT, MessageType::Client::BLUEPRINT_SAVE_RESPONSE);
bitStream.Write(localId);
bitStream.Write(eBlueprintSaveResponseType::EverythingWorked);
bitStream.Write<uint32_t>(1);
bitStream.Write(blueprintID);
bitStream.Write<uint32_t>(splitLxfmls.size());
bitStream.Write(newSd0Size);
std::vector<LWOOBJID> blueprintIDs;
std::vector<LWOOBJID> modelIDs;
for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
for (size_t i = 0; i < splitLxfmls.size(); ++i) {
// Legacy logic to check for old random IDs (regenerating these is not really feasible)
// Probably good to have this anyway in case someone messes with the last_object_id or it gets reset somehow
const uint32_t maxRetries = 100;
uint32_t retries = 0;
bool blueprintIDExists = true;
bool modelExists = true;
LWOOBJID newID = LWOOBJID_EMPTY;
LWOOBJID blueprintID = LWOOBJID_EMPTY;
do {
if (newID != LWOOBJID_EMPTY) LOG("Generating blueprintID for UGC model, collision with existing model ID: %llu", blueprintID);
newID = ObjectIDManager::GetPersistentID();
blueprintID = ObjectIDManager::GetPersistentID();
++retries;
blueprintIDExists = Database::Get()->GetUgcModel(blueprintID).has_value();
modelExists = Database::Get()->GetModel(newID).has_value();
} while ((blueprintIDExists || modelExists) && retries < maxRetries);
blueprintIDs.push_back(blueprintID);
modelIDs.push_back(newID);
// Save each model to the database
sd0.FromData(reinterpret_cast<const uint8_t*>(splitLxfmls[i].lxfml.data()), splitLxfmls[i].lxfml.size());
auto sd0AsStream = sd0.GetAsStream();
Database::Get()->InsertNewUgcModel(sd0AsStream, blueprintID, entity->GetCharacter()->GetParentUser()->GetAccountID(), entity->GetCharacter()->GetID());
// Insert the new property model
IPropertyContents::Model model;
model.id = newID;
model.ugcId = blueprintID;
model.position = splitLxfmls[i].center;
model.rotation = QuatUtils::IDENTITY;
model.lot = 14;
Database::Get()->InsertNewPropertyModel(propertyId, model, "Objects_14_name");
/*
Commented out until UGC server would be updated to use a sd0 file instead of lxfml stream.
(or you uncomment the lxfml decomp stuff above)
*/
// Send off to UGC for processing, if enabled:
// if (Game::config->GetValue("ugc_remote") == "1") {
// std::string ugcIP = Game::config->GetValue("ugc_ip");
// int ugcPort = std::stoi(Game::config->GetValue("ugc_port"));
// httplib::Client cli(ugcIP, ugcPort); //connect to UGC HTTP server using our config above ^
// //Send out a request:
// std::string request = "/3dservices/UGCC150/150" + std::to_string(blueprintID) + ".lxfml";
// cli.Put(request.c_str(), lxfml.c_str(), "text/lxfml");
// //When the "put" above returns, it means that the UGC HTTP server is done processing our model &
// //the nif, hkx and checksum files are ready to be downloaded from cache.
// }
// Write the ID and data to the response packet
bitStream.Write(blueprintID);
const auto& newSd0 = sd0.GetAsVector();
uint32_t newSd0Size{};
for (const auto& chunk : newSd0) newSd0Size += chunk.size();
bitStream.Write(newSd0Size);
for (const auto& chunk : newSd0) bitStream.WriteAlignedBytes(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
}
SEND_PACKET;
//Now we have to construct this object:
// Create entities for each model
for (size_t i = 0; i < splitLxfmls.size(); ++i) {
EntityInfo info;
info.lot = 14;
info.pos = splitLxfmls[i].center;
info.rot = QuatUtils::IDENTITY;
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
EntityInfo info;
info.lot = 14;
info.pos = newCenter;
info.rot = {};
info.spawner = nullptr;
info.spawnerID = entity->GetObjectID();
info.spawnerNodeID = 0;
info.settings.push_back(new LDFData<LWOOBJID>(u"blueprintid", blueprintIDs[i]));
info.settings.push_back(new LDFData<int>(u"componentWhitelist", 1));
info.settings.push_back(new LDFData<int>(u"modelType", 2));
info.settings.push_back(new LDFData<bool>(u"propertyObjectID", true));
info.settings.push_back(new LDFData<LWOOBJID>(u"userModelID", modelIDs[i]));
Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr);
if (newEntity) {
Game::entityManager->ConstructEntity(newEntity);
info.settings.push_back(new LDFData<LWOOBJID>(u"blueprintid", blueprintID));
info.settings.push_back(new LDFData<int>(u"componentWhitelist", 1));
info.settings.push_back(new LDFData<int>(u"modelType", 2));
info.settings.push_back(new LDFData<bool>(u"propertyObjectID", true));
info.settings.push_back(new LDFData<LWOOBJID>(u"userModelID", newIDL));
Entity* newEntity = Game::entityManager->CreateEntity(info, nullptr);
if (newEntity) {
Game::entityManager->ConstructEntity(newEntity);
//Make sure the propMgmt doesn't delete our model after the server dies
//Trying to do this after the entity is constructed. Shouldn't really change anything but
//there was an issue with builds not appearing since it was placed above ConstructEntity.
PropertyManagementComponent::Instance()->AddModel(newEntity->GetObjectID(), newIDL);
//Make sure the propMgmt doesn't delete our model after the server dies
//Trying to do this after the entity is constructed. Shouldn't really change anything but
//there was an issue with builds not appearing since it was placed above ConstructEntity.
PropertyManagementComponent::Instance()->AddModel(newEntity->GetObjectID(), modelIDs[i]);
}
}
}