diff --git a/dCommon/dEnums/eTransferFailure.h b/dCommon/dEnums/eTransferFailure.h new file mode 100644 index 000000000..184e5ff88 --- /dev/null +++ b/dCommon/dEnums/eTransferFailure.h @@ -0,0 +1,17 @@ +#ifndef __ETRANSFERFAILURE__H__ +#define __ETRANSFERFAILURE__H__ + +#include + +/** + * Why a transfer to a world got no world to go to. A TRANSFER_TO_WORLD message with an empty address + * carries it in place of the port, and the client tells the player. + */ +enum class eTransferFailure : uint16_t { + NoServer = 0, // nothing is free to start the zone on + MapLoad, // the zone can not be loaded + InstanceFull, // the instance asked for is full + Other // every other value is a failure without a reason +}; + +#endif //!__ETRANSFERFAILURE__H__ diff --git a/dGame/UserManager.cpp b/dGame/UserManager.cpp index 0a339e7d0..7590caace 100644 --- a/dGame/UserManager.cpp +++ b/dGame/UserManager.cpp @@ -588,6 +588,8 @@ void UserManager::LoginCharacter(const SystemAddress& sysAddr, LWOOBJID playerID if (zoneID == LWOZONEID_INVALID) zoneID = 1000; //Send char to VE ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, zoneID, character->GetZoneClone(), false, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { + if (WorldPackets::SendIfTransferFailed(sysAddr, serverIP, serverPort)) return; + LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", character->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort); if (character) { auto* entity = Game::entityManager->GetEntity(character->GetObjectID()); diff --git a/dGame/dBehaviors/PropertyTeleportBehavior.cpp b/dGame/dBehaviors/PropertyTeleportBehavior.cpp index f51fe3bdd..321f84eaf 100644 --- a/dGame/dBehaviors/PropertyTeleportBehavior.cpp +++ b/dGame/dBehaviors/PropertyTeleportBehavior.cpp @@ -1,4 +1,5 @@ #include "PropertyTeleportBehavior.h" +#include "WorldPackets.h" #include "BehaviorBranchContext.h" #include "BehaviorContext.h" @@ -37,6 +38,8 @@ void PropertyTeleportBehavior::Handle(BehaviorContext* context, RakNet::BitStrea const auto sysAddr = entity->GetSystemAddress(); + if (WorldPackets::SendIfTransferFailed(sysAddr, serverIP, serverPort)) return; + if (zoneClone != 0) ChatPackets::SendSystemMessage(sysAddr, u"Transfering to your property!"); else ChatPackets::SendSystemMessage(sysAddr, u"Transfering back to previous world!"); diff --git a/dGame/dComponents/Character/CharacterComponent.cpp b/dGame/dComponents/Character/CharacterComponent.cpp index f363841e9..e7bf138a9 100644 --- a/dGame/dComponents/Character/CharacterComponent.cpp +++ b/dGame/dComponents/Character/CharacterComponent.cpp @@ -1,4 +1,5 @@ #include "CharacterComponent.h" +#include "WorldPackets.h" #include "BitStream.h" #include "tinyxml2.h" #include "Game.h" @@ -1011,6 +1012,8 @@ void CharacterComponent::SendToZone(LWOMAPID zoneId, LWOCLONEID cloneId) const { const auto sysAddr = entity->GetSystemAddress(); + if (WorldPackets::SendIfTransferFailed(sysAddr, serverIP, serverPort)) return; + auto* character = entity->GetCharacter(); auto* characterComponent = entity->GetComponent(); diff --git a/dGame/dGameMessages/ObjectMessages.cpp b/dGame/dGameMessages/ObjectMessages.cpp index 1a1d6b956..e4afc7def 100644 --- a/dGame/dGameMessages/ObjectMessages.cpp +++ b/dGame/dGameMessages/ObjectMessages.cpp @@ -1,4 +1,5 @@ #include "ObjectMessages.h" +#include "WorldPackets.h" #include "BitStreamUtils.h" #include "Character.h" @@ -100,6 +101,8 @@ namespace GameMessages { if (launcher) launcher->UpdatePlayerStatistic(RocketsUsed); ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, mapId, cloneId, false, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { + if (WorldPackets::SendIfTransferFailed(sysAddr, serverIP, serverPort)) return; + LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", character->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort); if (character) { diff --git a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp index e4d4a1b62..48ee47670 100644 --- a/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp +++ b/dGame/dUtilities/SlashCommands/DEVGMCommands.cpp @@ -1,4 +1,5 @@ #include "DEVGMCommands.h" +#include "WorldPackets.h" #include "UgcKeys.h" #include "UgcManifest.h" @@ -1170,6 +1171,8 @@ namespace DEVGMCommands { const auto sysAddr = entity->GetSystemAddress(); + if (WorldPackets::SendIfTransferFailed(sysAddr, serverIP, serverPort)) return; + ChatPackets::SendSystemMessage(sysAddr, u"Transfering map..."); LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", sysAddr.ToString(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort); diff --git a/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp b/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp index ff1e3d140..731aecddc 100644 --- a/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp +++ b/dGame/dUtilities/SlashCommands/GMZeroCommands.cpp @@ -1,4 +1,5 @@ #include "GMZeroCommands.h" +#include "WorldPackets.h" // Classes #include "Amf3.h" @@ -182,6 +183,8 @@ namespace GMZeroCommands { const auto sysAddr = entity->GetSystemAddress(); + if (WorldPackets::SendIfTransferFailed(sysAddr, serverIP, serverPort)) return; + LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", entity->GetCharacter()->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort); if (entity->GetCharacter()) { @@ -217,6 +220,8 @@ namespace GMZeroCommands { ChatPackets::SendSystemMessage(sysAddr, u"Requesting private map..."); ZoneInstanceManager::Instance()->RequestPrivateZone(Game::server, false, password, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) { + if (WorldPackets::SendIfTransferFailed(sysAddr, serverIP, serverPort)) return; + LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", sysAddr.ToString(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort); if (entity->GetCharacter()) { diff --git a/dMasterServer/InstanceLimits.h b/dMasterServer/InstanceLimits.h index 9f25af4c0..71011a187 100644 --- a/dMasterServer/InstanceLimits.h +++ b/dMasterServer/InstanceLimits.h @@ -3,6 +3,8 @@ #include +#include "eTransferFailure.h" + namespace InstanceLimits { // The most world instances that run at once when the config does not say constexpr uint32_t DefaultMaxInstances = 256; @@ -27,6 +29,17 @@ namespace InstanceLimits { if (maxInstances != 0 && runningInstances >= maxInstances) return eNewInstance::TooManyInstances; return eNewInstance::Allowed; } + + /** + * What the player who asked for the zone is told when no instance was started for it. + */ + constexpr eTransferFailure TransferFailure(const eNewInstance newInstance) { + switch (newInstance) { + case eNewInstance::UnknownZone: return eTransferFailure::MapLoad; + case eNewInstance::TooManyInstances: return eTransferFailure::NoServer; + default: return eTransferFailure::Other; + } + } } #endif //!__INSTANCELIMITS__H__ diff --git a/dMasterServer/InstanceManager.cpp b/dMasterServer/InstanceManager.cpp index 03bd41692..d68f6e53f 100644 --- a/dMasterServer/InstanceManager.cpp +++ b/dMasterServer/InstanceManager.cpp @@ -43,6 +43,17 @@ namespace { } } +void InstanceManager::SendTransferFailure(const PendingInstanceRequest& request, const LWOMAPID mapID, const LWOCLONEID cloneID, const eTransferFailure failure) { + MasterPackets::RequestZoneTransferResponse response; + response.requestID = request.id; + response.mythranShift = request.mythranShift; + response.zoneID = mapID; + response.zoneClone = cloneID; + response.serverPort = static_cast(failure); + response.stamps = request.stamps; + MasterPackets::SendTo(request.sysAddr, response); +} + InstanceManager::InstanceManager(const std::string& externalIP) : mExternalIP{ externalIP } { m_LastPort = GeneralUtils::TryParse(Game::config->GetValue("world_port_start")).value_or(m_LastPort); @@ -51,10 +62,13 @@ InstanceManager::InstanceManager(const std::string& externalIP) : mExternalIP{ e GeneralUtils::TryParse(Game::config->GetValue("max_instances")).value_or(InstanceLimits::DefaultMaxInstances); } -bool InstanceManager::CanCreateInstance(const LWOMAPID mapID, const LWOCLONEID cloneID) const { +InstanceLimits::eNewInstance InstanceManager::CheckNewInstance(const LWOMAPID mapID) const { const bool isInZoneTable = mapID != 0 && CDZoneTableTable::Query(mapID) != nullptr; + return InstanceLimits::CheckNewInstance(mapID, isInZoneTable, m_Instances.size(), m_MaxInstances); +} - switch (InstanceLimits::CheckNewInstance(mapID, isInZoneTable, m_Instances.size(), m_MaxInstances)) { +bool InstanceManager::CanCreateInstance(const LWOMAPID mapID, const LWOCLONEID cloneID) const { + switch (CheckNewInstance(mapID)) { case InstanceLimits::eNewInstance::UnknownZone: LOG("Tried to create an instance of map %i/%i, which is not in the ZoneTable.", mapID, cloneID); return false; @@ -315,6 +329,13 @@ void InstanceManager::RedirectPendingRequests(const InstancePtr& instance) { for (const auto& request : instance->GetPendingAffirmations()) { const auto& in = Game::im->GetInstance(zoneId.GetMapID(), false, zoneId.GetCloneID()); + if (!in) { + // No instance takes the place of the one that is gone, so the transfer fails + const auto failure = InstanceLimits::TransferFailure(CheckNewInstance(zoneId.GetMapID())); + SendTransferFailure(request, zoneId.GetMapID(), zoneId.GetCloneID(), failure); + continue; + } + if (in && !in->GetIsReady()) // Instance not ready, make a pending request { in->GetPendingRequests().push_back(request); diff --git a/dMasterServer/InstanceManager.h b/dMasterServer/InstanceManager.h index 72391c80d..f25a4129c 100644 --- a/dMasterServer/InstanceManager.h +++ b/dMasterServer/InstanceManager.h @@ -15,6 +15,7 @@ #include "dZMCommon.h" #include "Logger.h" #include "master/InstanceMigration.h" +#include "InstanceLimits.h" struct Player { LWOOBJID id; @@ -220,6 +221,16 @@ public: // Mark the running instances which() picks outdated (old binary or old zone files); returns how many were new uint32_t MarkOutdated(const std::function& which); + /** + * Whether a new instance may be started for a zone, and why not when it may not + */ + InstanceLimits::eNewInstance CheckNewInstance(LWOMAPID mapID) const; + + /** + * Answers a transfer request no instance was started for: an empty address, and the reason in place of the port + */ + static void SendTransferFailure(const PendingInstanceRequest& request, LWOMAPID mapID, LWOCLONEID cloneID, eTransferFailure failure); + private: std::string mExternalIP; std::vector> m_Instances; diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index 110ad41e9..e7d60ad0c 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -746,7 +746,10 @@ namespace { } const auto& in = Game::im->GetInstance(zoneID, false, zoneClone); if (!in) { + // An empty address tells the world server there is no instance, the port is the reason + const auto failure = InstanceLimits::TransferFailure(Game::im->CheckNewInstance(zoneID)); LOG("No instance for transfer request %llu to zone %i %i", requestID, zoneID, zoneClone); + InstanceManager::SendTransferFailure({ requestID, static_cast(mythranShift), sysAddr, stamps }, zoneID, zoneClone, failure); return; } @@ -907,6 +910,8 @@ namespace { LOG("Join private zone: %llu %d %p", requestID, mythranShift, instance.get()); if (instance == nullptr) { + // No private zone has this password, the player is told the transfer failed + InstanceManager::SendTransferFailure({ requestID, static_cast(mythranShift), sysAddr, {} }, 0, 0, eTransferFailure::Other); return; } diff --git a/dNet/AuthPackets.cpp b/dNet/AuthPackets.cpp index 536f9a78c..2739c7cf9 100644 --- a/dNet/AuthPackets.cpp +++ b/dNet/AuthPackets.cpp @@ -276,6 +276,11 @@ void AuthPackets::LoginRequest::Handle() { stamps.Add(eStamps::PASSPORT_AUTH_WORLD_COMMUNICATION_START); // The stamps go along: master adds its steps and sends them back with the world server ZoneInstanceManager::Instance()->RequestZoneTransfer(server, 0, 0, false, stamps, [system, server, username](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string zoneIP, uint16_t zonePort, Stamps stamps) mutable { + // An empty address is the master server's answer when no character select runs + if (zoneIP.empty()) { + AuthPackets::SendLoginResponse(server, system, eLoginResponse::GENERAL_FAILED, "", "", 0, username, stamps); + return; + } AuthPackets::SendLoginResponse(server, system, eLoginResponse::SUCCESS, "", zoneIP, zonePort, username, stamps); }); } diff --git a/dNet/WorldPackets.cpp b/dNet/WorldPackets.cpp index b72dfb807..ec62b85ea 100644 --- a/dNet/WorldPackets.cpp +++ b/dNet/WorldPackets.cpp @@ -1,6 +1,8 @@ #include "WorldPackets.h" +#include "ClientPackets.h" #include "dCommonVars.h" +#include "Logger.h" #include @@ -18,6 +20,17 @@ namespace { } namespace WorldPackets { + bool SendIfTransferFailed(const SystemAddress& sysAddr, const std::string& serverIP, const uint32_t serverPort) { + if (!serverIP.empty()) return false; + + LOG("No instance to transfer %s to (reason %u)", sysAddr.ToString(), serverPort); + // The client reads the port of a transfer without an address as the reason and tells the player + ClientPackets::TransferToWorld transfer; + transfer.serverPort = static_cast(serverPort); + transfer.Send(sysAddr); + return true; + } + void Validation::Serialize(RakNet::BitStream& bitStream) const { bitStream.Write(username); bitStream.Write(sessionKey); diff --git a/dNet/WorldPackets.h b/dNet/WorldPackets.h index 1c3921be7..0b70f02fc 100644 --- a/dNet/WorldPackets.h +++ b/dNet/WorldPackets.h @@ -19,6 +19,13 @@ // Packets of ServiceType::WORLD: what a client sends to a world server. The server's answers are ClientPackets. // The structs only read and write bytes; the world server attaches the handlers (see WorldServer.cpp). namespace WorldPackets { + /** + * Tells a player that the zone they were to go to has no instance. The master server answers a transfer + * request with an empty address then, and with the eTransferFailure in place of the port. + * @return whether the transfer failed, the caller has nothing left to do for it then + */ + bool SendIfTransferFailed(const SystemAddress& sysAddr, const std::string& serverIP, uint32_t serverPort); + // Base of every world packet: the dispatcher sets sysAddr before Deserialize and Handle. struct WorldLUBitStream : public LUBitStream { SystemAddress sysAddr = UNASSIGNED_SYSTEM_ADDRESS; diff --git a/resources/masterconfig.ini b/resources/masterconfig.ini index 398e0c079..a98440c5a 100644 --- a/resources/masterconfig.ini +++ b/resources/masterconfig.ini @@ -8,7 +8,8 @@ master_server_port=2000 world_port_start=3000 # The most world instances that run at once. The character select is always started. -# A zone or property that needs a new instance past this is not started. 0 for no limit +# A zone or property that needs a new instance past this is not started and the player +# who asked for it is told that the map change failed. 0 for no limit max_instances=256 # 0 or 1, should autostart auth, chat, and char servers diff --git a/tests/dGameTests/dGameMessagesTests/InstanceLimitsTests.cpp b/tests/dGameTests/dGameMessagesTests/InstanceLimitsTests.cpp index 543b1def7..8076f4fa3 100644 --- a/tests/dGameTests/dGameMessagesTests/InstanceLimitsTests.cpp +++ b/tests/dGameTests/dGameMessagesTests/InstanceLimitsTests.cpp @@ -4,6 +4,7 @@ using InstanceLimits::CheckNewInstance; using InstanceLimits::eNewInstance; +using InstanceLimits::TransferFailure; TEST(InstanceLimitsTest, AZoneOfTheZoneTableIsStarted) { EXPECT_EQ(CheckNewInstance(1000, true, 0, 256), eNewInstance::Allowed); @@ -32,3 +33,19 @@ TEST(InstanceLimitsTest, TheCharacterSelectIsAlwaysStarted) { EXPECT_EQ(CheckNewInstance(0, false, 0, 256), eNewInstance::Allowed); EXPECT_EQ(CheckNewInstance(0, false, 256, 256), eNewInstance::Allowed); } + +TEST(InstanceLimitsTest, ARefusedInstanceHasAReasonForThePlayer) { + // The limit: nothing is free to start the zone on + EXPECT_EQ(TransferFailure(CheckNewInstance(1000, true, 256, 256)), eTransferFailure::NoServer); + // No row in the ZoneTable: the zone can not be loaded + EXPECT_EQ(TransferFailure(CheckNewInstance(31337, false, 0, 256)), eTransferFailure::MapLoad); + // Refused for another reason, such as a master server that shuts down + EXPECT_EQ(TransferFailure(eNewInstance::Allowed), eTransferFailure::Other); +} + +TEST(InstanceLimitsTest, TheReasonsAreTheValuesTheClientKnows) { + EXPECT_EQ(static_cast(eTransferFailure::NoServer), 0); + EXPECT_EQ(static_cast(eTransferFailure::MapLoad), 1); + EXPECT_EQ(static_cast(eTransferFailure::InstanceFull), 2); + EXPECT_EQ(static_cast(eTransferFailure::Other), 3); +}