fix: tell the player when the zone they go to gets no instance

The master server dropped a transfer request it started no instance
for, so the world server never heard back and the player waited. It
now answers the request with an empty address and the reason in place
of the port: no room for another instance (max_instances), a zone that
is not in the ZoneTable, or another reason. A request for a private
zone that does not exist is answered the same way.

The world server passes this on as a TRANSFER_TO_WORLD without an
address, which the client shows as a failed map change, and leaves the
character where it is. The auth server fails the login when there is
no character select to send the player to.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-11-10 16:06:31 -06:00
parent 24294561ae
commit ebcfd54915
16 changed files with 132 additions and 3 deletions

View File

@@ -3,6 +3,8 @@
#include <cstdint>
#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__

View File

@@ -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<uint16_t>(failure);
response.stamps = request.stamps;
MasterPackets::SendTo(request.sysAddr, response);
}
InstanceManager::InstanceManager(const std::string& externalIP) : mExternalIP{ externalIP } {
m_LastPort =
GeneralUtils::TryParse<uint16_t>(Game::config->GetValue("world_port_start")).value_or(m_LastPort);
@@ -51,10 +62,13 @@ InstanceManager::InstanceManager(const std::string& externalIP) : mExternalIP{ e
GeneralUtils::TryParse<uint32_t>(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);

View File

@@ -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<bool(const Instance&)>& 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<std::unique_ptr<Instance>> m_Instances;

View File

@@ -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<bool>(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<bool>(mythranShift), sysAddr, {} }, 0, 0, eTransferFailure::Other);
return;
}