From eff31fe7f6d6def3ac863d5b4aa160aa740fa7fa Mon Sep 17 00:00:00 2001 From: itsneufox <156133096+itsneufox@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:21:58 +0100 Subject: [PATCH 1/3] Make textdraw pool limits runtime configurable --- .../Components/LegacyConfig/config_main.cpp | 4 + Server/Components/TextDraws/textdraw.hpp | 18 +- .../Components/TextDraws/textdraws_main.cpp | 296 +++++++++++++++++- Shared/NetCode/textdraw.hpp | 15 +- 4 files changed, 312 insertions(+), 21 deletions(-) diff --git a/Server/Components/LegacyConfig/config_main.cpp b/Server/Components/LegacyConfig/config_main.cpp index 5045a29b5..527f28270 100644 --- a/Server/Components/LegacyConfig/config_main.cpp +++ b/Server/Components/LegacyConfig/config_main.cpp @@ -75,6 +75,8 @@ const FlatHashMap types = { { "stream_distance", ParamType::Float }, { "stream_rate", ParamType::Int }, { "maxnpc", ParamType::Int }, + { "max_global_textdraws", ParamType::Int }, + { "max_player_textdraws", ParamType::Int }, { "lagcompmode", ParamType::Int }, { "useartwork", ParamType::Bool }, { "artpath", ParamType::String }, @@ -122,6 +124,8 @@ const FlatHashMap dictionary = { { "stream_distance", "network.stream_radius" }, { "stream_rate", "network.stream_rate" }, { "maxnpc", "max_bots" }, + { "max_global_textdraws", "textdraw.global_limit" }, + { "max_player_textdraws", "textdraw.player_limit" }, { "lagcompmode", "game.lag_compensation_mode" }, { "useartwork", "artwork.enable" }, { "artpath", "artwork.models_path" } diff --git a/Server/Components/TextDraws/textdraw.hpp b/Server/Components/TextDraws/textdraw.hpp index 2f952a6c8..a2d36cf8d 100644 --- a/Server/Components/TextDraws/textdraw.hpp +++ b/Server/Components/TextDraws/textdraw.hpp @@ -35,13 +35,15 @@ class TextDrawBase : public T, public PoolIDProvider, public NoCopy Vector3 previewRotation = Vector3(0.f); Pair previewVehicleColours = std::make_pair(-1, -1); float previewZoom = 1.f; + int globalTextDrawPoolSize; public: - TextDrawBase(Vector2 pos, StringView text, TextDrawStyle style = TextDrawStyle_FontAharoniBold, int previewModel = 0) + TextDrawBase(int globalTextDrawPoolSize, Vector2 pos, StringView text, TextDrawStyle style = TextDrawStyle_FontAharoniBold, int previewModel = 0) : pos(pos) , text(text) , style(style) , previewModel(previewModel) + , globalTextDrawPoolSize(globalTextDrawPoolSize) { trimText(); } @@ -260,6 +262,7 @@ class TextDrawBase : public T, public PoolIDProvider, public NoCopy { NetCode::RPC::PlayerShowTextDraw playerShowTextDrawRPC; playerShowTextDrawRPC.PlayerTextDraw = isPlayerTextDraw; + playerShowTextDrawRPC.GlobalTextDrawPoolSize = globalTextDrawPoolSize; playerShowTextDrawRPC.UseBox = box; switch (alignment) { @@ -301,6 +304,7 @@ class TextDrawBase : public T, public PoolIDProvider, public NoCopy { NetCode::RPC::PlayerHideTextDraw playerHideTextDrawRPC; playerHideTextDrawRPC.PlayerTextDraw = isPlayerTextDraw; + playerHideTextDrawRPC.GlobalTextDrawPoolSize = globalTextDrawPoolSize; playerHideTextDrawRPC.TextDrawID = poolID; PacketHelper::send(playerHideTextDrawRPC, player); } @@ -309,6 +313,7 @@ class TextDrawBase : public T, public PoolIDProvider, public NoCopy { NetCode::RPC::PlayerTextDrawSetString playerTextDrawSetStringRPC; playerTextDrawSetStringRPC.PlayerTextDraw = isPlayerTextDraw; + playerTextDrawSetStringRPC.GlobalTextDrawPoolSize = globalTextDrawPoolSize; playerTextDrawSetStringRPC.TextDrawID = poolID; playerTextDrawSetStringRPC.Text = txt; PacketHelper::send(playerTextDrawSetStringRPC, player); @@ -338,9 +343,12 @@ class TextDraw final : public TextDrawBase private: UniqueIDArray shownFor_; - using TextDrawBase::TextDrawBase; - public: + TextDraw(int globalTextDrawPoolSize, Vector2 pos, StringView text, TextDrawStyle style = TextDrawStyle_FontAharoniBold, int previewModel = 0) + : TextDrawBase(globalTextDrawPoolSize, pos, text, style, previewModel) + { + } + void removeFor(int pid, IPlayer& player) { if (shownFor_.valid(pid)) @@ -408,8 +416,8 @@ class PlayerTextDraw final : public TextDrawBase bool shown = false; public: - PlayerTextDraw(IPlayer& player, Vector2 pos, StringView text, TextDrawStyle style = TextDrawStyle_FontAharoniBold, int previewModel = 0) - : TextDrawBase(pos, text, style, previewModel) + PlayerTextDraw(int globalTextDrawPoolSize, IPlayer& player, Vector2 pos, StringView text, TextDrawStyle style = TextDrawStyle_FontAharoniBold, int previewModel = 0) + : TextDrawBase(globalTextDrawPoolSize, pos, text, style, previewModel) , player(player) { } diff --git a/Server/Components/TextDraws/textdraws_main.cpp b/Server/Components/TextDraws/textdraws_main.cpp index 3a5413224..56af0eafe 100644 --- a/Server/Components/TextDraws/textdraws_main.cpp +++ b/Server/Components/TextDraws/textdraws_main.cpp @@ -9,14 +9,258 @@ #include "textdraw.hpp" #include #include +#include +#include +#include +#include +#include +#include using namespace Impl; +namespace +{ + template + class RuntimeMarkedPoolStorage final : public NoCopy + { + public: + using Iterator = MarkedPoolIterator>; + static constexpr size_t Lower = 0; + + explicit RuntimeMarkedPoolStorage(size_t capacity) + { + resize(capacity); + } + + ~RuntimeMarkedPoolStorage() + { + clear(); + } + + void resize(size_t capacity) + { + assert(entries_.empty()); + pool_.assign(capacity, nullptr); + refs_.assign(capacity, RefCountType(0)); + deleted_.assign(capacity, false); + lowestFreeIndex_ = Lower; + } + + size_t upper() const + { + return pool_.size(); + } + + Pair bounds() const + { + return std::make_pair(Lower, upper()); + } + + template + Type* emplace(Args&&... args) + { + const int freeIdx = findFreeIndex(); + if (freeIdx < 0) + { + return nullptr; + } + + const int id = claimHint(freeIdx, std::forward(args)...); + return id < 0 ? nullptr : get(id); + } + + Type* get(int index) + { + return valid(index) ? pool_[static_cast(index)] : nullptr; + } + + const Type* get(int index) const + { + return valid(index) ? pool_[static_cast(index)] : nullptr; + } + + void release(int index, bool force) + { + (void)force; + if (!inBounds(index)) + { + return; + } + + if (refs_[static_cast(index)] > 0) + { + deleted_[static_cast(index)] = true; + } + else + { + deleted_[static_cast(index)] = false; + remove(index); + } + } + + void lock(int index) + { + if (!inBounds(index)) + { + return; + } + ++refs_[static_cast(index)]; + assert(refs_[static_cast(index)] < std::numeric_limits::max()); + } + + bool unlock(int index) + { + if (!inBounds(index) || refs_[static_cast(index)] == 0) + { + return false; + } + + const size_t internalIndex = static_cast(index); + if (--refs_[internalIndex] == 0 && deleted_[internalIndex]) + { + remove(index); + return true; + } + return false; + } + + Iterator begin() + { + return Iterator(*this, entries_, entries_.begin()); + } + + Iterator end() + { + return Iterator(*this, entries_, entries_.end()); + } + + void clear() + { + for (Interface* const entry : entries_) + { + eventDispatcher_.dispatch(&PoolEventHandler::onPoolEntryDestroyed, *entry); + delete static_cast(entry); + } + entries_.clear(); + std::fill(pool_.begin(), pool_.end(), nullptr); + std::fill(refs_.begin(), refs_.end(), RefCountType(0)); + std::fill(deleted_.begin(), deleted_.end(), false); + lowestFreeIndex_ = Lower; + } + + const FlatPtrHashSet& _entries() const + { + return entries_; + } + + IEventDispatcher>& getEventDispatcher() + { + return eventDispatcher_; + } + + private: + bool inBounds(int index) const + { + return index >= static_cast(Lower) && static_cast(index) < pool_.size(); + } + + bool valid(int index) const + { + return inBounds(index) && pool_[static_cast(index)] != nullptr; + } + + int findFreeIndex() const + { + for (size_t index = static_cast(lowestFreeIndex_); index < pool_.size(); ++index) + { + if (pool_[index] == nullptr) + { + return static_cast(index); + } + } + return -1; + } + + template + int claim(Args&&... args) + { + const int freeIdx = findFreeIndex(); + if (freeIdx < 0) + { + return -1; + } + + if (freeIdx == lowestFreeIndex_) + { + ++lowestFreeIndex_; + } + claimAt(freeIdx, std::forward(args)...); + return freeIdx; + } + + template + int claimHint(int hint, Args&&... args) + { + if (inBounds(hint) && !valid(hint)) + { + if (hint == lowestFreeIndex_) + { + ++lowestFreeIndex_; + } + claimAt(hint, std::forward(args)...); + return hint; + } + return claim(std::forward(args)...); + } + + template + void claimAt(int index, Args&&... args) + { + const size_t internalIndex = static_cast(index); + pool_[internalIndex] = new Type(std::forward(args)...); + entries_.insert(pool_[internalIndex]); + if constexpr (std::is_base_of::value) + { + pool_[internalIndex]->poolID = index; + } + eventDispatcher_.dispatch(&PoolEventHandler::onPoolEntryCreated, *pool_[internalIndex]); + } + + void remove(int index) + { + if (!valid(index)) + { + return; + } + + const size_t internalIndex = static_cast(index); + if (index < lowestFreeIndex_) + { + lowestFreeIndex_ = index; + } + Type* entry = pool_[internalIndex]; + entries_.erase(entry); + eventDispatcher_.dispatch(&PoolEventHandler::onPoolEntryDestroyed, *entry); + delete entry; + pool_[internalIndex] = nullptr; + deleted_[internalIndex] = false; + } + + std::vector pool_; + std::vector refs_; + std::vector deleted_; + FlatPtrHashSet entries_; + int lowestFreeIndex_ = Lower; + DefaultEventDispatcher> eventDispatcher_; + }; +} + class PlayerTextDrawData final : public IPlayerTextDrawData { private: IPlayer& player; - MarkedPoolStorage storage; + int globalTextDrawPoolSize; + RuntimeMarkedPoolStorage storage; bool selecting; public: @@ -25,8 +269,10 @@ class PlayerTextDrawData final : public IPlayerTextDrawData selecting = false; } - PlayerTextDrawData(IPlayer& player) + PlayerTextDrawData(IPlayer& player, int playerTextDrawPoolSize, int globalTextDrawPoolSize) : player(player) + , globalTextDrawPoolSize(globalTextDrawPoolSize) + , storage(playerTextDrawPoolSize) , selecting(false) { } @@ -56,12 +302,12 @@ class PlayerTextDrawData final : public IPlayerTextDrawData IPlayerTextDraw* create(Vector2 position, StringView text) override { - return storage.emplace(player, position, text); + return storage.emplace(globalTextDrawPoolSize, player, position, text); } IPlayerTextDraw* create(Vector2 position, int model) override { - return storage.emplace(player, position, "_", TextDrawStyle_Preview, model); + return storage.emplace(globalTextDrawPoolSize, player, position, "_", TextDrawStyle_Preview, model); } void freeExtension() override @@ -77,7 +323,7 @@ class PlayerTextDrawData final : public IPlayerTextDrawData virtual Pair bounds() const override { - return std::make_pair(storage.Lower, storage.Upper); + return storage.bounds(); } IPlayerTextDraw* get(int index) override @@ -121,7 +367,9 @@ class TextDrawsComponent final : public ITextDrawsComponent, public PlayerConnec { private: ICore* core = nullptr; - MarkedPoolStorage storage; + int globalTextDrawPoolSize = GLOBAL_TEXTDRAW_POOL_SIZE; + int playerTextDrawPoolSize = PLAYER_TEXTDRAW_POOL_SIZE; + RuntimeMarkedPoolStorage storage; DefaultEventDispatcher dispatcher; public: @@ -146,6 +394,7 @@ class TextDrawsComponent final : public ITextDrawsComponent, public PlayerConnec bool onReceive(IPlayer& peer, NetworkBitStream& bs) override { NetCode::RPC::OnPlayerSelectTextDraw RPC; + RPC.GlobalTextDrawPoolSize = self.globalTextDrawPoolSize; if (!RPC.read(bs)) { return false; @@ -189,13 +438,38 @@ class TextDrawsComponent final : public ITextDrawsComponent, public PlayerConnec } playerSelectTextDrawEventHandler; TextDrawsComponent() - : playerSelectTextDrawEventHandler(*this) + : storage(GLOBAL_TEXTDRAW_POOL_SIZE) + , playerSelectTextDrawEventHandler(*this) { } void onLoad(ICore* c) override { core = c; + IConfig& config = core->getConfig(); + const int* configuredGlobalLimit = config.getInt("textdraw.global_limit"); + const int* configuredPlayerLimit = config.getInt("textdraw.player_limit"); + const int globalLimit = configuredGlobalLimit ? *configuredGlobalLimit : GLOBAL_TEXTDRAW_POOL_SIZE; + const int playerLimit = configuredPlayerLimit ? *configuredPlayerLimit : PLAYER_TEXTDRAW_POOL_SIZE; + const long long totalLimit = static_cast(globalLimit) + static_cast(playerLimit); + if (globalLimit < 1 || playerLimit < 1 || totalLimit > INVALID_TEXTDRAW) + { + core->logLn(LogLevel::Warning, + "Textdraw limits exceed the maximum allowed value, using defaults."); + } + else + { + globalTextDrawPoolSize = globalLimit; + playerTextDrawPoolSize = playerLimit; + } + if (globalTextDrawPoolSize != GLOBAL_TEXTDRAW_POOL_SIZE || playerTextDrawPoolSize != PLAYER_TEXTDRAW_POOL_SIZE) + { + core->logLn(LogLevel::Message, + "Using custom textdraw limits: global %d, player %d.", + globalTextDrawPoolSize, + playerTextDrawPoolSize); + } + storage.resize(globalTextDrawPoolSize); core->getPlayers().getPlayerConnectDispatcher().addEventHandler(this); core->getPlayers().getPoolEventDispatcher().addEventHandler(this); NetCode::RPC::OnPlayerSelectTextDraw::addEventHandler(*core, &playerSelectTextDrawEventHandler); @@ -219,7 +493,7 @@ class TextDrawsComponent final : public ITextDrawsComponent, public PlayerConnec void onPlayerConnect(IPlayer& player) override { - player.addExtension(new PlayerTextDrawData(player), true); + player.addExtension(new PlayerTextDrawData(player, playerTextDrawPoolSize, globalTextDrawPoolSize), true); } void onPoolEntryDestroyed(IPlayer& player) override @@ -240,12 +514,12 @@ class TextDrawsComponent final : public ITextDrawsComponent, public PlayerConnec ITextDraw* create(Vector2 position, StringView text) override { - return storage.emplace(position, text); + return storage.emplace(globalTextDrawPoolSize, position, text); } ITextDraw* create(Vector2 position, int model) override { - return storage.emplace(position, "_", TextDrawStyle_Preview, model); + return storage.emplace(globalTextDrawPoolSize, position, "_", TextDrawStyle_Preview, model); } void free() override @@ -255,7 +529,7 @@ class TextDrawsComponent final : public ITextDrawsComponent, public PlayerConnec virtual Pair bounds() const override { - return std::make_pair(storage.Lower, storage.Upper); + return storage.bounds(); } ITextDraw* get(int index) override diff --git a/Shared/NetCode/textdraw.hpp b/Shared/NetCode/textdraw.hpp index 92c520338..f98ec723f 100644 --- a/Shared/NetCode/textdraw.hpp +++ b/Shared/NetCode/textdraw.hpp @@ -11,6 +11,7 @@ #include #include #include +#include namespace NetCode { @@ -20,6 +21,7 @@ namespace RPC { bool PlayerTextDraw; int TextDrawID; + int GlobalTextDrawPoolSize = GLOBAL_TEXTDRAW_POOL_SIZE; bool UseBox; int Alignment; bool Proportional; @@ -48,7 +50,7 @@ namespace RPC void write(NetworkBitStream& bs) const { uint8_t flags = UseBox | (Alignment << 1) | (Proportional << 4); - bs.writeUINT16(PlayerTextDraw ? GLOBAL_TEXTDRAW_POOL_SIZE + TextDrawID : TextDrawID); + bs.writeUINT16(PlayerTextDraw ? GlobalTextDrawPoolSize + TextDrawID : TextDrawID); bs.writeUINT8(flags); bs.writeVEC2(LetterSize); bs.writeUINT32(LetterColour.ABGR()); @@ -73,6 +75,7 @@ namespace RPC { bool PlayerTextDraw; int TextDrawID; + int GlobalTextDrawPoolSize = GLOBAL_TEXTDRAW_POOL_SIZE; bool read(NetworkBitStream& bs) { @@ -81,7 +84,7 @@ namespace RPC void write(NetworkBitStream& bs) const { - bs.writeUINT16(PlayerTextDraw ? GLOBAL_TEXTDRAW_POOL_SIZE + TextDrawID : TextDrawID); + bs.writeUINT16(PlayerTextDraw ? GlobalTextDrawPoolSize + TextDrawID : TextDrawID); } }; @@ -89,6 +92,7 @@ namespace RPC { bool PlayerTextDraw; int TextDrawID; + int GlobalTextDrawPoolSize = GLOBAL_TEXTDRAW_POOL_SIZE; HybridString<256> Text; bool read(NetworkBitStream& bs) @@ -98,7 +102,7 @@ namespace RPC void write(NetworkBitStream& bs) const { - bs.writeUINT16(PlayerTextDraw ? GLOBAL_TEXTDRAW_POOL_SIZE + TextDrawID : TextDrawID); + bs.writeUINT16(PlayerTextDraw ? GlobalTextDrawPoolSize + TextDrawID : TextDrawID); bs.writeDynStr16(Text); } }; @@ -125,6 +129,7 @@ namespace RPC bool PlayerTextDraw; bool Invalid; int TextDrawID; + int GlobalTextDrawPoolSize = GLOBAL_TEXTDRAW_POOL_SIZE; bool read(NetworkBitStream& bs) { @@ -132,10 +137,10 @@ namespace RPC Invalid = TextDrawID == INVALID_TEXTDRAW; if (!Invalid) { - PlayerTextDraw = TextDrawID >= GLOBAL_TEXTDRAW_POOL_SIZE; + PlayerTextDraw = TextDrawID >= GlobalTextDrawPoolSize; if (PlayerTextDraw) { - TextDrawID -= GLOBAL_TEXTDRAW_POOL_SIZE; + TextDrawID -= GlobalTextDrawPoolSize; } } return res; From 129fb69f5b472c07b523325282442caf1bec6bf9 Mon Sep 17 00:00:00 2001 From: itsneufox <156133096+itsneufox@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:21:58 +0100 Subject: [PATCH 2/3] Make textdraw pool limits runtime configurable --- .../Components/TextDraws/textdraws_main.cpp | 348 +++++++++--------- 1 file changed, 174 insertions(+), 174 deletions(-) diff --git a/Server/Components/TextDraws/textdraws_main.cpp b/Server/Components/TextDraws/textdraws_main.cpp index 56af0eafe..8381b879d 100644 --- a/Server/Components/TextDraws/textdraws_main.cpp +++ b/Server/Components/TextDraws/textdraws_main.cpp @@ -20,239 +20,239 @@ using namespace Impl; namespace { - template - class RuntimeMarkedPoolStorage final : public NoCopy +template +class RuntimeMarkedPoolStorage final : public NoCopy +{ +public: + using Iterator = MarkedPoolIterator>; + static constexpr size_t Lower = 0; + + explicit RuntimeMarkedPoolStorage(size_t capacity) { - public: - using Iterator = MarkedPoolIterator>; - static constexpr size_t Lower = 0; + resize(capacity); + } - explicit RuntimeMarkedPoolStorage(size_t capacity) - { - resize(capacity); - } + ~RuntimeMarkedPoolStorage() + { + clear(); + } - ~RuntimeMarkedPoolStorage() - { - clear(); - } + void resize(size_t capacity) + { + assert(entries_.empty()); + pool_.assign(capacity, nullptr); + refs_.assign(capacity, RefCountType(0)); + deleted_.assign(capacity, false); + lowestFreeIndex_ = Lower; + } - void resize(size_t capacity) - { - assert(entries_.empty()); - pool_.assign(capacity, nullptr); - refs_.assign(capacity, RefCountType(0)); - deleted_.assign(capacity, false); - lowestFreeIndex_ = Lower; - } + size_t upper() const + { + return pool_.size(); + } - size_t upper() const - { - return pool_.size(); - } + Pair bounds() const + { + return std::make_pair(Lower, upper()); + } - Pair bounds() const + template + Type* emplace(Args&&... args) + { + const int freeIdx = findFreeIndex(); + if (freeIdx < 0) { - return std::make_pair(Lower, upper()); + return nullptr; } - template - Type* emplace(Args&&... args) - { - const int freeIdx = findFreeIndex(); - if (freeIdx < 0) - { - return nullptr; - } + const int id = claimHint(freeIdx, std::forward(args)...); + return id < 0 ? nullptr : get(id); + } - const int id = claimHint(freeIdx, std::forward(args)...); - return id < 0 ? nullptr : get(id); - } + Type* get(int index) + { + return valid(index) ? pool_[static_cast(index)] : nullptr; + } + + const Type* get(int index) const + { + return valid(index) ? pool_[static_cast(index)] : nullptr; + } - Type* get(int index) + void release(int index, bool force) + { + (void)force; + if (!inBounds(index)) { - return valid(index) ? pool_[static_cast(index)] : nullptr; + return; } - const Type* get(int index) const + if (refs_[static_cast(index)] > 0) { - return valid(index) ? pool_[static_cast(index)] : nullptr; + deleted_[static_cast(index)] = true; } - - void release(int index, bool force) + else { - (void)force; - if (!inBounds(index)) - { - return; - } - - if (refs_[static_cast(index)] > 0) - { - deleted_[static_cast(index)] = true; - } - else - { - deleted_[static_cast(index)] = false; - remove(index); - } + deleted_[static_cast(index)] = false; + remove(index); } + } - void lock(int index) + void lock(int index) + { + if (!inBounds(index)) { - if (!inBounds(index)) - { - return; - } - ++refs_[static_cast(index)]; - assert(refs_[static_cast(index)] < std::numeric_limits::max()); + return; } + ++refs_[static_cast(index)]; + assert(refs_[static_cast(index)] < std::numeric_limits::max()); + } - bool unlock(int index) + bool unlock(int index) + { + if (!inBounds(index) || refs_[static_cast(index)] == 0) { - if (!inBounds(index) || refs_[static_cast(index)] == 0) - { - return false; - } - - const size_t internalIndex = static_cast(index); - if (--refs_[internalIndex] == 0 && deleted_[internalIndex]) - { - remove(index); - return true; - } return false; } - Iterator begin() + const size_t internalIndex = static_cast(index); + if (--refs_[internalIndex] == 0 && deleted_[internalIndex]) { - return Iterator(*this, entries_, entries_.begin()); + remove(index); + return true; } + return false; + } - Iterator end() - { - return Iterator(*this, entries_, entries_.end()); - } + Iterator begin() + { + return Iterator(*this, entries_, entries_.begin()); + } - void clear() - { - for (Interface* const entry : entries_) - { - eventDispatcher_.dispatch(&PoolEventHandler::onPoolEntryDestroyed, *entry); - delete static_cast(entry); - } - entries_.clear(); - std::fill(pool_.begin(), pool_.end(), nullptr); - std::fill(refs_.begin(), refs_.end(), RefCountType(0)); - std::fill(deleted_.begin(), deleted_.end(), false); - lowestFreeIndex_ = Lower; - } + Iterator end() + { + return Iterator(*this, entries_, entries_.end()); + } - const FlatPtrHashSet& _entries() const + void clear() + { + for (Interface* const entry : entries_) { - return entries_; + eventDispatcher_.dispatch(&PoolEventHandler::onPoolEntryDestroyed, *entry); + delete static_cast(entry); } + entries_.clear(); + std::fill(pool_.begin(), pool_.end(), nullptr); + std::fill(refs_.begin(), refs_.end(), RefCountType(0)); + std::fill(deleted_.begin(), deleted_.end(), false); + lowestFreeIndex_ = Lower; + } - IEventDispatcher>& getEventDispatcher() - { - return eventDispatcher_; - } + const FlatPtrHashSet& _entries() const + { + return entries_; + } - private: - bool inBounds(int index) const - { - return index >= static_cast(Lower) && static_cast(index) < pool_.size(); - } + IEventDispatcher>& getEventDispatcher() + { + return eventDispatcher_; + } - bool valid(int index) const - { - return inBounds(index) && pool_[static_cast(index)] != nullptr; - } +private: + bool inBounds(int index) const + { + return index >= static_cast(Lower) && static_cast(index) < pool_.size(); + } + + bool valid(int index) const + { + return inBounds(index) && pool_[static_cast(index)] != nullptr; + } - int findFreeIndex() const + int findFreeIndex() const + { + for (size_t index = static_cast(lowestFreeIndex_); index < pool_.size(); ++index) { - for (size_t index = static_cast(lowestFreeIndex_); index < pool_.size(); ++index) + if (pool_[index] == nullptr) { - if (pool_[index] == nullptr) - { - return static_cast(index); - } + return static_cast(index); } + } + return -1; + } + + template + int claim(Args&&... args) + { + const int freeIdx = findFreeIndex(); + if (freeIdx < 0) + { return -1; } - template - int claim(Args&&... args) + if (freeIdx == lowestFreeIndex_) { - const int freeIdx = findFreeIndex(); - if (freeIdx < 0) - { - return -1; - } + ++lowestFreeIndex_; + } + claimAt(freeIdx, std::forward(args)...); + return freeIdx; + } - if (freeIdx == lowestFreeIndex_) + template + int claimHint(int hint, Args&&... args) + { + if (inBounds(hint) && !valid(hint)) + { + if (hint == lowestFreeIndex_) { ++lowestFreeIndex_; } - claimAt(freeIdx, std::forward(args)...); - return freeIdx; + claimAt(hint, std::forward(args)...); + return hint; } + return claim(std::forward(args)...); + } - template - int claimHint(int hint, Args&&... args) + template + void claimAt(int index, Args&&... args) + { + const size_t internalIndex = static_cast(index); + pool_[internalIndex] = new Type(std::forward(args)...); + entries_.insert(pool_[internalIndex]); + if constexpr (std::is_base_of::value) { - if (inBounds(hint) && !valid(hint)) - { - if (hint == lowestFreeIndex_) - { - ++lowestFreeIndex_; - } - claimAt(hint, std::forward(args)...); - return hint; - } - return claim(std::forward(args)...); + pool_[internalIndex]->poolID = index; } + eventDispatcher_.dispatch(&PoolEventHandler::onPoolEntryCreated, *pool_[internalIndex]); + } - template - void claimAt(int index, Args&&... args) + void remove(int index) + { + if (!valid(index)) { - const size_t internalIndex = static_cast(index); - pool_[internalIndex] = new Type(std::forward(args)...); - entries_.insert(pool_[internalIndex]); - if constexpr (std::is_base_of::value) - { - pool_[internalIndex]->poolID = index; - } - eventDispatcher_.dispatch(&PoolEventHandler::onPoolEntryCreated, *pool_[internalIndex]); + return; } - void remove(int index) + const size_t internalIndex = static_cast(index); + if (index < lowestFreeIndex_) { - if (!valid(index)) - { - return; - } - - const size_t internalIndex = static_cast(index); - if (index < lowestFreeIndex_) - { - lowestFreeIndex_ = index; - } - Type* entry = pool_[internalIndex]; - entries_.erase(entry); - eventDispatcher_.dispatch(&PoolEventHandler::onPoolEntryDestroyed, *entry); - delete entry; - pool_[internalIndex] = nullptr; - deleted_[internalIndex] = false; + lowestFreeIndex_ = index; } + Type* entry = pool_[internalIndex]; + entries_.erase(entry); + eventDispatcher_.dispatch(&PoolEventHandler::onPoolEntryDestroyed, *entry); + delete entry; + pool_[internalIndex] = nullptr; + deleted_[internalIndex] = false; + } - std::vector pool_; - std::vector refs_; - std::vector deleted_; - FlatPtrHashSet entries_; - int lowestFreeIndex_ = Lower; - DefaultEventDispatcher> eventDispatcher_; - }; + std::vector pool_; + std::vector refs_; + std::vector deleted_; + FlatPtrHashSet entries_; + int lowestFreeIndex_ = Lower; + DefaultEventDispatcher> eventDispatcher_; +}; } class PlayerTextDrawData final : public IPlayerTextDrawData From 48343eb531bf395f10a05183ca1e67e9e665a1a8 Mon Sep 17 00:00:00 2001 From: itsneufox <156133096+itsneufox@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:40:24 +0100 Subject: [PATCH 3/3] Fix textdraw limit validation to allow zero values --- Server/Components/TextDraws/textdraws_main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Server/Components/TextDraws/textdraws_main.cpp b/Server/Components/TextDraws/textdraws_main.cpp index 8381b879d..9fec5a283 100644 --- a/Server/Components/TextDraws/textdraws_main.cpp +++ b/Server/Components/TextDraws/textdraws_main.cpp @@ -452,7 +452,7 @@ class TextDrawsComponent final : public ITextDrawsComponent, public PlayerConnec const int globalLimit = configuredGlobalLimit ? *configuredGlobalLimit : GLOBAL_TEXTDRAW_POOL_SIZE; const int playerLimit = configuredPlayerLimit ? *configuredPlayerLimit : PLAYER_TEXTDRAW_POOL_SIZE; const long long totalLimit = static_cast(globalLimit) + static_cast(playerLimit); - if (globalLimit < 1 || playerLimit < 1 || totalLimit > INVALID_TEXTDRAW) + if (globalLimit < 0 || playerLimit < 0 || totalLimit > INVALID_TEXTDRAW) { core->logLn(LogLevel::Warning, "Textdraw limits exceed the maximum allowed value, using defaults.");