diff --git a/dMasterServer/InstanceLimits.h b/dMasterServer/InstanceLimits.h new file mode 100644 index 000000000..9f25af4c0 --- /dev/null +++ b/dMasterServer/InstanceLimits.h @@ -0,0 +1,32 @@ +#ifndef __INSTANCELIMITS__H__ +#define __INSTANCELIMITS__H__ + +#include + +namespace InstanceLimits { + // The most world instances that run at once when the config does not say + constexpr uint32_t DefaultMaxInstances = 256; + + enum class eNewInstance : uint8_t { + Allowed, + UnknownZone, + TooManyInstances + }; + + /** + * Whether a new world instance may be started for a zone. + * @param mapID the zone asked for, 0 is the character select and has no row in the ZoneTable + * @param isInZoneTable whether the ZoneTable has a row for the zone + * @param runningInstances the instances that run already + * @param maxInstances the most instances that may run at once, 0 for no limit + */ + constexpr eNewInstance CheckNewInstance(const uint32_t mapID, const bool isInZoneTable, const uint64_t runningInstances, const uint32_t maxInstances) { + // Players always have to be able to reach the character select + if (mapID == 0) return eNewInstance::Allowed; + if (!isInZoneTable) return eNewInstance::UnknownZone; + if (maxInstances != 0 && runningInstances >= maxInstances) return eNewInstance::TooManyInstances; + return eNewInstance::Allowed; + } +} + +#endif //!__INSTANCELIMITS__H__ diff --git a/dMasterServer/InstanceManager.cpp b/dMasterServer/InstanceManager.cpp index c98e954f8..03bd41692 100644 --- a/dMasterServer/InstanceManager.cpp +++ b/dMasterServer/InstanceManager.cpp @@ -11,6 +11,7 @@ #include "CDClientManager.h" #include "CDZoneTableTable.h" #include "Database.h" +#include "InstanceLimits.h" #include "MasterPackets.h" #include "BitStreamUtils.h" #include "ServiceType.h" @@ -46,6 +47,23 @@ InstanceManager::InstanceManager(const std::string& externalIP) : mExternalIP{ e m_LastPort = GeneralUtils::TryParse(Game::config->GetValue("world_port_start")).value_or(m_LastPort); m_LastInstanceID = LWOINSTANCEID_INVALID; + m_MaxInstances = + GeneralUtils::TryParse(Game::config->GetValue("max_instances")).value_or(InstanceLimits::DefaultMaxInstances); +} + +bool InstanceManager::CanCreateInstance(const LWOMAPID mapID, const LWOCLONEID cloneID) const { + const bool isInZoneTable = mapID != 0 && CDZoneTableTable::Query(mapID) != nullptr; + + switch (InstanceLimits::CheckNewInstance(mapID, isInZoneTable, m_Instances.size(), m_MaxInstances)) { + case InstanceLimits::eNewInstance::UnknownZone: + LOG("Tried to create an instance of map %i/%i, which is not in the ZoneTable.", mapID, cloneID); + return false; + case InstanceLimits::eNewInstance::TooManyInstances: + LOG("Tried to create an instance of map %i/%i, but %zu instances run already and max_instances is %u.", mapID, cloneID, m_Instances.size(), m_MaxInstances); + return false; + default: + return true; + } } const InstancePtr& InstanceManager::GetInstance(LWOMAPID mapID, bool isFriendTransfer, LWOCLONEID cloneID) { @@ -65,6 +83,9 @@ const InstancePtr& InstanceManager::CreateInstance(LWOMAPID mapID, LWOCLONEID cl cloneID); return g_Empty; } + + if (!CanCreateInstance(mapID, cloneID)) return g_Empty; + int softCap = 8; int maxPlayers = 12; @@ -360,6 +381,8 @@ const InstancePtr& InstanceManager::CreatePrivateInstance(LWOMAPID mapID, LWOCLO return g_Empty; } + if (!CanCreateInstance(mapID, cloneID)) return g_Empty; + int maxPlayers = 999; uint32_t port = GetFreePort(); diff --git a/dMasterServer/InstanceManager.h b/dMasterServer/InstanceManager.h index db120ec16..72391c80d 100644 --- a/dMasterServer/InstanceManager.h +++ b/dMasterServer/InstanceManager.h @@ -234,7 +234,17 @@ private: std::map m_ZoneLimits; std::map m_SpareBackoff; + /** + * The most instances that run at once, 0 for no limit + */ + uint32_t m_MaxInstances = 0; + //Private functions: + + /** + * Whether a new instance may be started for a zone, logs the reason when it may not + */ + bool CanCreateInstance(LWOMAPID mapID, LWOCLONEID cloneID) const; int GetSoftCap(LWOMAPID mapID); int GetHardCap(LWOMAPID mapID); const InstancePtr& CreateInstance(LWOMAPID mapID, LWOCLONEID cloneID); diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index 32f5b1307..110ad41e9 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -745,6 +745,10 @@ namespace { return; } const auto& in = Game::im->GetInstance(zoneID, false, zoneClone); + if (!in) { + LOG("No instance for transfer request %llu to zone %i %i", requestID, zoneID, zoneClone); + return; + } for (const auto& instance : Game::im->GetInstances()) { LOG("Instance: %i/%i/%i -> %i %s", instance->GetMapID(), instance->GetCloneID(), instance->GetInstanceID(), instance == in, instance->GetSysAddr().ToString()); @@ -887,6 +891,10 @@ namespace { void OnCreatePrivateZone(const CreatePrivateZone& request, const SystemAddress& sysAddr) { // Passwords were cut to 50 characters when read const auto& newInst = Game::im->CreatePrivateInstance(request.zoneID, request.cloneID, request.password.c_str()); + if (!newInst) { + LOG("No private zone was created for map %i/%i", request.zoneID, request.cloneID); + return; + } LOG("Creating private zone %i/%i/%i", newInst->GetMapID(), newInst->GetCloneID(), newInst->GetInstanceID()); } diff --git a/resources/masterconfig.ini b/resources/masterconfig.ini index 7d9b8ff3a..398e0c079 100644 --- a/resources/masterconfig.ini +++ b/resources/masterconfig.ini @@ -7,6 +7,10 @@ master_server_port=2000 # The port number to start world servers on. Will be incremented for each world 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 +max_instances=256 + # 0 or 1, should autostart auth, chat, and char servers prestart_servers=1 diff --git a/tests/dGameTests/CMakeLists.txt b/tests/dGameTests/CMakeLists.txt index 4b88b64ea..a2e925f81 100644 --- a/tests/dGameTests/CMakeLists.txt +++ b/tests/dGameTests/CMakeLists.txt @@ -62,6 +62,9 @@ if(APPLE) add_dependencies(dGameTests dGameTestsLink) endif() +# InstanceLimits.h +target_include_directories(dGameTests PRIVATE "${PROJECT_SOURCE_DIR}/dMasterServer") + # Local capture fixtures (tests/fixtures-local, never committed) are found from the source folder target_compile_definitions(dGameTests PRIVATE DLU_SOURCE_DIR="${PROJECT_SOURCE_DIR}") target_link_libraries(dGameTests ${COMMON_LIBRARIES} sqlite3 GTest::gtest_main diff --git a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt index d304907d6..f71cdb16b 100644 --- a/tests/dGameTests/dGameMessagesTests/CMakeLists.txt +++ b/tests/dGameTests/dGameMessagesTests/CMakeLists.txt @@ -20,6 +20,7 @@ SET(DGAMEMESSAGES_TESTS "ClientMessagesTests.cpp" "LoadSequenceTests.cpp" "ChatRoutingTests.cpp" + "InstanceLimitsTests.cpp" "TradeOfferTests.cpp" "VendorPricingTests.cpp") diff --git a/tests/dGameTests/dGameMessagesTests/InstanceLimitsTests.cpp b/tests/dGameTests/dGameMessagesTests/InstanceLimitsTests.cpp new file mode 100644 index 000000000..543b1def7 --- /dev/null +++ b/tests/dGameTests/dGameMessagesTests/InstanceLimitsTests.cpp @@ -0,0 +1,34 @@ +#include + +#include "InstanceLimits.h" + +using InstanceLimits::CheckNewInstance; +using InstanceLimits::eNewInstance; + +TEST(InstanceLimitsTest, AZoneOfTheZoneTableIsStarted) { + EXPECT_EQ(CheckNewInstance(1000, true, 0, 256), eNewInstance::Allowed); + EXPECT_EQ(CheckNewInstance(1150, true, 255, 256), eNewInstance::Allowed); +} + +TEST(InstanceLimitsTest, AZoneTheZoneTableDoesNotHaveIsRefused) { + EXPECT_EQ(CheckNewInstance(31337, false, 0, 256), eNewInstance::UnknownZone); + EXPECT_EQ(CheckNewInstance(31337, false, 0, 0), eNewInstance::UnknownZone); + // The unknown zone is the reason given, also when the limit is reached too + EXPECT_EQ(CheckNewInstance(31337, false, 256, 256), eNewInstance::UnknownZone); +} + +TEST(InstanceLimitsTest, NoInstanceIsStartedPastTheLimit) { + EXPECT_EQ(CheckNewInstance(1000, true, 256, 256), eNewInstance::TooManyInstances); + EXPECT_EQ(CheckNewInstance(1000, true, 1000, 256), eNewInstance::TooManyInstances); + EXPECT_EQ(CheckNewInstance(1000, true, 1, 1), eNewInstance::TooManyInstances); +} + +TEST(InstanceLimitsTest, ALimitOfZeroIsNoLimit) { + EXPECT_EQ(CheckNewInstance(1000, true, 100000, 0), eNewInstance::Allowed); +} + +TEST(InstanceLimitsTest, TheCharacterSelectIsAlwaysStarted) { + // Zone 0 has no row in the ZoneTable + EXPECT_EQ(CheckNewInstance(0, false, 0, 256), eNewInstance::Allowed); + EXPECT_EQ(CheckNewInstance(0, false, 256, 256), eNewInstance::Allowed); +}