diff --git a/dGame/dUtilities/SlashCommandHandler.cpp b/dGame/dUtilities/SlashCommandHandler.cpp index 58859a592..0efd0f85e 100644 --- a/dGame/dUtilities/SlashCommandHandler.cpp +++ b/dGame/dUtilities/SlashCommandHandler.cpp @@ -459,6 +459,15 @@ void SlashCommandHandler::Startup() { }; RegisterCommand(TestMapCommand); + Command ReprocessPropertyCommand{ + .help = "Make this property's models again and reload it", + .info = "The UGC server makes every model placed on the property you are on again (with the current UGC settings). Once they are made, everyone on the property is sent back into it, so their game loads the new meshes", + .aliases = { "reprocessproperty", "reloadpropertymodels" }, + .handle = DEVGMCommands::ReprocessProperty, + .requiredLevel = eGameMasterLevel::DEVELOPER + }; + RegisterCommand(ReprocessPropertyCommand); + Command ReportProxPhysCommand{ .help = "Display proximity sensor info", .info = "Prints to console the position and radius of proximity sensors.", diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index 1416a8b8e..c9af450b1 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -1,4 +1,7 @@ #include "DEVGMCommands.h" + +#include "UgcManifest.h" +#include "PropertyManagementComponent.h" #include "MasterPackets.h" #include @@ -1979,4 +1982,19 @@ namespace DEVGMCommands { } } } + + void ReprocessProperty(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + auto* const property = PropertyManagementComponent::Instance(); + if (!property || property->GetId() == LWOOBJID_EMPTY) { + ChatPackets::SendSystemMessage(sysAddr, u"You aren't on a property."); + return; + } + const auto queued = UgcManifest::ReprocessProperty(property->GetId()); + if (queued == 0) { + ChatPackets::SendSystemMessage(sysAddr, u"This property has no brick built models."); + return; + } + ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::UTF8ToUTF16("Making " + std::to_string(queued) + + " model(s) again. Everyone on the property is reloaded when they're made.")); + } }; diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.h b/dGame/dUtilities/SlashCommands/DEVGMCommands.h index 64783e24f..df948aab1 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.h +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.h @@ -13,6 +13,7 @@ namespace DEVGMCommands { void ShutdownUniverse(Entity* entity, const SystemAddress& sysAddr, const std::string args); void SetMinifig(Entity* entity, const SystemAddress& sysAddr, const std::string args); void TestMap(Entity* entity, const SystemAddress& sysAddr, const std::string args); + void ReprocessProperty(Entity* entity, const SystemAddress& sysAddr, const std::string args); void ReportProxPhys(Entity* entity, const SystemAddress& sysAddr, const std::string args); void SpawnPhysicsVerts(Entity* entity, const SystemAddress& sysAddr, const std::string args); void Teleport(Entity* entity, const SystemAddress& sysAddr, const std::string args); diff --git a/dGame/dUtilities/UgcManifest.cpp b/dGame/dUtilities/UgcManifest.cpp index 39a571f61..a1fc28f85 100644 --- a/dGame/dUtilities/UgcManifest.cpp +++ b/dGame/dUtilities/UgcManifest.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -21,6 +22,9 @@ #include "MD5.h" #include "PlayerManager.h" #include "Sd0.h" +#include "dZoneManager.h" +#include "ChatPackets.h" +#include "ZoneInstanceManager.h" namespace { // How often the waiting requests are looked up again, how long they wait at most and how many there can be @@ -59,6 +63,14 @@ namespace { }; std::vector g_Reconstructs; + // /reprocessproperty: the models being made again, and when the players are reloaded at the latest + struct PropertyReload { + std::set blueprintIds; + std::chrono::steady_clock::time_point since; + std::chrono::steady_clock::time_point nextCheck; + }; + std::optional g_PropertyReload; + bool ManifestOn() { // Off unless set: a client whose boot.cfg doesn't point it at the UGC server downloads from its built-in // http://127.0.0.1:80/lwoclient and is logged out when it can't connect there (docs/UgcServer.md) @@ -264,8 +276,66 @@ namespace { } +namespace { + // Sends every player in this world the made models' checksums, then transfers them back into this zone and clone: + // the client loads the property again, and its manifest cache now has the new meshes' checksums + void ReloadPlayers(const std::set& blueprintIds) { + const auto zoneId = Game::zoneManager->GetZoneID(); + for (auto* const player : PlayerManager::GetAllPlayers()) { + if (!player) continue; + const auto sysAddr = player->GetSystemAddress(); + for (const auto id : blueprintIds) { + const auto checksum = Database::Get()->GetUgcFileChecksum(id, "model.nif"); + if (checksum && checksum->md5.size() == 32) Send(sysAddr, id, eUgcResourceType::NIF, checksum); + } + ChatPackets::SendSystemMessage(sysAddr, u"The property's models were made again: loading the property again."); + const auto objectId = player->GetObjectID(); + ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, zoneId.GetMapID(), zoneId.GetCloneID(), false, + [objectId](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { + auto* const entity = Game::entityManager->GetEntity(objectId); + if (!entity || !entity->GetCharacter()) return; + auto* const character = entity->GetCharacter(); + character->SetZoneID(zoneID); + character->SetZoneInstance(zoneInstance); + character->SetZoneClone(zoneClone); + character->SaveXMLToDatabase(); + ClientPackets::TransferToWorld transfer; + transfer.serverIP = LUString(serverIP); + transfer.serverPort = serverPort; + transfer.mythranShift = mythranShift; + transfer.Send(entity->GetSystemAddress()); + }); + } + } +} + +size_t UgcManifest::ReprocessProperty(const LWOOBJID propertyId) { + std::set blueprintIds; + for (const auto& model : Database::Get()->GetPropertyModels(propertyId)) { + if (model.ugcId != 0) blueprintIds.insert(model.ugcId); + } + if (blueprintIds.empty()) return 0; + Database::Get()->ResetPropertyUgcModelProcessing(propertyId); + const auto now = std::chrono::steady_clock::now(); + g_PropertyReload = PropertyReload{ std::move(blueprintIds), now, now + RETRY_INTERVAL }; + LOG("Making the %zu models of property %llu again; players are reloaded when they're made", g_PropertyReload->blueprintIds.size(), static_cast(propertyId)); + return g_PropertyReload->blueprintIds.size(); +} + void UgcManifest::Update() { const auto now = std::chrono::steady_clock::now(); + if (g_PropertyReload && now >= g_PropertyReload->nextCheck) { + g_PropertyReload->nextCheck = now + RETRY_INTERVAL; + bool waiting = false; + for (const auto id : g_PropertyReload->blueprintIds) { + const auto info = Database::Get()->GetUgcProcessInfo(id); + waiting = waiting || (info && info->state == IUgc::eProcessState::PENDING); + } + if (!waiting || now - g_PropertyReload->since > MAX_WAIT) { + ReloadPlayers(g_PropertyReload->blueprintIds); + g_PropertyReload.reset(); + } + } std::erase_if(g_Reconstructs, [now](Reconstruct& pending) { return ConstructAgain(pending, now); }); if (g_Waiting.empty() || now < g_NextRetry) return; @@ -284,11 +354,13 @@ void UgcManifest::OnDisconnect(const SystemAddress& sysAddr) { void UgcManifest::OnModelsMade(const std::vector& blueprintIds) { for (const auto id : blueprintIds) g_LxfmlChecksums.erase(id); // made again after its LXFML changed, maybe if (!ServesModels() || !Game::entityManager) return; + // Models of a /reprocessproperty: the players are reloaded once they're all made const auto& players = PlayerManager::GetAllPlayers(); if (players.empty()) return; const auto models = Game::entityManager->GetEntitiesByLOT(BrickByBrick::MODEL_OBJECT_LOT); for (const auto id : blueprintIds) { + if (g_PropertyReload && g_PropertyReload->blueprintIds.contains(id)) continue; std::vector shown; for (auto* const model : models) { if (model && model->GetVar(u"blueprintid") == id) shown.push_back(model); diff --git a/dGame/dUtilities/UgcManifest.h b/dGame/dUtilities/UgcManifest.h index b69968b51..cca6d3ddb 100644 --- a/dGame/dUtilities/UgcManifest.h +++ b/dGame/dUtilities/UgcManifest.h @@ -60,6 +60,11 @@ namespace UgcManifest { // clients to served meshes that are due void Update(); + // /reprocessproperty: every model placed on the property is made again by the UGC server; once none is waiting + // (or after 15 minutes), every player in this world is sent the new mesh checksums and transferred back into it, + // so their client loads the property again with the new meshes. Returns how many models were queued. + size_t ReprocessProperty(LWOOBJID propertyId); + // A client left (or went back to character select): its waiting requests and pending switches are dropped void OnDisconnect(const SystemAddress& sysAddr); diff --git a/docs/Commands.md b/docs/Commands.md index 2d93adeb0..419829a3e 100644 --- a/docs/Commands.md +++ b/docs/Commands.md @@ -123,6 +123,7 @@ These commands are primarily for development and testing. The usage of many of t |resurrect|`/resurrect`|Resurrects the player.|8| |setminifig|`/setminifig `|Alters your player's minifig. Body part can be one of "Eyebrows", "Eyes", "HairColor", "HairStyle", "Pants", "LeftHand", "Mouth", "RightHand", "Shirt", or "Hands". Changing minifig parts could break the character so this command is limited to GMs.|1| |testmap|`/testmap (clone-id) (instance-id) (spawn-point)`|Transfers you to the given zone by id and clone id and then spawns you at the specified spawn point if one was specified. Ignores instance-id for now. Aliases: `/tm`.|1| +|reprocessproperty|`/reprocessproperty`|Has the UGC server make every brick built model on the property you are on again (with the current UGC settings). When they are all made (at most 15 minutes), everyone on the property is sent back into it, so their game downloads the new meshes. Aliases: `/reloadpropertymodels`.|8| |reportproxphys|`/reportproxphys`|Prints to console the position and radius of proximity sensors.|9| |spawnphysicsverts|`/spawnphysicsverts`|Spawns a 1x1 brick at all vertices of phantom physics objects|8| |teleport|`/teleport (y) `|Teleports you. If no Y is given, you are teleported to the height of the terrain or physics object at (x, z). Any of the coordinates can use the syntax of an exact position (10.0), or a relative position (~+10.0). A ~ means use the current value of that axis as the base value. Addition or subtraction is supported (~+10) (~-10). If source player and target player are players that exist in the world, then the source player will be teleported to target player. Aliases: `/tele`, `/tp`.|6| diff --git a/docs/UgcServer.md b/docs/UgcServer.md index 9212c1ece..b3987a038 100644 --- a/docs/UgcServer.md +++ b/docs/UgcServer.md @@ -426,6 +426,12 @@ serving), the worlds: replaces what the client showed. A model made again unchanged (after eviction) isn't sent. Nothing polls: one message per batch of made models. +* **`/reprocessproperty`** (GM 8): every model placed on the property the player is on goes back to the UGC server's + queue (`ResetPropertyUgcModelProcessing`). The world checks every 5 seconds; once none is pending (or after 15 + minutes) it sends every player in the world the new `model.nif` checksums and transfers them back into the same + zone and clone. The client loads the property again and downloads the new meshes (its manifest cache has the new + checksums, which its files don't match). Models of a reprocess skip the "made again" switch. + ### Waiting while the owner is still building A saved model isn't made right away: `InsertNewUgcModel` stores `process_after` = now + `ugc_debounce_seconds`